From b7d4c283ee3b52b86e1dc84e5f37229d52c58df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Rufiange?= Date: Wed, 18 Sep 2024 23:00:32 -0400 Subject: [PATCH 1/9] feat: contxtful bid adapter --- modules/contxtfulBidAdapter.js | 197 ++++++++++++ modules/contxtfulBidAdapter.md | 98 ++++++ test/spec/modules/contxtfulBidAdapter_spec.js | 296 ++++++++++++++++++ 3 files changed, 591 insertions(+) create mode 100644 modules/contxtfulBidAdapter.js create mode 100644 modules/contxtfulBidAdapter.md create mode 100644 test/spec/modules/contxtfulBidAdapter_spec.js diff --git a/modules/contxtfulBidAdapter.js b/modules/contxtfulBidAdapter.js new file mode 100644 index 00000000000..493d983f340 --- /dev/null +++ b/modules/contxtfulBidAdapter.js @@ -0,0 +1,197 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { _each, buildUrl, isStr, isEmptyStr, logInfo, ajax, logError } from '../src/utils.js'; +import { config } from '../src/config.js'; +import { + isBidRequestValid, + interpretResponse, + getUserSyncs as getUserSyncsLib, +} from '../libraries/teqblazeUtils/bidderUtils.js'; + +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; + +// Constants +const BIDDER_CODE = 'contxtful'; +const BIDDER_ENDPOINT = 'prebid.receptivity.io'; +const MONITORING_ENDPOINT = 'monitoring.receptivity.io'; +const DEFAULT_NET_REVENUE = true; +const DEFAULT_TTL = 300; + +// ORTB conversion +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_TTL + }, + imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const reqData = buildRequest(imps, bidderRequest, context); + return reqData; + }, + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + return bidResponse; + } +}); + +// Get Bid Floor +const _getRequestBidFloor = (mediaTypes, paramsBidFloor, bid) => { + const bidMediaType = Object.keys(mediaTypes)[0] || 'banner'; + const bidFloor = { floor: 0, currency: 'USD' }; + + if (typeof bid.getFloor === 'function') { + const { currency, floor } = bid.getFloor({ + mediaType: bidMediaType, + size: '*' + }); + floor && (bidFloor.floor = floor); + currency && (bidFloor.currency = currency); + } else if (paramsBidFloor) { + bidFloor.floor = paramsBidFloor + } + + return bidFloor; +} + +// Get Parameters from the config. +const extractParameters = (config) => { + const version = config?.contxtful?.version; + if (!isStr(version) || isEmptyStr(version)) { + throw Error(`contxfulBidAdapter: contxtful.version should be a non-empty string`); + } + + const customer = config?.contxtful?.customer; + if (!isStr(customer) || isEmptyStr(customer)) { + throw Error(`contxfulBidAdapter: contxtful.customer should be a non-empty string`); + } + + return { version, customer }; +} + +// Construct the Payload towards the Bidding endpoint +const buildRequests = (validBidRequests = [], bidderRequest = {}) => { + const ortb2 = converter.toORTB({bidderRequest: bidderRequest, bidRequests: validBidRequests}); + + const bidRequests = []; + _each(validBidRequests, bidRequest => { + const { + mediaTypes = {}, + params = {}, + } = bidRequest; + bidRequest.bidFloor = _getRequestBidFloor(mediaTypes, params.bidfloor, bidRequest); + bidRequests.push(bidRequest) + }); + const pubConfig = config.getConfig(); + const {version, customer} = extractParameters(pubConfig) + const adapterUrl = buildUrl({ + protocol: 'https', + host: BIDDER_ENDPOINT, + pathname: `/${version}/prebid/${customer}/bid`, + }); + + // https://docs.prebid.org/dev-docs/bidder-adaptor.html + let req = { + url: adapterUrl, + method: 'POST', + data: { + ortb2, + bidRequests, + bidderRequest, + pubConfig, + }, + }; + + return req; +}; + +// Prepare a sync object compatible with getUserSyncs. +const buildSyncEntry = (sync, gdprConsent, uspConsent, gppConsent) => { + const syncDefaults = getUserSyncsLib('')(sync, gdprConsent, uspConsent, gppConsent); + const syncDefaultEntry = syncDefaults?.find(item => item.url !== undefined); + if (!syncDefaultEntry) return null; + + const defaultParams = syncDefaultEntry.url.split('?')[1] ?? ''; + const syncUrl = defaultParams ? `${sync.url}?${defaultParams}` : sync.url; + return { + ...syncDefaultEntry, + url: syncUrl, + }; +}; + +// Returns the list of user synchronization objects. +const getUserSyncs = (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { + const serverSyncsData = serverResponses.flatMap(response => response.body || []) + .map(data => data.syncs) + .find(syncs => Array.isArray(syncs) && syncs.length > 0) || []; + const userSyncs = serverSyncsData + .map(sync => buildSyncEntry(sync, gdprConsent, uspConsent, gppConsent)) + .filter(Boolean); // Filter out nulls + return userSyncs; +}; + +// Retrieve the sampling rate for events +const getSamplingRate = (bidderConfig, eventType) => { + const entry = Object.entries(bidderConfig?.contxtful?.sampling ?? {}).find(([key, value]) => key.toLowerCase() === eventType.toLowerCase()); + return entry ? entry[1] : 0.001; +}; + +// Handles the logging of events +const logEvent = (eventType, data, samplingEnabled) => { + try { + // Log event + logInfo(BIDDER_CODE, `[${eventType}] ${JSON.stringify(data)}`); + + // Get Config + const bidderConfig = config.getConfig(); + const {version, customer} = extractParameters(bidderConfig); + + // Sampled monitoring + if (samplingEnabled) { + const shouldSampleDice = Math.random(); + const samplingRate = getSamplingRate(bidderConfig, eventType); + if (shouldSampleDice >= samplingRate) { + return; // Don't sample + } + } + + const options = { + method: 'POST', + contentType: 'application/json', + withCredentials: true, + }; + + const request = { type: eventType, data }; + + const eventUrl = buildUrl({ + protocol: 'https', + host: MONITORING_ENDPOINT, + pathname: `/${version}/prebid/${customer}/log/${eventType}`, + }); + + ajax(eventUrl, null, request, options); + } catch (error) { + logError(`Failed to log event: ${eventType}`); + } +}; + +// Bidder exposed specification +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: isBidRequestValid(['placementId']), + buildRequests, + interpretResponse, + getUserSyncs, + onBidWon: function(bid) { logEvent('onBidWon', bid, false); }, + onBidBillable: function(bid) { logEvent('onBidBillable', bid, false); }, + onAdRenderSucceeded: function(bid) { logEvent('onAdRenderSucceeded', bid, false); }, + onSetTargeting: function(bid) { }, + onTimeout: function(timeoutData) { logEvent('onTimeout', timeoutData, true); }, + onBidderError: function(args) { logEvent('onBidderError', args, true); }, +}; + +registerBidder(spec); diff --git a/modules/contxtfulBidAdapter.md b/modules/contxtfulBidAdapter.md new file mode 100644 index 00000000000..87a78c38a85 --- /dev/null +++ b/modules/contxtfulBidAdapter.md @@ -0,0 +1,98 @@ +# Overview + +``` +Module Name: Contxtful Bidder Adapter +Module Type: Bidder Adapter +Maintainer: contact@contxtful.com +``` + +# Description + +The Contxtful Bidder Adapter supports all mediatypes and connects to demand sources for bids. + +# Configuration +## Global Configuration +Contxtful uses the global configuration to store params once instead of duplicating for each ad unit. +Also, enabling user syncing greatly increases match rates and monetization. +Be sure to call `pbjs.setConfig()` only once. + +```javascript +pbjs.setConfig({ + debug: false, + contxtful: { + customer: '', // Required + version: '', // Required + }, + userSync: { + filterSettings: { + iframe: { + bidders: ['contxtful'], + filter: 'include' + } + } + } + // [...] +}); +``` + +## Bidder Setting +Contxtful leverages local storage for user syncing. + +```javascript +pbjs.bidderSettings = { + contxtful: { + storageAllowed: true + } +} +``` + +# Example Ad-units configuration +```javascript +var adUnits = [ + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [{ + bidder: 'contxtful', + }] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [{ + bidder: 'contxtful', + }] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [{ + bidder: 'contxtful', + }] + } +]; +``` diff --git a/test/spec/modules/contxtfulBidAdapter_spec.js b/test/spec/modules/contxtfulBidAdapter_spec.js new file mode 100644 index 00000000000..5822c0c20a3 --- /dev/null +++ b/test/spec/modules/contxtfulBidAdapter_spec.js @@ -0,0 +1,296 @@ +import { spec } from 'modules/contxtfulBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; +const VERSION = 'v1'; +const CUSTOMER = 'CUSTOMER'; +const BIDDER_ENDPOINT = 'prebid.receptivity.io'; +const RX_FROM_API = { ReceptivityState: 'Receptive', test_info: 'rx_from_engine' }; + +describe('contxtful bid adapter', function () { + const adapter = newBidder(spec); + + describe('is a functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('valid code', function () { + it('should return the bidder code of contxtful', function () { + expect(spec.code).to.eql('contxtful'); + }); + }); + + let bidRequests = + [ + { + bidder: 'contxtful', + bidId: 'bId1', + custom_param_1: 'value_1', + transactionId: 'tId1', + params: { + bcat: ['cat1', 'cat2'], + badv: ['adv1', 'adv2'], + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + }, + }, + ortb2Imp: { + ext: { + tid: 't-id-test-1', + gpid: 'gpid-id-unitest-1' + }, + }, + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'schain-seller-1.com', + sid: '00001', + hp: 1, + }, + ], + }, + getFloor: () => ({ currency: 'CAD', floor: 10 }), + } + ]; + + let expectedReceptivityData = { + rx: RX_FROM_API, + params: { + ev: VERSION, + ci: CUSTOMER, + }, + }; + + let bidderRequest = { + refererInfo: { + ref: 'https://my-referer-custom.com', + }, + ortb2: { + source: { + tid: 'auction-id', + }, + property_1: 'string_val_1', + regs: { + coppa: 1, + ext: { + us_privacy: '12345' + } + }, + user: { + data: [ + { + name: 'contxtful', + ext: expectedReceptivityData + } + ], + ext: { + eids: [ + { + source: 'id5-sync.com', + uids: [ + { + atype: 1, + id: 'fake-id5id', + }, + ] + } + ] + } + } + + }, + timeout: 1234, + uspConsent: '12345' + }; + + describe('valid configuration', function() { + const theories = [ + [ + null, + 'contxfulBidAdapter: contxtful.version should be a non-empty string', + 'null object for config', + ], + [ + {}, + 'contxfulBidAdapter: contxtful.version should be a non-empty string', + 'empty object for config', + ], + [ + { customer: CUSTOMER }, + 'contxfulBidAdapter: contxtful.version should be a non-empty string', + 'customer only in config', + ], + [ + { version: VERSION }, + 'contxfulBidAdapter: contxtful.customer should be a non-empty string', + 'version only in config', + ], + [ + { customer: CUSTOMER, version: '' }, + 'contxfulBidAdapter: contxtful.version should be a non-empty string', + 'empty string for version', + ], + [ + { customer: '', version: VERSION }, + 'contxfulBidAdapter: contxtful.customer should be a non-empty string', + 'empty string for customer', + ], + [ + { customer: '', version: '' }, + 'contxfulBidAdapter: contxtful.version should be a non-empty string', + 'empty string for version & customer', + ], + ]; + + theories.forEach(([params, expectedErrorMessage, description]) => { + it('detects invalid configuration and throws the expected error (' + description + ')', () => { + config.setConfig({ + contxtful: params + }); + expect(() => spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + })).to.throw( + expectedErrorMessage + ); + }); + }); + + it('uses a valid configuration and returns the right url', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION} + }); + const bidRequest = spec.buildRequests(bidRequests); + expect(bidRequest.url).to.eq('https://' + BIDDER_ENDPOINT + `/${VERSION}/prebid/${CUSTOMER}/bid`) + }); + + it('will take specific ortb2 configuration parameters and returns it in ortb2 object', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION}, + }); + const bidRequest = spec.buildRequests(bidRequests, bidderRequest); + expect(bidRequest.data.ortb2.property_1).to.equal('string_val_1'); + }); + }); + + describe('valid bid request', function () { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION}, + }); + const bidRequest = spec.buildRequests(bidRequests, bidderRequest); + + it('will return a data property containing properties ortb2, bidRequests, bidderRequest and pubConfig', () => { + expect(bidRequest.data).not.to.be.undefined; + expect(bidRequest.data.ortb2).not.to.be.undefined; + expect(bidRequest.data.bidRequests).not.to.be.undefined; + expect(bidRequest.data.bidderRequest).not.to.be.undefined; + expect(bidRequest.data.pubConfig).not.to.be.undefined; + }); + + it('will take custom parameters in the bid request and within the bidRequests array', () => { + expect(bidRequest.data.bidRequests[0].custom_param_1).to.equal('value_1') + }); + + it('will return any supply chain parameters within the bidRequests array', () => { + expect(bidRequest.data.bidRequests[0].schain.ver).to.equal('1.0'); + }); + + it('will return floor request within the bidFloor parameter in the bidRequests array', () => { + expect(bidRequest.data.bidRequests[0].bidFloor.currency).to.equal('CAD'); + expect(bidRequest.data.bidRequests[0].bidFloor.floor).to.equal(10); + }); + + it('will return the usp string in the uspConsent parameter within the bidderRequest property', () => { + expect(bidRequest.data.bidderRequest.uspConsent).to.equal('12345'); + }); + + it('will contains impressions array on ortb2.imp object for all ad units', () => { + expect(bidRequest.data.ortb2.imp.length).to.equal(1); + expect(bidRequest.data.ortb2.imp[0].id).to.equal('bId1'); + }); + + it('will contains the registration on ortb2.regs object', () => { + expect(bidRequest.data.ortb2.regs).not.to.be.undefined; + expect(bidRequest.data.ortb2.regs.coppa).to.equal(1); + expect(bidRequest.data.ortb2.regs.ext.us_privacy).to.equal('12345') + }) + + it('will contains the eids modules within the ortb2.user.ext.eids', () => { + expect(bidRequest.data.ortb2.user.ext.eids).not.to.be.undefined; + expect(bidRequest.data.ortb2.user.ext.eids[0].source).to.equal('id5-sync.com'); + expect(bidRequest.data.ortb2.user.ext.eids[0].uids[0].id).to.equal('fake-id5id'); + }); + + it('will contains the receptivity value within the ortb2.user.data with contxtful name', () => { + let obtained_receptivity_data = bidRequest.data.ortb2.user.data.filter(function(userData) { + return userData.name == 'contxtful'; + }); + expect(obtained_receptivity_data.length).to.equal(1); + expect(obtained_receptivity_data[0].ext).to.deep.equal(expectedReceptivityData); + }); + + it('will contains ortb2Imp of the bid request within the ortb2.imp.ext', () => { + let first_imp = bidRequest.data.ortb2.imp[0]; + expect(first_imp.ext).not.to.be.undefined; + expect(first_imp.ext.tid).to.equal('t-id-test-1'); + expect(first_imp.ext.gpid).to.equal('gpid-id-unitest-1'); + }); + }); + + describe('valid bid request with no floor module', () => { + let noFloorsBidRequests = + [ + { + bidder: 'contxtful', + bidId: 'bId1', + transactionId: 'tId1', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + }, + }, + }, + { + bidder: 'contxtful', + bidId: 'bId2', + transactionId: 'tId2', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + }, + }, + params: { + bidfloor: 54 + } + }, + ]; + + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION}, + }); + + const bidRequest = spec.buildRequests(noFloorsBidRequests, bidderRequest); + it('will contains default value of floor if the bid request do not contains floor function', () => { + expect(bidRequest.data.bidRequests[0].bidFloor.currency).to.equal('USD'); + expect(bidRequest.data.bidRequests[0].bidFloor.floor).to.equal(0); + }); + + it('will take the param.bidfloor as floor value if possible', () => { + expect(bidRequest.data.bidRequests[1].bidFloor.currency).to.equal('USD'); + expect(bidRequest.data.bidRequests[1].bidFloor.floor).to.equal(54); + }) + }) +}); From 298edd3f83f9613fbae1370f5ccc17531156b29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Rufiange?= Date: Thu, 19 Sep 2024 00:32:59 -0400 Subject: [PATCH 2/9] fix: ajax --- modules/contxtfulBidAdapter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/contxtfulBidAdapter.js b/modules/contxtfulBidAdapter.js index 493d983f340..b0aac71a2fb 100644 --- a/modules/contxtfulBidAdapter.js +++ b/modules/contxtfulBidAdapter.js @@ -1,6 +1,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { _each, buildUrl, isStr, isEmptyStr, logInfo, ajax, logError } from '../src/utils.js'; +import { _each, buildUrl, isStr, isEmptyStr, logInfo, logError } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import { isBidRequestValid, From d86843312fc08df837f7e88c143a864515bd6dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Rufiange?= Date: Thu, 19 Sep 2024 12:40:16 -0400 Subject: [PATCH 3/9] fix: config, valid bid request --- modules/contxtfulBidAdapter.js | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/modules/contxtfulBidAdapter.js b/modules/contxtfulBidAdapter.js index b0aac71a2fb..173039b15a4 100644 --- a/modules/contxtfulBidAdapter.js +++ b/modules/contxtfulBidAdapter.js @@ -2,7 +2,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { _each, buildUrl, isStr, isEmptyStr, logInfo, logError } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; -import { config } from '../src/config.js'; +import { config as pbjsConfig } from '../src/config.js'; import { isBidRequestValid, interpretResponse, @@ -85,8 +85,8 @@ const buildRequests = (validBidRequests = [], bidderRequest = {}) => { bidRequest.bidFloor = _getRequestBidFloor(mediaTypes, params.bidfloor, bidRequest); bidRequests.push(bidRequest) }); - const pubConfig = config.getConfig(); - const {version, customer} = extractParameters(pubConfig) + const config = pbjsConfig.getConfig(); + const {version, customer} = extractParameters(config) const adapterUrl = buildUrl({ protocol: 'https', host: BIDDER_ENDPOINT, @@ -101,7 +101,7 @@ const buildRequests = (validBidRequests = [], bidderRequest = {}) => { ortb2, bidRequests, bidderRequest, - pubConfig, + config, }, }; @@ -109,26 +109,19 @@ const buildRequests = (validBidRequests = [], bidderRequest = {}) => { }; // Prepare a sync object compatible with getUserSyncs. -const buildSyncEntry = (sync, gdprConsent, uspConsent, gppConsent) => { - const syncDefaults = getUserSyncsLib('')(sync, gdprConsent, uspConsent, gppConsent); - const syncDefaultEntry = syncDefaults?.find(item => item.url !== undefined); - if (!syncDefaultEntry) return null; - - const defaultParams = syncDefaultEntry.url.split('?')[1] ?? ''; - const syncUrl = defaultParams ? `${sync.url}?${defaultParams}` : sync.url; - return { - ...syncDefaultEntry, - url: syncUrl, - }; +const buildSyncEntry = (sync, syncOptions, gdprConsent, uspConsent, gppConsent) => { + const userSyncsLibList = getUserSyncsLib(sync?.url ?? '')(syncOptions, null, gdprConsent, uspConsent, gppConsent); + const userSyncsLibObject = userSyncsLibList?.find(item => item.url !== undefined); + return userSyncsLibObject ?? {}; }; // Returns the list of user synchronization objects. const getUserSyncs = (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { - const serverSyncsData = serverResponses.flatMap(response => response.body || []) + const serverSyncsData = serverResponses?.flatMap(response => response.body || []) .map(data => data.syncs) .find(syncs => Array.isArray(syncs) && syncs.length > 0) || []; const userSyncs = serverSyncsData - .map(sync => buildSyncEntry(sync, gdprConsent, uspConsent, gppConsent)) + .map(sync => buildSyncEntry(sync, syncOptions, gdprConsent, uspConsent, gppConsent)) .filter(Boolean); // Filter out nulls return userSyncs; }; @@ -146,7 +139,7 @@ const logEvent = (eventType, data, samplingEnabled) => { logInfo(BIDDER_CODE, `[${eventType}] ${JSON.stringify(data)}`); // Get Config - const bidderConfig = config.getConfig(); + const bidderConfig = pbjsConfig.getConfig(); const {version, customer} = extractParameters(bidderConfig); // Sampled monitoring @@ -183,7 +176,7 @@ export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: isBidRequestValid(['placementId']), + isBidRequestValid, buildRequests, interpretResponse, getUserSyncs, From 1d424c63e31c6672982ddc99ca104f0c9cb0af86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Rufiange?= Date: Thu, 19 Sep 2024 17:34:34 -0400 Subject: [PATCH 4/9] fix: config, valid bid request --- modules/contxtfulBidAdapter.js | 40 ++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/modules/contxtfulBidAdapter.js b/modules/contxtfulBidAdapter.js index 173039b15a4..c17abf599ff 100644 --- a/modules/contxtfulBidAdapter.js +++ b/modules/contxtfulBidAdapter.js @@ -109,10 +109,28 @@ const buildRequests = (validBidRequests = [], bidderRequest = {}) => { }; // Prepare a sync object compatible with getUserSyncs. +const constructUrl = (userSyncDefault, sync, syncOptions, gdprConsent, uspConsent, gppConsent) => { + const userSyncDefaultParts = userSyncDefault?.url?.split('?'); + const syncurlPart = (sync?.url ?? '').split('?'); + const type = userSyncDefault.type; + if (!syncurlPart || !userSyncDefaultParts) { + return null; + } + let url = syncurlPart[0] + '/' + type + '?'; + if (syncurlPart.length > 1) { + url += syncurlPart[1] + '&'; + } + url += userSyncDefaultParts[1]; + return { + type, + url, + } +}; + const buildSyncEntry = (sync, syncOptions, gdprConsent, uspConsent, gppConsent) => { - const userSyncsLibList = getUserSyncsLib(sync?.url ?? '')(syncOptions, null, gdprConsent, uspConsent, gppConsent); - const userSyncsLibObject = userSyncsLibList?.find(item => item.url !== undefined); - return userSyncsLibObject ?? {}; + const userSyncsLibList = getUserSyncsLib('')(syncOptions, null, gdprConsent, uspConsent, gppConsent); + const userSyncDefault = userSyncsLibList?.find(item => item.url !== undefined); + return constructUrl(userSyncDefault, sync, syncOptions, gdprConsent, uspConsent, gppConsent); }; // Returns the list of user synchronization objects. @@ -133,7 +151,7 @@ const getSamplingRate = (bidderConfig, eventType) => { }; // Handles the logging of events -const logEvent = (eventType, data, samplingEnabled) => { +const logEvent = (eventType, data, samplingEnabled, ajaxMethod = ajax) => { try { // Log event logInfo(BIDDER_CODE, `[${eventType}] ${JSON.stringify(data)}`); @@ -157,7 +175,7 @@ const logEvent = (eventType, data, samplingEnabled) => { withCredentials: true, }; - const request = { type: eventType, data }; + const payload = { type: eventType, data }; const eventUrl = buildUrl({ protocol: 'https', @@ -165,7 +183,7 @@ const logEvent = (eventType, data, samplingEnabled) => { pathname: `/${version}/prebid/${customer}/log/${eventType}`, }); - ajax(eventUrl, null, request, options); + ajaxMethod(eventUrl, null, JSON.stringify(payload), options); } catch (error) { logError(`Failed to log event: ${eventType}`); } @@ -180,12 +198,12 @@ export const spec = { buildRequests, interpretResponse, getUserSyncs, - onBidWon: function(bid) { logEvent('onBidWon', bid, false); }, - onBidBillable: function(bid) { logEvent('onBidBillable', bid, false); }, - onAdRenderSucceeded: function(bid) { logEvent('onAdRenderSucceeded', bid, false); }, + onBidWon: function(bid, ajaxMethod = ajax) { logEvent('onBidWon', bid, false, ajaxMethod); }, + onBidBillable: function(bid, ajaxMethod = ajax) { logEvent('onBidBillable', bid, false, ajaxMethod); }, + onAdRenderSucceeded: function(bid, ajaxMethod = ajax) { logEvent('onAdRenderSucceeded', bid, false, ajaxMethod); }, onSetTargeting: function(bid) { }, - onTimeout: function(timeoutData) { logEvent('onTimeout', timeoutData, true); }, - onBidderError: function(args) { logEvent('onBidderError', args, true); }, + onTimeout: function(timeoutData, ajaxMethod = ajax) { logEvent('onTimeout', timeoutData, true, ajaxMethod); }, + onBidderError: function(args, ajaxMethod = ajax) { logEvent('onBidderError', args, true, ajaxMethod); }, }; registerBidder(spec); From 0173c3c43d2af772468e1950510e7c503acfbb0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Rufiange?= Date: Thu, 19 Sep 2024 17:34:44 -0400 Subject: [PATCH 5/9] fix: tests --- test/spec/modules/contxtfulBidAdapter_spec.js | 168 +++++++++++++++++- 1 file changed, 165 insertions(+), 3 deletions(-) diff --git a/test/spec/modules/contxtfulBidAdapter_spec.js b/test/spec/modules/contxtfulBidAdapter_spec.js index 5822c0c20a3..56920d083db 100644 --- a/test/spec/modules/contxtfulBidAdapter_spec.js +++ b/test/spec/modules/contxtfulBidAdapter_spec.js @@ -1,6 +1,7 @@ import { spec } from 'modules/contxtfulBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; +import * as ajax from 'src/ajax.js'; const VERSION = 'v1'; const CUSTOMER = 'CUSTOMER'; const BIDDER_ENDPOINT = 'prebid.receptivity.io'; @@ -8,6 +9,17 @@ const RX_FROM_API = { ReceptivityState: 'Receptive', test_info: 'rx_from_engine' describe('contxtful bid adapter', function () { const adapter = newBidder(spec); + let sandbox, ajaxStub; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + ajaxStub = sandbox.stub(ajax, 'ajax'); + }); + + afterEach(function () { + sandbox.restore(); + ajaxStub.restore(); + }); describe('is a functions', function () { it('exists and is a function', function () { @@ -186,12 +198,12 @@ describe('contxtful bid adapter', function () { }); const bidRequest = spec.buildRequests(bidRequests, bidderRequest); - it('will return a data property containing properties ortb2, bidRequests, bidderRequest and pubConfig', () => { + it('will return a data property containing properties ortb2, bidRequests, bidderRequest and config', () => { expect(bidRequest.data).not.to.be.undefined; expect(bidRequest.data.ortb2).not.to.be.undefined; expect(bidRequest.data.bidRequests).not.to.be.undefined; expect(bidRequest.data.bidderRequest).not.to.be.undefined; - expect(bidRequest.data.pubConfig).not.to.be.undefined; + expect(bidRequest.data.config).not.to.be.undefined; }); it('will take custom parameters in the bid request and within the bidRequests array', () => { @@ -291,6 +303,156 @@ describe('contxtful bid adapter', function () { it('will take the param.bidfloor as floor value if possible', () => { expect(bidRequest.data.bidRequests[1].bidFloor.currency).to.equal('USD'); expect(bidRequest.data.bidRequests[1].bidFloor.floor).to.equal(54); + }); + }); + + describe('valid bid response', () => { + const bidResponse = [ + { + 'requestId': 'arequestId', + 'originalCpm': 1.5, + 'cpm': 1.35, + 'currency': 'CAD', + 'width': 300, + 'height': 600, + 'creativeId': 'creativeid', + 'netRevenue': true, + 'ttl': 300, + 'ad': '', + 'mediaType': 'banner', + 'syncs': [ + { + 'url': 'mysyncurl.com?qparam1=qparamv1&qparam2=qparamv2' + } + ] + } + ]; + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION}, + }); + + const bidRequest = spec.buildRequests(bidRequests, bidderRequest); + + it('will interpret response correcly', () => { + const bids = spec.interpretResponse({ body: bidResponse }, bidRequest); + expect(bids).not.to.be.undefined; + expect(bids).to.have.lengthOf(1); + expect(bids).to.deep.equal(bidResponse); + }); + + it('will return empty response if bid response is empty', () => { + const bids = spec.interpretResponse({ body: [] }, bidRequest); + expect(bids).to.have.lengthOf(0); }) - }) + + it('will trigger user sync if enable pixel mode', () => { + const syncOptions = { + pixelEnabled: true + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [{ body: bidResponse }]); + expect(userSyncs).to.deep.equal([ + { + 'url': 'mysyncurl.com/image?qparam1=qparamv1&qparam2=qparamv2&pbjs=1&coppa=0', + 'type': 'image' + } + ]); + }); + + it('will trigger user sync if enable iframe mode', () => { + const syncOptions = { + iframeEnabled: true + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [{ body: bidResponse }]); + expect(userSyncs).to.deep.equal([ + { + 'url': 'mysyncurl.com/iframe?qparam1=qparamv1&qparam2=qparamv2&pbjs=1&coppa=0', + 'type': 'iframe' + } + ]); + }); + + it('will return empty value if no server response', () => { + const syncOptions = { + iframeEnabled: true + }; + + const userSyncs = spec.getUserSyncs(syncOptions, []); + expect(userSyncs).to.have.lengthOf(0); + const userSyncs2 = spec.getUserSyncs(syncOptions, null); + expect(userSyncs2).to.have.lengthOf(0); + }); + + describe('on timeout callback', () => { + it('will never call server if sampling is 0', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION, 'sampling': {'onTimeout' : 0.0}}, + }); + + expect(spec.onTimeout({'customData': 'customvalue'}, ajaxStub)).to.not.throw; + expect(ajaxStub.calledOnce).to.equal(false); + }); + + it('will always call server if sampling is 1', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION, 'sampling': {'onTimeout' : 1.0}}, + }); + + spec.onTimeout({'customData': 'customvalue'}, ajaxStub); + expect(ajaxStub.calledOnce).to.equal(true); + }); + }); + + describe('on onBidderError callback', () => { + it('will never call server if sampling is 0', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION, 'sampling': {'onBidderError' : 0.0}}, + }); + + expect(spec.onBidderError({'customData': 'customvalue'}, ajaxStub)).to.not.throw; + expect(ajaxStub.calledOnce).to.equal(false); + }); + + it('will always call server if sampling is 1', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION, 'sampling': {'onBidderError' : 1.0}}, + }); + + spec.onBidderError({'customData': 'customvalue'}, ajaxStub); + expect(ajaxStub.calledOnce).to.equal(true); + }); + }); + + describe('on onBidWon callback', () => { + it('will always call server', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION}, + }); + spec.onBidWon({'customData': 'customvalue'}, ajaxStub); + expect(ajaxStub.calledOnce).to.equal(true); + }); + }); + + describe('on onBidBillable callback', () => { + it('will always call server', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION}, + }); + spec.onBidBillable({'customData': 'customvalue'}, ajaxStub); + expect(ajaxStub.calledOnce).to.equal(true); + }); + }); + + describe('on onAdRenderSucceeded callback', () => { + it('will always call server', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION}, + }); + spec.onAdRenderSucceeded({'customData': 'customvalue'}, ajaxStub); + expect(ajaxStub.calledOnce).to.equal(true); + }); + }); + + }); }); From 23de79f023bfa053ebdc96059f9320baef138ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Rufiange?= Date: Thu, 19 Sep 2024 19:35:45 -0400 Subject: [PATCH 6/9] refactor: construct url --- modules/contxtfulBidAdapter.js | 32 +++++++++---------- test/spec/modules/contxtfulBidAdapter_spec.js | 4 +-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/modules/contxtfulBidAdapter.js b/modules/contxtfulBidAdapter.js index c17abf599ff..10ab1f07d2a 100644 --- a/modules/contxtfulBidAdapter.js +++ b/modules/contxtfulBidAdapter.js @@ -109,28 +109,28 @@ const buildRequests = (validBidRequests = [], bidderRequest = {}) => { }; // Prepare a sync object compatible with getUserSyncs. -const constructUrl = (userSyncDefault, sync, syncOptions, gdprConsent, uspConsent, gppConsent) => { - const userSyncDefaultParts = userSyncDefault?.url?.split('?'); - const syncurlPart = (sync?.url ?? '').split('?'); - const type = userSyncDefault.type; - if (!syncurlPart || !userSyncDefaultParts) { - return null; - } - let url = syncurlPart[0] + '/' + type + '?'; - if (syncurlPart.length > 1) { - url += syncurlPart[1] + '&'; +const constructUrl = (userSyncsDefault, userSyncServer) => { + const urlSyncServer = (userSyncServer?.url ?? '').split('?'); + const userSyncUrl = userSyncsDefault?.url || ''; + const baseSyncUrl = urlSyncServer[0] || ''; + + let url = `${baseSyncUrl}${userSyncUrl}`; + + if (urlSyncServer.length > 1) { + const urlParams = urlSyncServer[1]; + url += url.includes('?') ? `&${urlParams}` : `?${urlParams}`; } - url += userSyncDefaultParts[1]; + return { - type, + ...userSyncsDefault, url, - } + }; }; const buildSyncEntry = (sync, syncOptions, gdprConsent, uspConsent, gppConsent) => { - const userSyncsLibList = getUserSyncsLib('')(syncOptions, null, gdprConsent, uspConsent, gppConsent); - const userSyncDefault = userSyncsLibList?.find(item => item.url !== undefined); - return constructUrl(userSyncDefault, sync, syncOptions, gdprConsent, uspConsent, gppConsent); + const userSyncsDefaultLib = getUserSyncsLib('')(syncOptions, null, gdprConsent, uspConsent, gppConsent); + const userSyncsDefault = userSyncsDefaultLib?.find(item => item.url !== undefined); + return constructUrl(userSyncsDefault, sync); }; // Returns the list of user synchronization objects. diff --git a/test/spec/modules/contxtfulBidAdapter_spec.js b/test/spec/modules/contxtfulBidAdapter_spec.js index 56920d083db..87a94a4874b 100644 --- a/test/spec/modules/contxtfulBidAdapter_spec.js +++ b/test/spec/modules/contxtfulBidAdapter_spec.js @@ -353,7 +353,7 @@ describe('contxtful bid adapter', function () { const userSyncs = spec.getUserSyncs(syncOptions, [{ body: bidResponse }]); expect(userSyncs).to.deep.equal([ { - 'url': 'mysyncurl.com/image?qparam1=qparamv1&qparam2=qparamv2&pbjs=1&coppa=0', + 'url': 'mysyncurl.com/image?pbjs=1&coppa=0&qparam1=qparamv1&qparam2=qparamv2', 'type': 'image' } ]); @@ -367,7 +367,7 @@ describe('contxtful bid adapter', function () { const userSyncs = spec.getUserSyncs(syncOptions, [{ body: bidResponse }]); expect(userSyncs).to.deep.equal([ { - 'url': 'mysyncurl.com/iframe?qparam1=qparamv1&qparam2=qparamv2&pbjs=1&coppa=0', + 'url': 'mysyncurl.com/iframe?pbjs=1&coppa=0&qparam1=qparamv1&qparam2=qparamv2', 'type': 'iframe' } ]); From b604eeaf80e06b5168b4658abe81217a0eea1a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Rufiange?= Date: Thu, 19 Sep 2024 19:43:34 -0400 Subject: [PATCH 7/9] fix: test --- test/spec/modules/contxtfulBidAdapter_spec.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/spec/modules/contxtfulBidAdapter_spec.js b/test/spec/modules/contxtfulBidAdapter_spec.js index 87a94a4874b..ed428b2d9be 100644 --- a/test/spec/modules/contxtfulBidAdapter_spec.js +++ b/test/spec/modules/contxtfulBidAdapter_spec.js @@ -387,7 +387,7 @@ describe('contxtful bid adapter', function () { describe('on timeout callback', () => { it('will never call server if sampling is 0', () => { config.setConfig({ - contxtful: {customer: CUSTOMER, version: VERSION, 'sampling': {'onTimeout' : 0.0}}, + contxtful: {customer: CUSTOMER, version: VERSION, 'sampling': {'onTimeout': 0.0}}, }); expect(spec.onTimeout({'customData': 'customvalue'}, ajaxStub)).to.not.throw; @@ -396,7 +396,7 @@ describe('contxtful bid adapter', function () { it('will always call server if sampling is 1', () => { config.setConfig({ - contxtful: {customer: CUSTOMER, version: VERSION, 'sampling': {'onTimeout' : 1.0}}, + contxtful: {customer: CUSTOMER, version: VERSION, 'sampling': {'onTimeout': 1.0}}, }); spec.onTimeout({'customData': 'customvalue'}, ajaxStub); @@ -407,7 +407,7 @@ describe('contxtful bid adapter', function () { describe('on onBidderError callback', () => { it('will never call server if sampling is 0', () => { config.setConfig({ - contxtful: {customer: CUSTOMER, version: VERSION, 'sampling': {'onBidderError' : 0.0}}, + contxtful: {customer: CUSTOMER, version: VERSION, 'sampling': {'onBidderError': 0.0}}, }); expect(spec.onBidderError({'customData': 'customvalue'}, ajaxStub)).to.not.throw; @@ -416,7 +416,7 @@ describe('contxtful bid adapter', function () { it('will always call server if sampling is 1', () => { config.setConfig({ - contxtful: {customer: CUSTOMER, version: VERSION, 'sampling': {'onBidderError' : 1.0}}, + contxtful: {customer: CUSTOMER, version: VERSION, 'sampling': {'onBidderError': 1.0}}, }); spec.onBidderError({'customData': 'customvalue'}, ajaxStub); @@ -453,6 +453,5 @@ describe('contxtful bid adapter', function () { expect(ajaxStub.calledOnce).to.equal(true); }); }); - }); }); From 943b974bd3cd7b5ef35131f19a6f862e72214278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Rufiange?= Date: Fri, 20 Sep 2024 10:14:08 -0400 Subject: [PATCH 8/9] fix: test --- test/spec/modules/contxtfulBidAdapter_spec.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/spec/modules/contxtfulBidAdapter_spec.js b/test/spec/modules/contxtfulBidAdapter_spec.js index ed428b2d9be..fef1c51a32f 100644 --- a/test/spec/modules/contxtfulBidAdapter_spec.js +++ b/test/spec/modules/contxtfulBidAdapter_spec.js @@ -373,6 +373,24 @@ describe('contxtful bid adapter', function () { ]); }); + describe('no sync option', () => { + it('will return image sync if no sync options', () => { + const userSyncs = spec.getUserSyncs({}, [{ body: bidResponse }]); + expect(userSyncs).to.deep.equal([ + { + 'url': 'mysyncurl.com/image?pbjs=1&coppa=0&qparam1=qparamv1&qparam2=qparamv2', + 'type': 'image' + } + ]); + }); + it('will return empty value if no server response', () => { + const userSyncs = spec.getUserSyncs({}, []); + expect(userSyncs).to.have.lengthOf(0); + const userSyncs2 = spec.getUserSyncs({}, null); + expect(userSyncs2).to.have.lengthOf(0); + }); + }); + it('will return empty value if no server response', () => { const syncOptions = { iframeEnabled: true From 23967ed0ab01633a3981ce2da1a02085b00a79f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Rufiange?= Date: Fri, 20 Sep 2024 12:53:37 -0400 Subject: [PATCH 9/9] fix: space --- modules/contxtfulBidAdapter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/contxtfulBidAdapter.js b/modules/contxtfulBidAdapter.js index 10ab1f07d2a..f744e4a4c88 100644 --- a/modules/contxtfulBidAdapter.js +++ b/modules/contxtfulBidAdapter.js @@ -8,7 +8,6 @@ import { interpretResponse, getUserSyncs as getUserSyncsLib, } from '../libraries/teqblazeUtils/bidderUtils.js'; - import {ortbConverter} from '../libraries/ortbConverter/converter.js'; // Constants