From d003bab638806df432c031f18aaf70a9005d7bb8 Mon Sep 17 00:00:00 2001 From: escalax Date: Thu, 21 Nov 2024 09:40:21 +0200 Subject: [PATCH 1/2] init escalax adapter --- modules/escalaxBidAdapter.js | 84 ++++++ modules/escalaxBidAdapter.md | 83 ++++++ test/spec/modules/escalaxBidAdapter_spec.js | 307 ++++++++++++++++++++ 3 files changed, 474 insertions(+) create mode 100644 modules/escalaxBidAdapter.js create mode 100644 modules/escalaxBidAdapter.md create mode 100644 test/spec/modules/escalaxBidAdapter_spec.js diff --git a/modules/escalaxBidAdapter.js b/modules/escalaxBidAdapter.js new file mode 100644 index 00000000000..dc5f88504f0 --- /dev/null +++ b/modules/escalaxBidAdapter.js @@ -0,0 +1,84 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'escalax'; +const ESCALAX_SOURCE_ID_MACRO = '[sourceId]'; +const ESCALAX_ACCOUNT_ID_MACRO = '[accountId]'; +const ESCALAX_SUBDOMAIN_MACRO = '[subdomain]'; +const ESCALAX_URL = `https://${ESCALAX_SUBDOMAIN_MACRO}.escalax.io/bid?type=pjs&partner=${ESCALAX_SOURCE_ID_MACRO}&token=${ESCALAX_ACCOUNT_ID_MACRO}`; +const ESCALAX_DEFAULT_CURRENCY = 'USD'; +const ESCALAX_DEFAULT_SUBDOMAIN = 'bidder_us'; + +function createImp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + imp.ext = { + [BIDDER_CODE]: { + sourceId: bidRequest.params.sourceId, + accountId: bidRequest.params.accountId, + subdomain: bidRequest.params.subdomain || ESCALAX_DEFAULT_SUBDOMAIN, + } + }; + if (!imp.bidfloor) imp.bidfloor = bidRequest.params.bidfloor; + return imp; +} + +function createRequest(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const bid = context.bidRequests[0]; + request.test = config.getConfig('debug') ? 1 : 0; + if (!request.cur) request.cur = [bid.params.currency || ESCALAX_DEFAULT_CURRENCY]; + return request; +} + +function createBidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.cur = 'USD'; + return bidResponse; +} + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 20, + }, + imp: createImp, + request: createRequest, + bidResponse: createBidResponse +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid) => { + return Boolean(bid.params.sourceId) && Boolean(bid.params.accountId); + }, + + buildRequests: (validBidRequests, bidderRequest) => { + if (validBidRequests && validBidRequests.length === 0) return []; + const { sourceId, accountId } = validBidRequests[0].params; + const subdomain = validBidRequests[0].params.subdomain; + const endpointURL = ESCALAX_URL + .replace(ESCALAX_SUBDOMAIN_MACRO, subdomain || ESCALAX_DEFAULT_SUBDOMAIN) + .replace(ESCALAX_ACCOUNT_ID_MACRO, sourceId) + .replace(ESCALAX_SOURCE_ID_MACRO, accountId); + const request = converter.toORTB({ bidRequests: validBidRequests, bidderRequest }); + return { + method: 'POST', + url: endpointURL, + data: request + }; + }, + + interpretResponse: (response, request) => { + if (response?.body) { + const bids = converter.fromORTB({ response: response.body, request: request.data }).bids; + return bids; + } + return []; + }, +}; + +registerBidder(spec); diff --git a/modules/escalaxBidAdapter.md b/modules/escalaxBidAdapter.md new file mode 100644 index 00000000000..839e9f6d796 --- /dev/null +++ b/modules/escalaxBidAdapter.md @@ -0,0 +1,83 @@ +# Overview + +``` +Module Name: Escalax SSP Bidder Adapter +Module Type: Bidder Adapter +Maintainer: connect@escalax.io +``` + +# Description + +Escalax Bidding adapter requires setup before beginning. Please contact us at + +# Test Parameters + +```js +const adUnits = [ + { + code: "banner1", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + bids: [ + { + bidder: "escalax", + params: { + accountId: "hash", + sourceId: "sourceId", + subdomain: "subdomain", + }, + }, + ], + }, + { + code: "native_example", + mediaTypes: { + native: {}, + }, + bids: [ + { + bidder: "escalax", + params: { + accountId: "hash", + sourceId: "sourceId", + subdomain: "subdomain", + }, + }, + ], + }, + { + code: "video1", + sizes: [640, 480], + mediaTypes: { + video: { + minduration: 0, + maxduration: 999, + boxingallowed: 1, + skip: 0, + mimes: ["application/javascript", "video/mp4"], + w: 1920, + h: 1080, + protocols: [2], + linearity: 1, + api: [1, 2], + }, + }, + bids: [ + { + bidder: "escalax", + params: { + accountId: "hash", + sourceId: "sourceId", + subdomain: "subdomain", + }, + }, + ], + }, +]; +``` diff --git a/test/spec/modules/escalaxBidAdapter_spec.js b/test/spec/modules/escalaxBidAdapter_spec.js new file mode 100644 index 00000000000..da2c3dab9e7 --- /dev/null +++ b/test/spec/modules/escalaxBidAdapter_spec.js @@ -0,0 +1,307 @@ +import { expect } from 'chai'; +import { spec } from 'modules/escalaxBidAdapter'; +import 'modules/priceFloors.js'; +import { newBidder } from 'src/adapters/bidderFactory'; +import { config } from '../../../src/config.js'; +import { syncAddFPDToBidderRequest } from '../../helpers/fpd.js'; + +import 'src/prebid.js'; +import 'modules/currency.js'; +import 'modules/userId/index.js'; +import 'modules/multibid/index.js'; +import 'modules/priceFloors.js'; +import 'modules/consentManagementTcf.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/schain.js'; + +const SIMPLE_BID_REQUEST = { + bidder: 'escalax', + params: { + sourceId: 'sourceId', + accountId: 'accountId', + subdomain: 'bidder_us', + }, + mediaTypes: { + banner: { + sizes: [ + [320, 250], + [300, 600], + ], + }, + }, + adUnitCode: 'div-gpt-ad-1234567890123-0', + transactionId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + bidId: 'abcdef1234567890', + bidderRequestId: '1234567890abcdef', + auctionId: 'abcdef1234567890', + sizes: [[300, 250], [160, 600]], + gdprConsent: { + apiVersion: 2, + consentString: 'CONSENT', + vendorData: { purpose: { consents: { 1: true } } }, + gdprApplies: true, + addtlConsent: '1~1.35.41.101', + }, +} + +const BANNER_BID_REQUEST = { + bidder: 'escalax', + params: { + sourceId: 'sourceId', + accountId: 'accountId', + subdomain: 'bidder_us', + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + code: 'banner_example', + timeout: 1000, +} + +const VIDEO_BID_REQUEST = { + placementCode: '/DfpAccount1/slotVideo', + bidId: 'test-bid-id-2', + mediaTypes: { + video: { + playerSize: [400, 300], + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }, + bidder: 'escalax', + params: { + sourceId: '123', + accountId: '123', + subdomain: 'bidder_us', + }, + adUnitCode: '/adunit-code/test-path', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + timeout: 1000, +} + +const NATIVE_BID_REQUEST = { + code: 'native_example', + mediaTypes: { + native: { + title: { + required: true, + len: 800 + }, + image: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + privacyLink: { + required: false + }, + body: { + required: true + }, + icon: { + required: true, + sizes: [50, 50] + } + } + }, + bidder: 'escalax', + params: { + sourceId: 'sourceId', + accountId: 'accountId', + subdomain: 'bidder_us', + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + timeout: 1000, + uspConsent: 'uspConsent' +}; + +const bidderRequest = { + refererInfo: { + page: 'https://publisher.com/home', + ref: 'https://referrer' + } +}; + +const gdprConsent = { + apiVersion: 2, + consentString: 'CONSENT', + vendorData: { purpose: { consents: { 1: true } } }, + gdprApplies: true, + addtlConsent: '1~1.35.41.101', +} + +describe('escalaxAdapter', function () { + const adapter = newBidder(spec); + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('with user privacy regulations', function () { + it('should send the Coppa "required" flag set to "1" in the request', function () { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(serverRequest.data.regs.coppa).to.equal(1); + config.getConfig.restore(); + }); + + it('should send the GDPR Consent data in the request', function () { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest({ ...bidderRequest, gdprConsent })); + expect(serverRequest.data.regs.ext.gdpr).to.exist.and.to.equal(1); + expect(serverRequest.data.user.ext.consent).to.equal('CONSENT'); + }); + + it('should send the CCPA data in the request', function () { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest({...bidderRequest, ...{ uspConsent: '1YYY' }})); + expect(serverRequest.data.regs.ext.us_privacy).to.equal('1YYY'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(BANNER_BID_REQUEST)).to.equal(true); + }); + + it('should return false when sourceId/accountId is missing', function () { + let localbid = Object.assign({}, BANNER_BID_REQUEST); + delete localbid.params.sourceId; + delete localbid.params.accountId; + expect(spec.isBidRequestValid(BANNER_BID_REQUEST)).to.equal(false); + }); + }); + + describe('build request', function () { + it('should return an empty array when no bid requests', function () { + const bidRequest = spec.buildRequests([], syncAddFPDToBidderRequest(bidderRequest)); + expect(bidRequest).to.be.an('array'); + expect(bidRequest.length).to.equal(0); + }); + + it('should return a valid bid request object', function () { + const request = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request).to.not.equal('array'); + expect(request.data).to.be.an('object'); + expect(request.method).to.equal('POST'); + expect(request.url).to.not.equal(''); + expect(request.url).to.not.equal(undefined); + expect(request.url).to.not.equal(null); + + expect(request.data.site).to.have.property('page'); + expect(request.data.site).to.have.property('domain'); + expect(request.data).to.have.property('id'); + expect(request.data).to.have.property('imp'); + expect(request.data).to.have.property('device'); + }); + + it('should return a valid bid BANNER request object', function () { + const request = spec.buildRequests([BANNER_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0].banner).to.exist; + expect(request.data.imp[0].banner.format[0].w).to.be.an('number'); + expect(request.data.imp[0].banner.format[0].h).to.be.an('number'); + }); + + if (FEATURES.VIDEO) { + it('should return a valid bid VIDEO request object', function () { + const request = spec.buildRequests([VIDEO_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0].video).to.exist; + expect(request.data.imp[0].video.w).to.be.an('number'); + expect(request.data.imp[0].video.h).to.be.an('number'); + }); + } + + it('should return a valid bid NATIVE request object', function () { + const request = spec.buildRequests([NATIVE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0]).to.be.an('object'); + }); + }) + + describe('interpretResponse', function () { + let bidRequests, bidderRequest; + beforeEach(function () { + bidRequests = [{ + 'bidId': '28ffdk2B952532', + 'bidder': 'escalax', + 'userId': { + 'freepassId': { + 'userIp': '172.21.0.1', + 'userId': '123', + 'commonId': 'commonIdValue' + } + }, + 'adUnitCode': 'adunit-code', + 'params': { + 'publisherId': 'publisherIdValue' + } + }]; + bidderRequest = {}; + }); + + it('Empty response must return empty array', function () { + const emptyResponse = null; + let response = spec.interpretResponse(emptyResponse, BANNER_BID_REQUEST); + + expect(response).to.be.an('array').that.is.empty; + }) + + it('Should interpret banner response', function () { + const serverResponse = { + body: { + 'cur': 'USD', + 'seatbid': [{ + 'bid': [{ + 'impid': '28ffdk2B952532', + 'price': 97, + 'adm': '', + 'w': 300, + 'h': 250, + 'crid': 'creative0' + }] + }] + } + }; + it('should interpret server response', function () { + const bidRequest = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse(serverResponse, bidRequest); + expect(bids).to.be.an('array'); + const bid = bids[0]; + expect(bid).to.be.an('object'); + expect(bid.currency).to.equal('USD'); + expect(bid.cpm).to.equal(97); + expect(bid.ad).to.equal(ad) + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('creative0'); + }); + }) + }); +}); From a9abcd5c1a40c3a57bf9d909d2c899909ed365a7 Mon Sep 17 00:00:00 2001 From: escalax Date: Mon, 16 Dec 2024 10:14:46 +0200 Subject: [PATCH 2/2] region substitution based on time zone --- modules/escalaxBidAdapter.js | 26 +++++++++++++++++++-- modules/escalaxBidAdapter.md | 3 --- test/spec/modules/escalaxBidAdapter_spec.js | 4 ---- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/modules/escalaxBidAdapter.js b/modules/escalaxBidAdapter.js index dc5f88504f0..70a10d748bc 100644 --- a/modules/escalaxBidAdapter.js +++ b/modules/escalaxBidAdapter.js @@ -17,7 +17,6 @@ function createImp(buildImp, bidRequest, context) { [BIDDER_CODE]: { sourceId: bidRequest.params.sourceId, accountId: bidRequest.params.accountId, - subdomain: bidRequest.params.subdomain || ESCALAX_DEFAULT_SUBDOMAIN, } }; if (!imp.bidfloor) imp.bidfloor = bidRequest.params.bidfloor; @@ -38,6 +37,29 @@ function createBidResponse(buildBidResponse, bid, context) { return bidResponse; } +function getSubdomain() { + const regionMap = { + 'Europe': 'bidder_eu', + 'Africa': 'bidder_eu', + 'Atlantic': 'bidder_eu', + 'Arctic': 'bidder_eu', + 'Asia': 'bidder_apac', + 'Australia': 'bidder_apac', + 'Antarctica': 'bidder_apac', + 'Pacific': 'bidder_apac', + 'Indian': 'bidder_apac', + 'America': 'bidder_us' + }; + + try { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const region = timezone.split('/')[0]; + return regionMap[region] || 'bidder_us'; + } catch (err) { + return 'bidder_us'; + } +} + const converter = ortbConverter({ context: { netRevenue: true, @@ -59,7 +81,7 @@ export const spec = { buildRequests: (validBidRequests, bidderRequest) => { if (validBidRequests && validBidRequests.length === 0) return []; const { sourceId, accountId } = validBidRequests[0].params; - const subdomain = validBidRequests[0].params.subdomain; + const subdomain = getSubdomain(); const endpointURL = ESCALAX_URL .replace(ESCALAX_SUBDOMAIN_MACRO, subdomain || ESCALAX_DEFAULT_SUBDOMAIN) .replace(ESCALAX_ACCOUNT_ID_MACRO, sourceId) diff --git a/modules/escalaxBidAdapter.md b/modules/escalaxBidAdapter.md index 839e9f6d796..7cd45cabdc6 100644 --- a/modules/escalaxBidAdapter.md +++ b/modules/escalaxBidAdapter.md @@ -30,7 +30,6 @@ const adUnits = [ params: { accountId: "hash", sourceId: "sourceId", - subdomain: "subdomain", }, }, ], @@ -46,7 +45,6 @@ const adUnits = [ params: { accountId: "hash", sourceId: "sourceId", - subdomain: "subdomain", }, }, ], @@ -74,7 +72,6 @@ const adUnits = [ params: { accountId: "hash", sourceId: "sourceId", - subdomain: "subdomain", }, }, ], diff --git a/test/spec/modules/escalaxBidAdapter_spec.js b/test/spec/modules/escalaxBidAdapter_spec.js index da2c3dab9e7..bc375ff3dae 100644 --- a/test/spec/modules/escalaxBidAdapter_spec.js +++ b/test/spec/modules/escalaxBidAdapter_spec.js @@ -19,7 +19,6 @@ const SIMPLE_BID_REQUEST = { params: { sourceId: 'sourceId', accountId: 'accountId', - subdomain: 'bidder_us', }, mediaTypes: { banner: { @@ -49,7 +48,6 @@ const BANNER_BID_REQUEST = { params: { sourceId: 'sourceId', accountId: 'accountId', - subdomain: 'bidder_us', }, mediaTypes: { banner: { @@ -88,7 +86,6 @@ const VIDEO_BID_REQUEST = { params: { sourceId: '123', accountId: '123', - subdomain: 'bidder_us', }, adUnitCode: '/adunit-code/test-path', bidderRequestId: 'test-bid-request-1', @@ -131,7 +128,6 @@ const NATIVE_BID_REQUEST = { params: { sourceId: 'sourceId', accountId: 'accountId', - subdomain: 'bidder_us', }, adUnitCode: '/adunit-code/test-path', bidId: 'test-bid-id-1',