From 1400fbed357cf58d8341e7d3728928d18347f40d Mon Sep 17 00:00:00 2001 From: Baptiste Haudegand <89531368+github-baptiste-haudegand@users.noreply.github.com> Date: Wed, 22 May 2024 12:25:48 +0200 Subject: [PATCH 01/46] Add GPP support in Teads adapter (#11535) --- modules/teadsBidAdapter.js | 12 +++++ test/spec/modules/teadsBidAdapter_spec.js | 53 +++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index a3c8d3e24dc..8f775f2580d 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -81,6 +81,18 @@ export const spec = { payload.schain = firstBidRequest.schain; } + let gpp = bidderRequest.gppConsent; + if (bidderRequest && gpp) { + let isValidConsentString = typeof gpp.gppString === 'string'; + let validateApplicableSections = + Array.isArray(gpp.applicableSections) && + gpp.applicableSections.every((section) => typeof (section) === 'number') + payload.gpp = { + consentString: isValidConsentString ? gpp.gppString : '', + applicableSectionIds: validateApplicableSections ? gpp.applicableSections : [], + }; + } + let gdpr = bidderRequest.gdprConsent; if (bidderRequest && gdpr) { let isCmp = typeof gdpr.gdprApplies === 'boolean'; diff --git a/test/spec/modules/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index 81e09b09d08..40011367ac0 100644 --- a/test/spec/modules/teadsBidAdapter_spec.js +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -142,6 +142,59 @@ describe('teadsBidAdapter', () => { expect(payload.us_privacy).to.equal(usPrivacy); }); + it('should send GPP values to endpoint when available and valid', function () { + let consentString = 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'; + let applicableSectionIds = [7, 8]; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gppConsent': { + 'gppString': consentString, + 'applicableSections': applicableSectionIds + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gpp).to.exist; + expect(payload.gpp.consentString).to.equal(consentString); + expect(payload.gpp.applicableSectionIds).to.have.members(applicableSectionIds); + }); + + it('should send default GPP values to endpoint when available but invalid', function () { + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gppConsent': { + 'gppString': undefined, + 'applicableSections': ['a'] + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gpp).to.exist; + expect(payload.gpp.consentString).to.equal(''); + expect(payload.gpp.applicableSectionIds).to.have.members([]); + }); + + it('should not set the GPP object in the request sent to the endpoint when not present', function () { + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000 + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gpp).to.not.exist; + }); + it('should send GDPR to endpoint', function() { let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; let bidderRequest = { From 4d0d0b79fe0ca5af5ef857ea22de08c5c66c8d66 Mon Sep 17 00:00:00 2001 From: fliccione <165936219+fliccione@users.noreply.github.com> Date: Wed, 22 May 2024 14:10:53 +0200 Subject: [PATCH 02/46] Onetag Bid Adapter: add reading of ortb2Imp field (#11539) --- modules/onetagBidAdapter.js | 1 + test/spec/modules/onetagBidAdapter_spec.js | 3 +++ 2 files changed, 4 insertions(+) diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index d8423253aaf..62bee5c2aeb 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -291,6 +291,7 @@ function setGeneralInfo(bidRequest) { this['gpid'] = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); this['pubId'] = params.pubId; this['ext'] = params.ext; + this['ortb2Imp'] = deepAccess(bidRequest, 'ortb2Imp'); if (params.pubClick) { this['click'] = params.pubClick; } diff --git a/test/spec/modules/onetagBidAdapter_spec.js b/test/spec/modules/onetagBidAdapter_spec.js index 93db5ffc57f..3ceaec13cd5 100644 --- a/test/spec/modules/onetagBidAdapter_spec.js +++ b/test/spec/modules/onetagBidAdapter_spec.js @@ -226,6 +226,7 @@ describe('onetag', function () { 'bidId', 'bidderRequestId', 'pubId', + 'ortb2Imp', 'transactionId', 'context', 'playerSize', @@ -240,6 +241,7 @@ describe('onetag', function () { 'bidId', 'bidderRequestId', 'pubId', + 'ortb2Imp', 'transactionId', 'mediaTypeInfo', 'sizes', @@ -270,6 +272,7 @@ describe('onetag', function () { expect(payload.bids).to.exist.and.to.have.length(1); expect(payload.bids[0].auctionId).to.equal(bannerBid.ortb2.source.tid); expect(payload.bids[0].transactionId).to.equal(bannerBid.ortb2Imp.ext.tid); + expect(payload.bids[0].ortb2Imp).to.deep.equal(bannerBid.ortb2Imp); }); it('should send GDPR consent data', function () { let consentString = 'consentString'; From e55a64b33c44d136d54ed71e347ce840ad023c70 Mon Sep 17 00:00:00 2001 From: Antonios Sarhanis Date: Wed, 22 May 2024 22:18:34 +1000 Subject: [PATCH 03/46] Handle kvs and segments better from multiple sources (#11508) --- modules/adnuntiusBidAdapter.js | 86 ++++++++++++------- test/spec/modules/adnuntiusBidAdapter_spec.js | 26 ++++-- 2 files changed, 73 insertions(+), 39 deletions(-) diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js index 060f87c0f9c..189e2287571 100644 --- a/modules/adnuntiusBidAdapter.js +++ b/modules/adnuntiusBidAdapter.js @@ -130,31 +130,6 @@ const storageTool = (function () { return (meta && meta.usi) ? meta.usi : false } - const getSegmentsFromOrtb = function (ortb2) { - const userData = deepAccess(ortb2, 'user.data'); - let segments = []; - if (userData) { - userData.forEach(userdat => { - if (userdat.segment) { - segments.push(...userdat.segment.map((segment) => { - if (isStr(segment)) return segment; - if (isStr(segment.id)) return segment.id; - }).filter((seg) => !!seg)); - } - }); - } - return segments - } - - const getKvsFromOrtb = function (ortb2) { - const siteData = deepAccess(ortb2, 'site.ext.data'); - if (siteData) { - return siteData - } else { - return null - } - } - return { refreshStorage: function (bidderRequest) { const ortb2 = bidderRequest.ortb2 || {}; @@ -171,25 +146,69 @@ const storageTool = (function () { return voidAuId.auId; }); } - metaInternal.segments = getSegmentsFromOrtb(ortb2); - metaInternal.kv = getKvsFromOrtb(ortb2); }, saveToStorage: function (serverData, network) { setMetaInternal(serverData, network); }, getUrlRelatedData: function () { // getting the URL information is theoretically not network-specific - const { segments, kv, usi, voidAuIdsArray } = metaInternal; - return { segments, kv, usi, voidAuIdsArray }; + const { usi, voidAuIdsArray } = metaInternal; + return { usi, voidAuIdsArray }; }, getPayloadRelatedData: function (network) { // getting the payload data should be network-specific - const { segments, kv, usi, userId, voidAuIdsArray, voidAuIds, ...payloadRelatedData } = getMetaDataFromLocalStorage(network).reduce((a, entry) => ({ ...a, [entry.key]: entry.value }), {}); + const { segments, usi, userId, voidAuIdsArray, voidAuIds, ...payloadRelatedData } = getMetaDataFromLocalStorage(network).reduce((a, entry) => ({ ...a, [entry.key]: entry.value }), {}); return payloadRelatedData; } }; })(); +const targetingTool = (function() { + const getSegmentsFromOrtb = function(bidderRequest) { + const userData = deepAccess(bidderRequest.ortb2 || {}, 'user.data'); + let segments = []; + if (userData) { + userData.forEach(userdat => { + if (userdat.segment) { + segments.push(...userdat.segment.map((segment) => { + if (isStr(segment)) return segment; + if (isStr(segment.id)) return segment.id; + }).filter((seg) => !!seg)); + } + }); + } + return segments + }; + + const getKvsFromOrtb = function(bidderRequest) { + return deepAccess(bidderRequest.ortb2 || {}, 'site.ext.data'); + }; + + return { + addSegmentsToUrlData: function (validBids, bidderRequest, existingUrlRelatedData) { + let segments = getSegmentsFromOrtb(bidderRequest || {}); + + for (let i = 0; i < validBids.length; i++) { + const bid = validBids[i]; + const targeting = bid.params.targeting || {}; + if (Array.isArray(targeting.segments)) { + segments = segments.concat(targeting.segments); + delete bid.params.targeting.segments; + } + } + + existingUrlRelatedData.segments = segments; + }, + mergeKvsFromOrtb: function(bidTargeting, bidderRequest) { + const kv = getKvsFromOrtb(bidderRequest || {}); + if (!kv) { + return; + } + bidTargeting.kv = {...kv, ...bidTargeting.kv}; + } + } +})(); + const validateBidType = function (bidTypeOption) { return VALID_BID_TYPES.indexOf(bidTypeOption || '') > -1 ? bidTypeOption : 'bid'; } @@ -227,6 +246,7 @@ export const spec = { storageTool.refreshStorage(bidderRequest); const urlRelatedMetaData = storageTool.getUrlRelatedData(); + targetingTool.addSegmentsToUrlData(validBidRequests, bidderRequest, urlRelatedMetaData); if (urlRelatedMetaData.segments.length > 0) queryParamsAndValues.push('segments=' + urlRelatedMetaData.segments.join(',')); if (urlRelatedMetaData.usi) queryParamsAndValues.push('userId=' + urlRelatedMetaData.usi); @@ -261,9 +281,9 @@ export const spec = { networks[network].metaData = payloadRelatedData; } - const targeting = bid.params.targeting || {}; - if (urlRelatedMetaData.kv) targeting.kv = urlRelatedMetaData.kv; - const adUnit = { ...targeting, auId: bid.params.auId, targetId: bid.params.targetId || bid.bidId }; + const bidTargeting = {...bid.params.targeting || {}}; + targetingTool.mergeKvsFromOrtb(bidTargeting, bidderRequest); + const adUnit = { ...bidTargeting, auId: bid.params.auId, targetId: bid.params.targetId || bid.bidId }; const maxDeals = Math.max(0, Math.min(bid.params.maxDeals || 0, MAXIMUM_DEALS_LIMIT)); if (maxDeals > 0) { adUnit.maxDeals = maxDeals; diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js index c288bfb4f12..4044e62280a 100644 --- a/test/spec/modules/adnuntiusBidAdapter_spec.js +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -566,7 +566,7 @@ describe('adnuntiusBidAdapter', function () { expect(request[0].url).to.equal(ENDPOINT_URL_VIDEO); }); - it('should pass segments if available in config', function () { + it('should pass segments if available in config and merge from targeting', function () { const ortb2 = { user: { data: [{ @@ -580,10 +580,16 @@ describe('adnuntiusBidAdapter', function () { } }; + bidderRequests[0].params.targeting = { + segments: ['merge-this', 'and-this'] + }; + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 })); expect(request.length).to.equal(1); expect(request[0]).to.have.property('url') - expect(request[0].url).to.equal(ENDPOINT_URL_SEGMENTS); + expect(request[0].url).to.equal(ENDPOINT_URL_SEGMENTS.replace('segment3', 'segment3,merge-this,and-this')); + + delete bidderRequests[0].params.targeting; }); it('should pass site data ext as key values to ad server', function () { @@ -592,20 +598,28 @@ describe('adnuntiusBidAdapter', function () { ext: { data: { '12345': 'true', - '45678': 'true' + '45678': 'true', + '9090': 'should-be-overwritten' } } } }; - + bidderRequests[0].params.targeting = { + kv: { + 'merge': ['this'], + '9090': ['take it over'] + } + }; const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 })); expect(request.length).to.equal(1); expect(request[0]).to.have.property('url') const data = JSON.parse(request[0].data); - expect(data.adUnits[0].kv).to.have.property('12345'); expect(data.adUnits[0].kv['12345']).to.equal('true'); - expect(data.adUnits[0].kv).to.have.property('45678'); expect(data.adUnits[0].kv['45678']).to.equal('true'); + expect(data.adUnits[0].kv['9090'][0]).to.equal('take it over'); + expect(data.adUnits[0].kv['merge'][0]).to.equal('this'); + + delete bidderRequests[0].params.targeting; }); it('should skip passing site data ext if missing', function () { From 178ef64b11bd73638c8098c9270bb5b84b52cd11 Mon Sep 17 00:00:00 2001 From: kapil-tuptewar <91458408+kapil-tuptewar@users.noreply.github.com> Date: Thu, 23 May 2024 01:09:23 +0530 Subject: [PATCH 04/46] PubMatic Bid Adapter : start sending displaymanager & displaymanagerver (#11530) * Added support for displaymanager & version to pubmatic adapter * Added comments --- modules/pubmaticBidAdapter.js | 4 +++- test/spec/modules/pubmaticBidAdapter_spec.js | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 7ff86c5c46f..a0e117a2e1b 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -673,7 +673,9 @@ function _createImpressionObject(bid, bidderRequest) { ext: { pmZoneId: _parseSlotParam('pmzoneid', bid.params.pmzoneid) }, - bidfloorcur: bid.params.currency ? _parseSlotParam('currency', bid.params.currency) : DEFAULT_CURRENCY + bidfloorcur: bid.params.currency ? _parseSlotParam('currency', bid.params.currency) : DEFAULT_CURRENCY, + displaymanager: 'Prebid.js', + displaymanagerver: '$prebid.version$' // prebid version }; _addPMPDealsInImpression(impObj, bid); diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index ebda4b1767d..e47d301fae4 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -1187,6 +1187,8 @@ describe('PubMatic adapter', function () { expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); expect(data.source.ext.schain).to.deep.equal(bidRequests[0].schain); expect(data.ext.epoch).to.exist; + expect(data.imp[0].displaymanager).to.equal('Prebid.js'); + expect(data.imp[0].displaymanagerver).to.equal('$prebid.version$'); }); it('Set tmax from global config if not set by requestBids method', function() { From 565825f9d558fbdf370162e545de358e8b70c80f Mon Sep 17 00:00:00 2001 From: bjorn-lw <32431346+bjorn-lw@users.noreply.github.com> Date: Thu, 23 May 2024 00:16:22 +0200 Subject: [PATCH 05/46] Forward extended parameters (#11527) --- modules/livewrappedAnalyticsAdapter.js | 10 +--- .../livewrappedAnalyticsAdapter_spec.js | 51 +++++++++++++------ 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/modules/livewrappedAnalyticsAdapter.js b/modules/livewrappedAnalyticsAdapter.js index 2797664e954..413147b4fa4 100644 --- a/modules/livewrappedAnalyticsAdapter.js +++ b/modules/livewrappedAnalyticsAdapter.js @@ -1,4 +1,4 @@ -import { timestamp, logInfo, getWindowTop } from '../src/utils.js'; +import { timestamp, logInfo } from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import { EVENTS, STATUS } from '../src/constants.js'; @@ -171,7 +171,7 @@ livewrappedAnalyticsAdapter.sendEvents = function() { timeouts: getTimeouts(sentRequests.gdpr, sentRequests.auctionIds), bidAdUnits: getbidAdUnits(), rf: getAdRenderFailed(sentRequests.auctionIds), - rcv: getAdblockerRecovered() + ext: initOptions.ext }; if (events.requests.length == 0 && @@ -185,12 +185,6 @@ livewrappedAnalyticsAdapter.sendEvents = function() { ajax(initOptions.endpoint || URL, undefined, JSON.stringify(events), {method: 'POST'}); }; -function getAdblockerRecovered() { - try { - return getWindowTop().I12C && getWindowTop().I12C.Morph === 1; - } catch (e) {} -} - function getSentRequests() { var sentRequests = []; var gdpr = []; diff --git a/test/spec/modules/livewrappedAnalyticsAdapter_spec.js b/test/spec/modules/livewrappedAnalyticsAdapter_spec.js index f82cc4c4f52..1e5f089ca53 100644 --- a/test/spec/modules/livewrappedAnalyticsAdapter_spec.js +++ b/test/spec/modules/livewrappedAnalyticsAdapter_spec.js @@ -341,7 +341,6 @@ describe('Livewrapped analytics adapter', function () { }); it('should build a batched message from prebid events', function () { - sandbox.stub(utils, 'getWindowTop').returns({}); performStandardAuction(); clock.tick(BID_WON_TIMEOUT + 1000); @@ -403,20 +402,6 @@ describe('Livewrapped analytics adapter', function () { expect(message.timeouts[0].adUnit).to.equal('panorama_d_1'); }); - it('should detect adblocker recovered request', function () { - sandbox.stub(utils, 'getWindowTop').returns({ I12C: { Morph: 1 } }); - performStandardAuction(); - - clock.tick(BID_WON_TIMEOUT + 1000); - - expect(server.requests.length).to.equal(1); - let request = server.requests[0]; - - let message = JSON.parse(request.requestBody); - - expect(message.rcv).to.equal(true); - }); - it('should forward GDPR data', function () { events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); events.emit(BID_REQUESTED, { @@ -623,4 +608,40 @@ describe('Livewrapped analytics adapter', function () { expect(request.url).to.equal('https://whitelabeled.com/analytics/10'); }); }); + + describe('when given extended options', function () { + adapterManager.registerAnalyticsAdapter({ + code: 'livewrapped', + adapter: livewrappedAnalyticsAdapter + }); + + beforeEach(function () { + adapterManager.enableAnalytics({ + provider: 'livewrapped', + options: { + publisherId: 'CC411485-42BC-4F92-8389-42C503EE38D7', + ext: { + testparam: 123 + } + } + }); + }); + + afterEach(function () { + livewrappedAnalyticsAdapter.disableAnalytics(); + }); + + it('should forward the extended options', function () { + performStandardAuction(); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + expect(message.ext).to.not.equal(null); + expect(message.ext.testparam).to.equal(123); + }); + }); }); From e8c68911a5894490b70b0833ed9350a1bc581a54 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Thu, 23 May 2024 16:55:56 +0200 Subject: [PATCH 06/46] 51d: remove specific API request limits from doc (#11546) API limits should not be hardcoded in the module doc, as they might change - the interested use should check the pricing plan web page that contains actual information. --- modules/51DegreesRtdProvider.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/51DegreesRtdProvider.md b/modules/51DegreesRtdProvider.md index 991e756a0d7..424a559797d 100644 --- a/modules/51DegreesRtdProvider.md +++ b/modules/51DegreesRtdProvider.md @@ -49,7 +49,7 @@ In order to use the module please first obtain a Resource Key using the [Configu * ScreenInchesWidth * PixelRatio (optional) -PixelRatio is desirable, but it's a paid property requiring a paid license. Also free API service is limited to 500,000 requests per month - consider picking a [51Degrees pricing plan](https://51degrees.com/pricing) that fits your needs. +PixelRatio is desirable, but it's a paid property requiring a paid license. Free API service is limited. Please check [51Degrees pricing](https://51degrees.com/pricing) to choose a plan that suits your needs. #### User Agent Client Hint (UA-CH) Permissions From e07451212d8b708fe662031f8a4a1e159b469b49 Mon Sep 17 00:00:00 2001 From: Viktor Dreiling <34981284+3link@users.noreply.github.com> Date: Thu, 23 May 2024 17:21:43 +0200 Subject: [PATCH 07/46] LiveIntent Identity Module: Introduce First Party ID (#11437) * fpid * lint * Adjust tests * Fix test expectation * live-connect v6.7.3 --- modules/liveIntentIdSystem.js | 103 ++++++---- modules/userId/eids.md | 17 +- package-lock.json | 30 +-- package.json | 2 +- test/spec/modules/eids_spec.js | 16 +- test/spec/modules/liveIntentIdSystem_spec.js | 192 ++++++++++++------- 6 files changed, 230 insertions(+), 130 deletions(-) diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index 786feeb8052..6925f5fd4a0 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -7,12 +7,12 @@ import { triggerPixel, logError } from '../src/utils.js'; import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import { LiveConnect } from 'live-connect-js'; // eslint-disable-line prebid/validate-imports -import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterManager.js'; -import {getStorageManager} from '../src/storageManager.js'; -import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import { LiveConnect } from 'live-connect-js/prebid'; // eslint-disable-line prebid/validate-imports +import { gdprDataHandler, uspDataHandler, gppDataHandler, coppaDataHandler } from '../src/adapterManager.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; +import { UID2_EIDS } from '../libraries/uid2Eids/uid2Eids.js'; import {UID1_EIDS} from '../libraries/uid1Eids/uid1Eids.js'; -import {UID2_EIDS} from '../libraries/uid2Eids/uid2Eids.js'; import { getRefererInfo } from '../src/refererDetection.js'; /** @@ -21,12 +21,12 @@ import { getRefererInfo } from '../src/refererDetection.js'; * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse */ -const DEFAULT_AJAX_TIMEOUT = 5000 -const EVENTS_TOPIC = 'pre_lips' +const DEFAULT_AJAX_TIMEOUT = 5000; +const EVENTS_TOPIC = 'pre_lips'; const MODULE_NAME = 'liveIntentId'; const LI_PROVIDER_DOMAIN = 'liveintent.com'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); -const defaultRequestedAttributes = {'nonId': true} +const defaultRequestedAttributes = {'nonId': true}; const calls = { ajaxGet: (url, onSuccess, onError, timeout) => { ajaxBuilder(timeout)( @@ -53,10 +53,10 @@ let liveConnect = null; */ export function reset() { if (window && window.liQ_instances) { - window.liQ_instances.forEach(i => i.eventBus.off(EVENTS_TOPIC, setEventFiredFlag)) + window.liQ_instances.forEach(i => i.eventBus.off(EVENTS_TOPIC, setEventFiredFlag)); window.liQ_instances = []; } - liveIntentIdSubmodule.setModuleMode(null) + liveIntentIdSubmodule.setModuleMode(null); eventFired = false; liveConnect = null; } @@ -70,7 +70,7 @@ export function setEventFiredFlag() { function parseLiveIntentCollectorConfig(collectConfig) { const config = {}; - collectConfig = collectConfig || {} + collectConfig = collectConfig || {}; collectConfig.appId && (config.appId = collectConfig.appId); collectConfig.fpiStorageStrategy && (config.storageStrategy = collectConfig.fpiStorageStrategy); collectConfig.fpiExpirationDays && (config.expirationDays = collectConfig.fpiExpirationDays); @@ -86,30 +86,39 @@ function parseLiveIntentCollectorConfig(collectConfig) { * @returns {Array} */ function parseRequestedAttributes(overrides) { + function renameAttribute(attribute) { + if (attribute === 'fpid') { + return 'idCookie'; + } else { + return attribute; + }; + } function createParameterArray(config) { - return Object.entries(config).flatMap(([k, v]) => (typeof v === 'boolean' && v) ? [k] : []); + return Object.entries(config).flatMap(([k, v]) => (typeof v === 'boolean' && v) ? [renameAttribute(k)] : []); } if (typeof overrides === 'object') { - return createParameterArray({...defaultRequestedAttributes, ...overrides}) + return createParameterArray({...defaultRequestedAttributes, ...overrides}); } else { return createParameterArray(defaultRequestedAttributes); } } function initializeLiveConnect(configParams) { - configParams = configParams || {}; if (liveConnect) { return liveConnect; } + configParams = configParams || {}; + const fpidConfig = configParams.fpid || {}; + const publisherId = configParams.publisherId || 'any'; const identityResolutionConfig = { publisherId: publisherId, requestedAttributes: parseRequestedAttributes(configParams.requestedAttributesOverrides) }; if (configParams.url) { - identityResolutionConfig.url = configParams.url - } + identityResolutionConfig.url = configParams.url; + }; identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; @@ -119,7 +128,7 @@ function initializeLiveConnect(configParams) { liveConnectConfig.distributorId = configParams.distributorId; identityResolutionConfig.source = configParams.distributorId; } else { - identityResolutionConfig.source = configParams.partner || 'prebid' + identityResolutionConfig.source = configParams.partner || 'prebid'; } liveConnectConfig.wrapperName = 'prebid'; @@ -127,11 +136,16 @@ function initializeLiveConnect(configParams) { liveConnectConfig.identityResolutionConfig = identityResolutionConfig; liveConnectConfig.identifiersToResolve = configParams.identifiersToResolve || []; liveConnectConfig.fireEventDelay = configParams.fireEventDelay; + + liveConnectConfig.idCookie = {}; + liveConnectConfig.idCookie.name = fpidConfig.name; + liveConnectConfig.idCookie.strategy = fpidConfig.strategy == 'html5' ? 'localStorage' : fpidConfig.strategy; + const usPrivacyString = uspDataHandler.getConsentData(); if (usPrivacyString) { liveConnectConfig.usPrivacyString = usPrivacyString; } - const gdprConsent = gdprDataHandler.getConsentData() + const gdprConsent = gdprDataHandler.getConsentData(); if (gdprConsent) { liveConnectConfig.gdprApplies = gdprConsent.gdprApplies; liveConnectConfig.gdprConsent = gdprConsent.consentString; @@ -145,21 +159,21 @@ function initializeLiveConnect(configParams) { // The third param is the ajax and pixel object, the ajax and pixel use PBJS liveConnect = liveIntentIdSubmodule.getInitializer()(liveConnectConfig, storage, calls); if (configParams.emailHash) { - liveConnect.push({ hash: configParams.emailHash }) + liveConnect.push({ hash: configParams.emailHash }); } return liveConnect; } function tryFireEvent() { if (!eventFired && liveConnect) { - const eventDelay = liveConnect.config.fireEventDelay || 500 + const eventDelay = liveConnect.config.fireEventDelay || 500; setTimeout(() => { - const instances = window.liQ_instances - instances.forEach(i => i.eventBus.once(EVENTS_TOPIC, setEventFiredFlag)) + const instances = window.liQ_instances; + instances.forEach(i => i.eventBus.once(EVENTS_TOPIC, setEventFiredFlag)); if (!eventFired && liveConnect) { liveConnect.fire(); } - }, eventDelay) + }, eventDelay); } } @@ -173,10 +187,10 @@ export const liveIntentIdSubmodule = { name: MODULE_NAME, setModuleMode(mode) { - this.moduleMode = mode + this.moduleMode = mode; }, getInitializer() { - return (liveConnectConfig, storage, calls) => LiveConnect(liveConnectConfig, storage, calls, this.moduleMode) + return (liveConnectConfig, storage, calls) => LiveConnect(liveConnectConfig, storage, calls, this.moduleMode); }, /** @@ -194,46 +208,54 @@ export const liveIntentIdSubmodule = { const result = {}; // old versions stored lipbid in unifiedId. Ensure that we can still read the data. - const lipbid = value.nonId || value.unifiedId + const lipbid = value.nonId || value.unifiedId; if (lipbid) { - value.lipbid = lipbid - delete value.unifiedId - result.lipb = value + const lipb = { ...value, lipbid }; + delete lipb.unifiedId; + result.lipb = lipb; } // Lift usage of uid2 by exposing uid2 if we were asked to resolve it. // As adapters are applied in lexicographical order, we will always // be overwritten by the 'proper' uid2 module if it is present. if (value.uid2) { - result.uid2 = { 'id': value.uid2, ext: { provider: LI_PROVIDER_DOMAIN } } + result.uid2 = { 'id': value.uid2, ext: { provider: LI_PROVIDER_DOMAIN } }; } if (value.bidswitch) { - result.bidswitch = { 'id': value.bidswitch, ext: { provider: LI_PROVIDER_DOMAIN } } + result.bidswitch = { 'id': value.bidswitch, ext: { provider: LI_PROVIDER_DOMAIN } }; } if (value.medianet) { - result.medianet = { 'id': value.medianet, ext: { provider: LI_PROVIDER_DOMAIN } } + result.medianet = { 'id': value.medianet, ext: { provider: LI_PROVIDER_DOMAIN } }; } if (value.magnite) { - result.magnite = { 'id': value.magnite, ext: { provider: LI_PROVIDER_DOMAIN } } + result.magnite = { 'id': value.magnite, ext: { provider: LI_PROVIDER_DOMAIN } }; } if (value.index) { - result.index = { 'id': value.index, ext: { provider: LI_PROVIDER_DOMAIN } } + result.index = { 'id': value.index, ext: { provider: LI_PROVIDER_DOMAIN } }; } if (value.openx) { - result.openx = { 'id': value.openx, ext: { provider: LI_PROVIDER_DOMAIN } } + result.openx = { 'id': value.openx, ext: { provider: LI_PROVIDER_DOMAIN } }; } if (value.pubmatic) { - result.pubmatic = { 'id': value.pubmatic, ext: { provider: LI_PROVIDER_DOMAIN } } + result.pubmatic = { 'id': value.pubmatic, ext: { provider: LI_PROVIDER_DOMAIN } }; } if (value.sovrn) { - result.sovrn = { 'id': value.sovrn, ext: { provider: LI_PROVIDER_DOMAIN } } + result.sovrn = { 'id': value.sovrn, ext: { provider: LI_PROVIDER_DOMAIN } }; + } + + if (value.idCookie) { + if (!coppaDataHandler.getCoppa()) { + result.lipb = { ...result.lipb, fpid: value.idCookie }; + result.fpid = { 'id': value.idCookie }; + } + delete result.lipb.idCookie; } if (value.thetradedesk) { @@ -380,6 +402,13 @@ export const liveIntentIdSubmodule = { return data.ext; } } + }, + 'fpid': { + source: 'fpid.liveintent.com', + atype: 1, + getValue: function(data) { + return data.id; + } } } }; diff --git a/modules/userId/eids.md b/modules/userId/eids.md index c10ecde9c30..aa1601e95e3 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -107,7 +107,7 @@ userIdAsEids = [ segments: ['s1', 's2'] } }, - + { source: 'bidswitch.net', uids: [{ @@ -118,7 +118,7 @@ userIdAsEids = [ } }] }, - + { source: 'liveintent.indexexchange.com', uids: [{ @@ -161,7 +161,7 @@ userIdAsEids = [ provider: 'liveintent.com' } }] - }, + }, { source: 'media.net', @@ -185,6 +185,17 @@ userIdAsEids = [ }] }, + { + source: 'fpid.liveintent.com', + uids: [{ + id: 'some-random-id-value', + atype: 1, + ext: { + provider: 'liveintent.com' + } + }] + }, + { source: 'merkleinc.com', uids: [{ diff --git a/package-lock.json b/package-lock.json index 198eb105ed5..ab475b57fcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "fun-hooks": "^0.9.9", "gulp-wrap": "^0.15.0", "klona": "^2.0.6", - "live-connect-js": "^6.3.4" + "live-connect-js": "^6.7.3" }, "devDependencies": { "@babel/eslint-parser": "^7.16.5", @@ -19842,19 +19842,19 @@ "dev": true }, "node_modules/live-connect-common": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.0.3.tgz", - "integrity": "sha512-ZPycT04ROBUvPiksnLTunrKC3ROhBSeO99fQ+4qMIkgKwP2CvS44L7fK+0WFV4nAi+65KbzSng7JWcSlckfw8w==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.1.4.tgz", + "integrity": "sha512-NK5HH0b/6bQX6hZQttlDfqrpDiP+iYtYYGO47LfM9YVwT1OZITgYZUJ0oG4IVynwdpas/VGvXv5hN0UcVK97oQ==", "engines": { "node": ">=18" } }, "node_modules/live-connect-js": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-6.3.4.tgz", - "integrity": "sha512-lg2XeCaj/eEbK66QGGDEdz9IdT/K3ExZ83Qo6xGVLdP5XJ33xAUCk/gds34rRTmpIwUfAnboOpyj3UoYtS3QUQ==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-6.7.3.tgz", + "integrity": "sha512-K2/GGhyhJ7/bFJfjiNw41W5xLRER9Smc49a8A6PImCcgit/sp2UsYz/F+sQwoj8IkJ3PufHvBnIGBbeQ31VsBg==", "dependencies": { - "live-connect-common": "^v3.0.3", + "live-connect-common": "^v3.1.4", "tiny-hashes": "1.0.1" }, "engines": { @@ -44521,16 +44521,16 @@ "dev": true }, "live-connect-common": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.0.3.tgz", - "integrity": "sha512-ZPycT04ROBUvPiksnLTunrKC3ROhBSeO99fQ+4qMIkgKwP2CvS44L7fK+0WFV4nAi+65KbzSng7JWcSlckfw8w==" + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.1.4.tgz", + "integrity": "sha512-NK5HH0b/6bQX6hZQttlDfqrpDiP+iYtYYGO47LfM9YVwT1OZITgYZUJ0oG4IVynwdpas/VGvXv5hN0UcVK97oQ==" }, "live-connect-js": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-6.3.4.tgz", - "integrity": "sha512-lg2XeCaj/eEbK66QGGDEdz9IdT/K3ExZ83Qo6xGVLdP5XJ33xAUCk/gds34rRTmpIwUfAnboOpyj3UoYtS3QUQ==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-6.7.3.tgz", + "integrity": "sha512-K2/GGhyhJ7/bFJfjiNw41W5xLRER9Smc49a8A6PImCcgit/sp2UsYz/F+sQwoj8IkJ3PufHvBnIGBbeQ31VsBg==", "requires": { - "live-connect-common": "^v3.0.3", + "live-connect-common": "^v3.1.4", "tiny-hashes": "1.0.1" } }, diff --git a/package.json b/package.json index 087cab55d7c..3ae934df3b5 100644 --- a/package.json +++ b/package.json @@ -135,7 +135,7 @@ "fun-hooks": "^0.9.9", "gulp-wrap": "^0.15.0", "klona": "^2.0.6", - "live-connect-js": "^6.3.4" + "live-connect-js": "^6.7.3" }, "optionalDependencies": { "fsevents": "^2.3.2" diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index e1f2394ab27..260864dd1a2 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -1,7 +1,7 @@ import {createEidsArray} from 'modules/userId/eids.js'; import {expect} from 'chai'; -// Note: In unit tets cases for bidders, call the createEidsArray function over userId object that is used for calling fetchBids +// Note: In unit test cases for bidders, call the createEidsArray function over userId object that is used for calling fetchBids // this way the request will stay consistent and unit test cases will not need lots of changes. describe('eids array generation for known sub-modules', function() { @@ -184,6 +184,20 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('fpid; getValue call', function() { + const userId = { + fpid: { + id: 'some-random-id-value' + } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'fpid.liveintent.com', + uids: [{id: 'some-random-id-value', atype: 1}] + }); + }); + it('bidswitch', function() { const userId = { bidswitch: {'id': 'sample_id'} diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index 373142db82e..e5caacd1547 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -1,13 +1,13 @@ import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from 'modules/liveIntentIdSystem.js'; import * as utils from 'src/utils.js'; -import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../../../src/adapterManager.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler, coppaDataHandler } from '../../../src/adapterManager.js'; import { server } from 'test/mocks/xhr.js'; import * as refererDetection from '../../../src/refererDetection.js'; resetLiveIntentIdSubmodule(); liveIntentIdSubmodule.setModuleMode('standard') const PUBLISHER_ID = '89899'; -const defaultConfigParams = { params: {publisherId: PUBLISHER_ID, fireEventDelay: 1} }; +const defaultConfigParams = {publisherId: PUBLISHER_ID, fireEventDelay: 1}; const responseHeader = {'Content-Type': 'application/json'} function requests(...urlRegExps) { @@ -30,6 +30,7 @@ describe('LiveIntentId', function() { let getCookieStub; let getDataFromLocalStorageStub; let imgStub; + let coppaConsentDataStub; let refererInfoStub; beforeEach(function() { @@ -41,6 +42,7 @@ describe('LiveIntentId', function() { uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); gdprConsentDataStub = sinon.stub(gdprDataHandler, 'getConsentData'); gppConsentDataStub = sinon.stub(gppDataHandler, 'getConsentData'); + coppaConsentDataStub = sinon.stub(coppaDataHandler, 'getCoppa'); refererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); }); @@ -52,11 +54,12 @@ describe('LiveIntentId', function() { uspConsentDataStub.restore(); gdprConsentDataStub.restore(); gppConsentDataStub.restore(); + coppaConsentDataStub.restore(); refererInfoStub.restore(); resetLiveIntentIdSubmodule(); }); - it('should initialize LiveConnect with a privacy string when getId, and include it in the resolution request', function () { + it('should initialize LiveConnect with a privacy string when getId but not send request', function (done) { uspConsentDataStub.returns('1YNY'); gdprConsentDataStub.returns({ gdprApplies: true, @@ -67,61 +70,58 @@ describe('LiveIntentId', function() { applicableSections: [1, 2] }) let callBackSpy = sinon.spy(); - let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; + let submoduleCallback = liveIntentIdSubmodule.getId({ params: defaultConfigParams }).callback; submoduleCallback(callBackSpy); - let request = idxRequests()[0]; - expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=1&n3pc=1&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString&gpp_as=1%2C2.*/); - const response = { - unifiedId: 'a_unified_id', - segments: [123, 234] - } - request.respond( - 200, - responseHeader, - JSON.stringify(response) - ); - expect(callBackSpy.calledOnceWith(response)).to.be.true; + setTimeout(() => { + let requests = idxRequests().concat(rpRequests()); + expect(requests).to.be.empty; + expect(callBackSpy.notCalled).to.be.true; + done(); + }, 300) }); - it('should fire an event when getId', function(done) { + it('should fire an event without privacy setting when getId', function(done) { uspConsentDataStub.returns('1YNY'); gdprConsentDataStub.returns({ - gdprApplies: true, + gdprApplies: false, consentString: 'consentDataString' }) gppConsentDataStub.returns({ gppString: 'gppConsentDataString', applicableSections: [1] }) - liveIntentIdSubmodule.getId(defaultConfigParams); + liveIntentIdSubmodule.getId({ params: defaultConfigParams }); setTimeout(() => { - expect(rpRequests()[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=1&n3pc=1&n3pct=1&nb=1&gdpr_consent=consentDataString&gpp_s=gppConsentDataString&gpp_as=1.*/); + let request = rpRequests()[0]; + expect(request.url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=0.*&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString.*&gpp_as=1.*/); done(); }, 300); }); it('should fire an event when getId and a hash is provided', function(done) { liveIntentIdSubmodule.getId({ params: { - ...defaultConfigParams.params, + ...defaultConfigParams, emailHash: '58131bc547fb87af94cebdaf3102321f' }}); setTimeout(() => { - expect(rpRequests()[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/) + let request = rpRequests()[0]; + expect(request.url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/) done(); }, 300); }); it('should initialize LiveConnect and forward the prebid version when decode and emit an event', function(done) { - liveIntentIdSubmodule.decode({}, defaultConfigParams); + liveIntentIdSubmodule.decode({}, { params: defaultConfigParams }); setTimeout(() => { - expect(rpRequests()[0].url).to.contain('tv=$prebid.version$') + let request = rpRequests()[0]; + expect(request.url).to.contain('tv=$prebid.version$') done(); }, 300); }); it('should initialize LiveConnect with the config params when decode and emit an event', function (done) { liveIntentIdSubmodule.decode({}, { params: { - ...defaultConfigParams.params, + ...defaultConfigParams, ...{ url: 'https://dummy.liveintent.com', liCollectConfig: { @@ -131,7 +131,8 @@ describe('LiveIntentId', function() { } }}); setTimeout(() => { - expect(requests(/https:\/\/collector.liveintent.com.*/)[0].url).to.match(/https:\/\/collector.liveintent.com\/j\?.*aid=a-0001.*&wpn=prebid.*/); + let request = requests(/https:\/\/collector.liveintent.com\/j\?.*aid=a-0001.*&wpn=prebid.*/); + expect(request.length).to.be.greaterThan(0); done(); }, 300); }); @@ -139,7 +140,8 @@ describe('LiveIntentId', function() { it('should fire an event with the provided distributorId', function (done) { liveIntentIdSubmodule.decode({}, { params: { fireEventDelay: 1, distributorId: 'did-1111' } }); setTimeout(() => { - expect(rpRequests()[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*did=did-1111.*&wpn=prebid.*/); + let request = rpRequests()[0]; + expect(request.url).to.match(/https:\/\/rp.liadm.com\/j\?.*did=did-1111.*&wpn=prebid.*/); done(); }, 300); }); @@ -147,7 +149,7 @@ describe('LiveIntentId', function() { it('should fire an event without the provided distributorId when appId is provided', function (done) { liveIntentIdSubmodule.decode({}, { params: { fireEventDelay: 1, distributorId: 'did-1111', liCollectConfig: { appId: 'a-0001' } } }); setTimeout(() => { - const request = rpRequests()[0]; + let request = rpRequests()[0]; expect(request.url).to.match(/https:\/\/rp.liadm.com\/j\?.*aid=a-0001.*&wpn=prebid.*/); expect(request.url).to.not.match(/.*did=*/); done(); @@ -164,42 +166,44 @@ describe('LiveIntentId', function() { gppString: 'gppConsentDataString', applicableSections: [1] }) - liveIntentIdSubmodule.decode({}, defaultConfigParams); + liveIntentIdSubmodule.decode({}, { params: defaultConfigParams }); setTimeout(() => { - expect(rpRequests()[0].url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString&gpp_as=1.*/); + let request = rpRequests()[0]; + expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString&gpp_as=1.*/); done(); }, 300); }); it('should fire an event when decode and a hash is provided', function(done) { liveIntentIdSubmodule.decode({}, { params: { - ...defaultConfigParams.params, + ...defaultConfigParams, emailHash: '58131bc547fb87af94cebdaf3102321f' }}); setTimeout(() => { - expect(rpRequests()[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/); + let request = rpRequests()[0]; + expect(request.url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/); done(); }, 300); }); it('should not return a decoded identifier when the unifiedId is not present in the value', function() { - const result = liveIntentIdSubmodule.decode({ fireEventDelay: 1, additionalData: 'data' }); + const result = liveIntentIdSubmodule.decode({ params: { fireEventDelay: 1, additionalData: 'data' } }); expect(result).to.be.eql({}); }); it('should fire an event when decode', function(done) { - liveIntentIdSubmodule.decode({}, defaultConfigParams); + liveIntentIdSubmodule.decode({}, { params: defaultConfigParams }); setTimeout(() => { - expect(rpRequests()[0].url).to.be.not.null + expect(rpRequests().length).to.be.eq(1); done(); }, 300); }); it('should initialize LiveConnect and send data only once', function(done) { - liveIntentIdSubmodule.getId(defaultConfigParams); - liveIntentIdSubmodule.decode({}, defaultConfigParams); - liveIntentIdSubmodule.getId(defaultConfigParams); - liveIntentIdSubmodule.decode({}, defaultConfigParams); + liveIntentIdSubmodule.getId({ params: defaultConfigParams }); + liveIntentIdSubmodule.decode({}, { params: defaultConfigParams }); + liveIntentIdSubmodule.getId({ params: defaultConfigParams }); + liveIntentIdSubmodule.decode({}, { params: defaultConfigParams }); setTimeout(() => { expect(rpRequests().length).to.be.eq(1); done(); @@ -209,10 +213,10 @@ describe('LiveIntentId', function() { it('should call the custom URL of the LiveIntent Identity Exchange endpoint', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); - let submoduleCallback = liveIntentIdSubmodule.getId({ params: {...defaultConfigParams.params, ...{'url': 'https://dummy.liveintent.com/idex'}} }).callback; + let submoduleCallback = liveIntentIdSubmodule.getId({ params: {...defaultConfigParams, ...{'url': 'https://dummy.liveintent.com/idex'}} }).callback; submoduleCallback(callBackSpy); let request = requests(/https:\/\/dummy.liveintent.com\/idex\/.*/)[0]; - expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/prebid/89899?cd=.localhost&resolve=nonId'); + expect(request.url).to.match(/https:\/\/dummy.liveintent.com\/idex\/prebid\/89899\?.*cd=.localhost.*&resolve=nonId.*/); request.respond( 204, responseHeader @@ -226,7 +230,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111' } }).callback; submoduleCallback(callBackSpy); let request = idxRequests()[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/did-1111/any?did=did-1111&cd=.localhost&resolve=nonId'); + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/did-1111\/any\?.*did=did-1111.*&cd=.localhost.*&resolve=nonId.*/); request.respond( 204, responseHeader @@ -240,7 +244,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111', liCollectConfig: { appId: 'a-0001' } } }).callback; submoduleCallback(callBackSpy); let request = idxRequests()[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/any?cd=.localhost&resolve=nonId'); + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/prebid\/any\?.*cd=.localhost.*&resolve=nonId.*/); request.respond( 204, responseHeader @@ -252,7 +256,7 @@ describe('LiveIntentId', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { - ...defaultConfigParams.params, + ...defaultConfigParams, ...{ 'url': 'https://dummy.liveintent.com/idex', 'partner': 'rubicon' @@ -260,7 +264,7 @@ describe('LiveIntentId', function() { } }).callback; submoduleCallback(callBackSpy); let request = requests(/https:\/\/dummy.liveintent.com\/idex\/.*/)[0]; - expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/rubicon/89899?cd=.localhost&resolve=nonId'); + expect(request.url).to.match(/https:\/\/dummy.liveintent.com\/idex\/rubicon\/89899\?.*cd=.localhost.*&resolve=nonId.*/); request.respond( 200, responseHeader, @@ -272,10 +276,10 @@ describe('LiveIntentId', function() { it('should call the LiveIntent Identity Exchange endpoint, with no additional query params', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); - let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; + let submoduleCallback = liveIntentIdSubmodule.getId({ params: defaultConfigParams }).callback; submoduleCallback(callBackSpy); let request = idxRequests()[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=nonId'); + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*cd=.localhost.*&resolve=nonId.*/); request.respond( 200, responseHeader, @@ -287,10 +291,10 @@ describe('LiveIntentId', function() { it('should log an error and continue to callback if ajax request errors', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); - let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; + let submoduleCallback = liveIntentIdSubmodule.getId({ params: defaultConfigParams }).callback; submoduleCallback(callBackSpy); let request = idxRequests()[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=nonId'); + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*cd=.localhost.*&resolve=nonId.*/); request.respond( 503, responseHeader, @@ -304,10 +308,11 @@ describe('LiveIntentId', function() { const oldCookie = 'a-xxxx--123e4567-e89b-12d3-a456-426655440000' getCookieStub.withArgs('_lc2_fpi').returns(oldCookie) let callBackSpy = sinon.spy(); - let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; + let submoduleCallback = liveIntentIdSubmodule.getId({ params: defaultConfigParams }).callback; submoduleCallback(callBackSpy); let request = idxRequests()[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&cd=.localhost&resolve=nonId`); + const expected = new RegExp('https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*duid=' + oldCookie + '.*&cd=.localhost.*&resolve=nonId.*'); + expect(request.url).to.match(expected); request.respond( 200, responseHeader, @@ -321,7 +326,7 @@ describe('LiveIntentId', function() { getCookieStub.withArgs('_lc2_fpi').returns(oldCookie); getDataFromLocalStorageStub.withArgs('_thirdPC').returns('third-pc'); const configParams = { params: { - ...defaultConfigParams.params, + ...defaultConfigParams, ...{ 'identifiersToResolve': ['_thirdPC'] } @@ -330,7 +335,8 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); let request = idxRequests()[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&cd=.localhost&_thirdPC=third-pc&resolve=nonId`); + const expected = new RegExp('https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*duid=' + oldCookie + '.*&cd=.localhost.*&_thirdPC=third-pc.*&resolve=nonId.*'); + expect(request.url).to.match(expected); request.respond( 200, responseHeader, @@ -343,7 +349,7 @@ describe('LiveIntentId', function() { getCookieStub.returns(null); getDataFromLocalStorageStub.withArgs('_thirdPC').returns({'key': 'value'}); const configParams = { params: { - ...defaultConfigParams.params, + ...defaultConfigParams, ...{ 'identifiersToResolve': ['_thirdPC'] } @@ -352,7 +358,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); let request = idxRequests()[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?cd=.localhost&_thirdPC=%7B%22key%22%3A%22value%22%7D&resolve=nonId'); + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*cd=.localhost.*&_thirdPC=%7B%22key%22%3A%22value%22%7D.*&resolve=nonId.*/); request.respond( 200, responseHeader, @@ -363,29 +369,29 @@ describe('LiveIntentId', function() { it('should send an error when the cookie jar throws an unexpected error', function() { getCookieStub.throws('CookieError', 'A message'); - liveIntentIdSubmodule.getId(defaultConfigParams); + liveIntentIdSubmodule.getId({ params: defaultConfigParams }); expect(imgStub.getCall(0).args[0]).to.match(/.*ae=.+/); }); it('should decode a unifiedId to lipbId and remove it', function() { - const result = liveIntentIdSubmodule.decode({ unifiedId: 'data' }, defaultConfigParams); + const result = liveIntentIdSubmodule.decode({ unifiedId: 'data' }, { params: defaultConfigParams }); expect(result).to.eql({'lipb': {'lipbid': 'data'}}); }); it('should decode a nonId to lipbId', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'data' }, defaultConfigParams); + const result = liveIntentIdSubmodule.decode({ nonId: 'data' }, { params: defaultConfigParams }); expect(result).to.eql({'lipb': {'lipbid': 'data', 'nonId': 'data'}}); }); it('should resolve extra attributes', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { - ...defaultConfigParams.params, + ...defaultConfigParams, ...{ requestedAttributesOverrides: { 'foo': true, 'bar': false } } } }).callback; submoduleCallback(callBackSpy); let request = idxRequests()[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=nonId&resolve=foo`); + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*cd=.localhost.*&resolve=nonId.*&resolve=foo.*/); request.respond( 200, responseHeader, @@ -395,66 +401,66 @@ describe('LiveIntentId', function() { }); it('should decode a uid2 to a separate object when present', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', uid2: 'bar' }, defaultConfigParams); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', uid2: 'bar' }, { params: defaultConfigParams }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode values with uid2 but no nonId', function() { - const result = liveIntentIdSubmodule.decode({ uid2: 'bar' }, defaultConfigParams); + const result = liveIntentIdSubmodule.decode({ uid2: 'bar' }, { params: defaultConfigParams }); expect(result).to.eql({'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode a bidswitch id to a separate object when present', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', bidswitch: 'bar' }, defaultConfigParams); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', bidswitch: 'bar' }, { params: defaultConfigParams }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'bidswitch': 'bar'}, 'bidswitch': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode a medianet id to a separate object when present', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', medianet: 'bar' }, defaultConfigParams); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', medianet: 'bar' }, { params: defaultConfigParams }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'medianet': 'bar'}, 'medianet': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode a sovrn id to a separate object when present', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', sovrn: 'bar' }, defaultConfigParams); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', sovrn: 'bar' }, { params: defaultConfigParams }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'sovrn': 'bar'}, 'sovrn': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode a magnite id to a separate object when present', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', magnite: 'bar' }, defaultConfigParams); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', magnite: 'bar' }, { params: defaultConfigParams }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'magnite': 'bar'}, 'magnite': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode an index id to a separate object when present', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', index: 'bar' }, defaultConfigParams); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', index: 'bar' }, { params: defaultConfigParams }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'index': 'bar'}, 'index': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode an openx id to a separate object when present', function () { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', openx: 'bar' }, defaultConfigParams); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', openx: 'bar' }, { params: defaultConfigParams }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'openx': 'bar'}, 'openx': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode an pubmatic id to a separate object when present', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', pubmatic: 'bar' }, defaultConfigParams); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', pubmatic: 'bar' }, { params: defaultConfigParams }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'pubmatic': 'bar'}, 'pubmatic': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode a thetradedesk id to a separate object when present', function() { const provider = 'liveintent.com' refererInfoStub.returns({domain: provider}) - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', thetradedesk: 'bar' }, defaultConfigParams); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', thetradedesk: 'bar' }, { params: defaultConfigParams }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'tdid': 'bar'}, 'tdid': {'id': 'bar', 'ext': {'rtiPartner': 'TDID', 'provider': provider}}}); }); it('should allow disabling nonId resolution', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { - ...defaultConfigParams.params, + ...defaultConfigParams, ...{ requestedAttributesOverrides: { 'nonId': false, 'uid2': true } } } }).callback; submoduleCallback(callBackSpy); let request = idxRequests()[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=uid2`); + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*cd=.localhost.*&resolve=uid2.*/); request.respond( 200, responseHeader, @@ -462,4 +468,44 @@ describe('LiveIntentId', function() { ); expect(callBackSpy.calledOnce).to.be.true; }); -}); + + it('should decode a idCookie as fpid if it exists and coppa is false', function() { + coppaConsentDataStub.returns(false) + const result = liveIntentIdSubmodule.decode({nonId: 'foo', idCookie: 'bar'}, { params: defaultConfigParams }) + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'fpid': 'bar'}, 'fpid': {'id': 'bar'}}) + }); + + it('should not decode a idCookie as fpid if it exists and coppa is true', function() { + coppaConsentDataStub.returns(true) + const result = liveIntentIdSubmodule.decode({nonId: 'foo', idCookie: 'bar'}, { params: defaultConfigParams }) + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo'}}) + }); + + it('should resolve fpid from cookie', async function() { + const expectedValue = 'someValue' + const cookieName = 'testcookie' + getCookieStub.withArgs(cookieName).returns(expectedValue) + const config = { params: { + ...defaultConfigParams, + fpid: { 'strategy': 'cookie', 'name': cookieName }, + requestedAttributesOverrides: { 'fpid': true } } + } + const submoduleCallback = liveIntentIdSubmodule.getId(config).callback; + const decodedResult = new Promise(resolve => { + submoduleCallback((x) => resolve(liveIntentIdSubmodule.decode(x, config))); + }); + const request = idxRequests()[0]; + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*cd=.localhost.*&ic=someValue.*&resolve=nonId.*/); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + + const result = await decodedResult + expect(result).to.be.eql({ + lipb: { 'fpid': expectedValue }, + fpid: { id: expectedValue } + }); + }); +}) From f4d8ed3970a1a59c54f7155eda7a144a25a94b11 Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Thu, 23 May 2024 18:51:54 +0000 Subject: [PATCH 08/46] Prebid 8.50.0 release --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab475b57fcd..c37ef8b8b72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.50.0-pre", + "version": "8.50.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 3ae934df3b5..00c739fcf22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.50.0-pre", + "version": "8.50.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 1ebf1a668ff3a44a2d4cf06caeea9752944ef512 Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Thu, 23 May 2024 18:51:54 +0000 Subject: [PATCH 09/46] Increment version to 8.51.0-pre --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index c37ef8b8b72..50d72367d6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.50.0", + "version": "8.51.0-pre", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 00c739fcf22..c6d19acbe1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.50.0", + "version": "8.51.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From b1d5237007165c162944a0fe462a4f89347d1d1a Mon Sep 17 00:00:00 2001 From: Cadent Aperture MX <43830380+EMXDigital@users.noreply.github.com> Date: Thu, 23 May 2024 18:31:02 -0400 Subject: [PATCH 10/46] CadentApertureMX Bid Adapter : update gpid support (#11557) Co-authored-by: Michael Denton --- modules/cadentApertureMXBidAdapter.js | 11 +++++----- .../cadentApertureMXBidAdapter_spec.js | 21 +++++++++++++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/modules/cadentApertureMXBidAdapter.js b/modules/cadentApertureMXBidAdapter.js index e73564dacdb..97283952888 100644 --- a/modules/cadentApertureMXBidAdapter.js +++ b/modules/cadentApertureMXBidAdapter.js @@ -282,12 +282,13 @@ export const spec = { }; // adding gpid support - let gpid = deepAccess(bid, 'ortb2Imp.ext.data.adserver.adslot'); - if (!gpid) { - gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); - } + let gpid = + deepAccess(bid, 'ortb2Imp.ext.gpid') || + deepAccess(bid, 'ortb2Imp.ext.data.adserver.adslot') || + deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + if (gpid) { - data.ext = {gpid: gpid.toString()}; + data.ext = { gpid: gpid.toString() }; } let typeSpecifics = isVideo ? { video: cadentAdapter.buildVideo(bid) } : { banner: cadentAdapter.buildBanner(bid) }; let bidfloorObj = bidfloor > 0 ? { bidfloor, bidfloorcur: DEFAULT_CUR } : {}; diff --git a/test/spec/modules/cadentApertureMXBidAdapter_spec.js b/test/spec/modules/cadentApertureMXBidAdapter_spec.js index 3ccb5405552..4f0f2cf8f20 100644 --- a/test/spec/modules/cadentApertureMXBidAdapter_spec.js +++ b/test/spec/modules/cadentApertureMXBidAdapter_spec.js @@ -451,10 +451,27 @@ describe('cadent_aperture_mx Adapter', function () { }); }); - it('should add gpid to request if present', () => { + it('should add gpid to request if present in ext.gpid', () => { + const gpid = '/12345/my-gpt-tag-0'; + let bid = utils.deepClone(bidderRequest.bids[0]); + bid.ortb2Imp = { ext: { gpid, data: { adserver: { adslot: gpid + '1' }, pbadslot: gpid + '2' } } }; + let requestWithGPID = spec.buildRequests([bid], bidderRequest); + requestWithGPID = JSON.parse(requestWithGPID.data); + expect(requestWithGPID.imp[0].ext.gpid).to.exist.and.equal(gpid); + }); + + it('should add gpid to request if present in ext.data.adserver.adslot', () => { + const gpid = '/12345/my-gpt-tag-0'; + let bid = utils.deepClone(bidderRequest.bids[0]); + bid.ortb2Imp = { ext: { data: { adserver: { adslot: gpid }, pbadslot: gpid + '1' } } }; + let requestWithGPID = spec.buildRequests([bid], bidderRequest); + requestWithGPID = JSON.parse(requestWithGPID.data); + expect(requestWithGPID.imp[0].ext.gpid).to.exist.and.equal(gpid); + }); + + it('should add gpid to request if present in ext.data.pbadslot', () => { const gpid = '/12345/my-gpt-tag-0'; let bid = utils.deepClone(bidderRequest.bids[0]); - bid.ortb2Imp = { ext: { data: { adserver: { adslot: gpid } } } }; bid.ortb2Imp = { ext: { data: { pbadslot: gpid } } }; let requestWithGPID = spec.buildRequests([bid], bidderRequest); requestWithGPID = JSON.parse(requestWithGPID.data); From 8c4955bcfad5e6811e311629e38136cf872061b3 Mon Sep 17 00:00:00 2001 From: Andrius Versockas Date: Fri, 24 May 2024 02:11:12 +0300 Subject: [PATCH 11/46] Eskimi Bid Adapter: switching to plcmt (#11543) Co-authored-by: Andrius Versockas --- modules/eskimiBidAdapter.js | 5 ++--- test/spec/modules/eskimiBidAdapter_spec.js | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/eskimiBidAdapter.js b/modules/eskimiBidAdapter.js index ce01abb9e71..02568c01014 100644 --- a/modules/eskimiBidAdapter.js +++ b/modules/eskimiBidAdapter.js @@ -9,7 +9,6 @@ import {getBidIdParameter} from '../src/utils.js'; */ const BIDDER_CODE = 'eskimi'; -// const ENDPOINT = 'https://hb.eskimi.com/bids' const ENDPOINT = 'https://sspback.eskimi.com/bid-request' const DEFAULT_BID_TTL = 30; @@ -21,7 +20,7 @@ const VIDEO_ORTB_PARAMS = [ 'mimes', 'minduration', 'maxduration', - 'placement', + 'plcmt', 'protocols', 'startdelay', 'skip', @@ -142,7 +141,7 @@ function buildVideoImp(bidRequest, imp) { }); if (imp.video && videoParams?.context === 'outstream') { - imp.video.placement = imp.video.placement || 4; + imp.video.plcmt = imp.video.plcmt || 4; } return { ...imp }; diff --git a/test/spec/modules/eskimiBidAdapter_spec.js b/test/spec/modules/eskimiBidAdapter_spec.js index d01240c86ab..c00a22a5aac 100644 --- a/test/spec/modules/eskimiBidAdapter_spec.js +++ b/test/spec/modules/eskimiBidAdapter_spec.js @@ -33,7 +33,7 @@ const VIDEO_BID = { playbackmethod: [2, 4, 6], playerSize: [[1024, 768]], protocols: [3, 4, 7, 8, 10], - placement: 1, + plcmt: 1, minduration: 0, maxduration: 60, startdelay: 0 @@ -222,7 +222,7 @@ describe('Eskimi bid adapter', function () { mimes: ['video/mp4', 'video/x-flv'], playbackmethod: [3, 4], protocols: [5, 6], - placement: 1, + plcmt: 1, minduration: 0, maxduration: 60, w: 1024, From 8efc3be414f1d98bf18c2d06743b60198e2cafc5 Mon Sep 17 00:00:00 2001 From: adtech-colombia Date: Fri, 24 May 2024 05:32:09 +0530 Subject: [PATCH 12/46] Colombia Bid Adapter : initial release (#11478) * new Colombia bid Adapter implementation * new Colombia bid Adapter implementation * new Colombia bid Adapter implementation * new Colombia bid Adapter implementation- 'lint' errored Fixed * new Colombia bid Adapter implementation- Resolved test case issue * new Colombia bid Adapter implementation- Resolved test case issue * new Colombia bid Adapter implementation- Resolved test case issue --------- Co-authored-by: subhashish.singh --- modules/colombiaBidAdapter.js | 107 +++++++++++++ modules/colombiaBidAdapter.md | 31 ++++ package-lock.json | 2 +- test/spec/modules/colombiaBidAdapter_spec.js | 155 +++++++++++++++++++ 4 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 modules/colombiaBidAdapter.js create mode 100644 modules/colombiaBidAdapter.md create mode 100644 test/spec/modules/colombiaBidAdapter_spec.js diff --git a/modules/colombiaBidAdapter.js b/modules/colombiaBidAdapter.js new file mode 100644 index 00000000000..0d25ca6cb60 --- /dev/null +++ b/modules/colombiaBidAdapter.js @@ -0,0 +1,107 @@ +import * as utils from '../src/utils.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +const BIDDER_CODE = 'colombia'; +const ENDPOINT_URL = 'https://ade.clmbtech.com/cde/prebid.htm'; +const HOST_NAME = document.location.protocol + '//' + window.location.host; + +export const spec = { + code: BIDDER_CODE, + aliases: ['clmb'], + supportedMediaTypes: [BANNER], + isBidRequestValid: function(bid) { + return !!(bid.params.placementId); + }, + buildRequests: function(validBidRequests, bidderRequest) { + if (validBidRequests.length === 0) { + return []; + } + let payloadArr = [] + let ctr = 1; + validBidRequests = validBidRequests.map(bidRequest => { + const params = bidRequest.params; + const sizes = utils.parseSizesInput(bidRequest.sizes)[0]; + const width = sizes.split('x')[0]; + const height = sizes.split('x')[1]; + const placementId = params.placementId; + const cb = Math.floor(Math.random() * 99999999999); + const bidId = bidRequest.bidId; + const referrer = (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer) ? bidderRequest.refererInfo.referer : ''; + let mediaTypes = {} + let payload = { + v: 'hb1', + p: placementId, + pos: '~' + ctr, + w: width, + h: height, + cb: cb, + r: referrer, + uid: bidId, + t: 'i', + d: HOST_NAME, + fpc: params.fpc, + _u: window.location.href, + mediaTypes: Object.assign({}, mediaTypes, bidRequest.mediaTypes) + }; + if (params.keywords) payload.keywords = params.keywords; + if (params.category) payload.cat = params.category; + if (params.pageType) payload.pgt = params.pageType; + if (params.incognito) payload.ic = params.incognito; + if (params.dsmi) payload.smi = params.dsmi; + if (params.optout) payload.out = params.optout; + if (bidRequest && bidRequest.hasOwnProperty('ortb2Imp') && bidRequest.ortb2Imp.hasOwnProperty('ext')) { + payload.ext = bidRequest.ortb2Imp.ext; + if (bidRequest.ortb2Imp.ext.hasOwnProperty('gpid')) payload.pubAdCode = bidRequest.ortb2Imp.ext.gpid.split('#')[0]; + } + payloadArr.push(payload); + ctr++; + }); + return [{ + method: 'POST', + url: ENDPOINT_URL, + data: payloadArr, + }] + }, + interpretResponse: function(serverResponse, bidRequest) { + const bidResponses = []; + const res = serverResponse.body || serverResponse; + if (!res || res.length === 0) { + return bidResponses; + } + try { + res.forEach(response => { + const crid = response.creativeId || 0; + const width = response.width || 0; + const height = response.height || 0; + let cpm = response.cpm || 0; + if (cpm <= 0) { + return bidResponses; + } + if (width !== 0 && height !== 0 && cpm !== 0 && crid !== 0) { + const dealId = response.dealid || ''; + const currency = response.currency || 'USD'; + const netRevenue = (response.netRevenue === undefined) ? true : response.netRevenue; + const bidResponse = { + requestId: response.requestId, + cpm: cpm.toFixed(2), + width: response.width, + height: response.height, + creativeId: crid, + dealId: dealId, + currency: currency, + netRevenue: netRevenue, + ttl: config.getConfig('_bidderTimeout') || 300, + referrer: bidRequest.data.r, + ad: response.ad + }; + bidResponses.push(bidResponse); + } + }); + } catch (error) { + utils.logError(error); + } + return bidResponses; + } +} +registerBidder(spec); diff --git a/modules/colombiaBidAdapter.md b/modules/colombiaBidAdapter.md new file mode 100644 index 00000000000..c6ef5e6b749 --- /dev/null +++ b/modules/colombiaBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +``` +Module Name: COLOMBIA Bidder Adapter +Module Type: Bidder Adapter +Maintainer: colombiaonline@timesinteret.in +``` + +# Description + +Connect to COLOMBIA for bids. + +COLOMBIA adapter requires setup and approval from the COLOMBIA team. Please reach out to your account team or colombiaonline@timesinteret.in for more information. + +# Test Parameters +``` + var adUnits = [{ + code: 'test-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250],[728,90],[320,50]] + } + }, + bids: [{ + bidder: 'colombia', + params: { + placementId: '540799' + } + }] + }]; +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 50d72367d6e..7c28a82a9f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "prebid.js", - "version": "8.48.0-pre", + "version": "8.49.0-pre", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.16.7", diff --git a/test/spec/modules/colombiaBidAdapter_spec.js b/test/spec/modules/colombiaBidAdapter_spec.js new file mode 100644 index 00000000000..b7256545c5e --- /dev/null +++ b/test/spec/modules/colombiaBidAdapter_spec.js @@ -0,0 +1,155 @@ +import { expect } from 'chai'; +import { spec } from 'modules/colombiaBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; + +const HOST_NAME = document.location.protocol + '//' + window.location.host; +const ENDPOINT = 'https://ade.clmbtech.com/cde/prebid.htm'; + +describe('colombiaBidAdapter', function() { + const adapter = newBidder(spec); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'colombia', + 'params': { + placementId: '307466' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [ + [300, 250] + ], + 'bidId': '23beaa6af6cdde', + 'bidderRequestId': '19c0c1efdf37e7', + 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when placementId not passed correctly', function () { + bid.params.placementId = ''; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when require params are not passed', function () { + let bid = Object.assign({}, bid); + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let bidRequests = [ + { + 'bidder': 'colombia', + 'params': { + placementId: '307466' + }, + 'adUnitCode': 'adunit-code1', + 'sizes': [ + [300, 250] + ], + 'bidId': '23beaa6af6cdde', + 'bidderRequestId': '19c0c1efdf37e7', + 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', + }, + { + 'bidder': 'colombia', + 'params': { + placementId: '307466' + }, + 'adUnitCode': 'adunit-code2', + 'sizes': [ + [300, 250] + ], + 'bidId': '382091349b149f"', + 'bidderRequestId': '"1f9c98192de2511"', + 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', + } + ]; + let bidderRequest = { + refererInfo: { + numIframes: 0, + reachedTop: true, + referer: 'http://example.com', + stack: ['http://example.com'] + } + }; + + const request = spec.buildRequests(bidRequests); + it('sends bid request to our endpoint via POST', function () { + expect(request[0].method).to.equal('POST'); + }); + + it('attaches source and version to endpoint URL as query params', function () { + expect(request[0].url).to.equal(ENDPOINT); + }); + }); + + describe('interpretResponse', function () { + let bidRequest = [ + { + 'method': 'POST', + 'url': 'https://ade.clmbtech.com/cde/prebid.htm', + 'data': { + 'v': 'hb1', + 'p': '307466', + 'w': '300', + 'h': '250', + 'cb': 12892917383, + 'r': 'http%3A%2F%2Flocalhost%3A9876%2F%3Fid%3D74552836', + 'uid': '23beaa6af6cdde', + 't': 'i', + } + } + ]; + + let serverResponse = [{ + 'ad': '
This is test case for colombia adapter
', + 'cpm': 3.14, + 'creativeId': '6b958110-612c-4b03-b6a9-7436c9f746dc-1sk24', + 'currency': 'USD', + 'requestId': '23beaa6af6cdde', + 'width': 728, + 'height': 90, + 'netRevenue': true, + 'ttl': 600, + 'dealid': '', + 'referrer': 'http%3A%2F%2Flocalhost%3A9876%2F%3Fid%3D74552836' + }]; + + it('should get the correct bid response', function () { + let expectedResponse = [{ + 'requestId': '23beaa6af6cdde', + 'cpm': 3.14, + 'width': 728, + 'height': 90, + 'creativeId': '6b958110-612c-4b03-b6a9-7436c9f746dc-1sk24', + 'dealId': '', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'referrer': 'http%3A%2F%2Flocalhost%3A9876%2F%3Fid%3D74552836', + 'ad': '
This is test case for colombia adapter
' + }]; + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + }); + + it('handles empty bid response', function () { + let response = { + body: { + 'uid': '23beaa6af6cdde', + 'height': 0, + 'creativeId': '', + 'statusMessage': 'Bid returned empty or error response', + 'width': 0, + 'cpm': 0 + } + }; + let result = spec.interpretResponse(response, bidRequest[0]); + expect(result.length).to.equal(0); + }); + }); +}); From dd34052c65c05e405739f6057b8e89897a5f9f81 Mon Sep 17 00:00:00 2001 From: Shubham <127132399+shubhamc-ins@users.noreply.github.com> Date: Fri, 24 May 2024 05:43:13 +0530 Subject: [PATCH 13/46] Insticator Bid Adapter: Add support for BidFloors (#11472) * support bidfloor from params * update test case for bidder bidfloor * prioritize module floor * fix * update bidfloorcur * add USD currency for module floor * add test cases for bidfloors and fix module floor scopes * add logwarn for non usd floors --- modules/insticatorBidAdapter.js | 58 +++++++++++- .../spec/modules/insticatorBidAdapter_spec.js | 94 +++++++++++++++++++ 2 files changed, 151 insertions(+), 1 deletion(-) diff --git a/modules/insticatorBidAdapter.js b/modules/insticatorBidAdapter.js index 617ce49f171..bff74f0755b 100644 --- a/modules/insticatorBidAdapter.js +++ b/modules/insticatorBidAdapter.js @@ -1,7 +1,7 @@ import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {deepAccess, generateUUID, logError, isArray, isInteger, isArrayOfNums, deepSetValue} from '../src/utils.js'; +import {deepAccess, generateUUID, logError, isArray, isInteger, isArrayOfNums, deepSetValue, isFn, logWarn} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; import {find} from '../src/polyfill.js'; @@ -170,6 +170,19 @@ function buildImpression(bidRequest) { }, } + let bidFloor = parseFloat(deepAccess(bidRequest, 'params.floor')); + + if (!isNaN(bidFloor)) { + imp.bidfloor = deepAccess(bidRequest, 'params.floor'); + imp.bidfloorcur = 'USD'; + const bidfloorcur = deepAccess(bidRequest, 'params.bidfloorcur') + if (bidfloorcur && bidfloorcur !== 'USD') { + delete imp.bidfloor; + delete imp.bidfloorcur; + logWarn('insticator: bidfloorcur supported by insticator is USD only. ignoring bidfloor and bidfloorcur params'); + } + } + if (deepAccess(bidRequest, 'mediaTypes.banner')) { imp.banner = buildBanner(bidRequest); } @@ -178,6 +191,49 @@ function buildImpression(bidRequest) { imp.video = buildVideo(bidRequest); } + if (isFn(bidRequest.getFloor)) { + let moduleBidFloor; + + const mediaType = deepAccess(bidRequest, 'mediaTypes.banner') ? 'banner' : deepAccess(bidRequest, 'mediaTypes.video') ? 'video' : undefined; + + let _mediaType = mediaType; + let _size = '*'; + + if (mediaType && ['banner', 'video'].includes(mediaType)) { + if (mediaType === 'banner') { + const { w: width, h: height } = imp[mediaType]; + if (width && height) { + _size = [width, height]; + } else { + const sizes = deepAccess(bidRequest, 'mediaTypes.banner.format'); + if (sizes && sizes.length > 0) { + const {w: width, h: height} = sizes[0]; + _size = [width, height]; + } + } + } else if (mediaType === 'video') { + const { w: width, h: height } = imp[mediaType]; + _mediaType = mediaType; + _size = [width, height]; + } + } + try { + moduleBidFloor = bidRequest.getFloor({ + currency: 'USD', + mediaType: _mediaType, + size: _size + }); + } catch (err) { + // continue with no module floors + logWarn('priceFloors module call getFloor failed, error : ', err); + } + + if (moduleBidFloor) { + imp.bidfloor = moduleBidFloor.floor; + imp.bidfloorcur = moduleBidFloor.currency; + } + } + return imp; } diff --git a/test/spec/modules/insticatorBidAdapter_spec.js b/test/spec/modules/insticatorBidAdapter_spec.js index 5e41cd6d7aa..489e213b0fa 100644 --- a/test/spec/modules/insticatorBidAdapter_spec.js +++ b/test/spec/modules/insticatorBidAdapter_spec.js @@ -569,6 +569,100 @@ describe('InsticatorBidAdapter', function () { expect(data.imp[0].video.h).to.equal(480); }); + it('should have bidder bidfloor from the request', function () { + const tempBiddRequest = { + ...bidRequest, + params: { + ...bidRequest.params, + floor: 0.5, + }, + } + const requests = spec.buildRequests([tempBiddRequest], bidderRequest); + const data = JSON.parse(requests[0].data); + expect(data.imp[0].bidfloor).to.equal(0.5); + expect(data.imp[0].bidfloorcur).to.equal('USD'); + }); + + it('should have bidder bidfloorcur from the request', function () { + const expectedFloor = 1.5; + const currency = 'USD'; + const tempBiddRequest = { + ...bidRequest, + params: { + ...bidRequest.params, + floor: 0.5, + currency: 'USD', + }, + } + tempBiddRequest.getFloor = () => ({ floor: expectedFloor, currency }) + + const requests = spec.buildRequests([tempBiddRequest], bidderRequest); + const data = JSON.parse(requests[0].data); + expect(data.imp[0].bidfloor).to.equal(1.5); + expect(data.imp[0].bidfloorcur).to.equal('USD'); + }); + + it('should have 1 floor for banner 300x250 and 1.5 for 300x600', function () { + const tempBiddRequest = { + ...bidRequest, + params: { + ...bidRequest.params, + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + format: [{ w: 300, h: 250 }] + }, + }, + } + tempBiddRequest.getFloor = (params) => { + return { floor: params.size[1] === 250 ? 1 : 1.5, currency: 'USD' } + } + + const requests = spec.buildRequests([tempBiddRequest], bidderRequest); + const data = JSON.parse(requests[0].data); + expect(data.imp[0].bidfloor).to.equal(1); + + tempBiddRequest.mediaTypes.banner.format = [ { w: 300, h: 600 }, + ]; + const request2 = spec.buildRequests([tempBiddRequest], bidderRequest); + const data2 = JSON.parse(request2[0].data); + expect(data2.imp[0].bidfloor).to.equal(1.5); + }); + + it('should have 4 floor for video 300x250 and 4.5 for 300x600', function () { + const tempBiddRequest = { + ...bidRequest, + params: { + ...bidRequest.params, + }, + mediaTypes: { + video: { + mimes: [ + 'video/mp4', + 'video/mpeg', + ], + w: 300, + h: 250, + placement: 2, + }, + }, + } + tempBiddRequest.getFloor = (params) => { + return { floor: params.size[1] === 250 ? 4 : 4.5, currency: 'USD' } + } + + const requests = spec.buildRequests([tempBiddRequest], bidderRequest); + const data = JSON.parse(requests[0].data); + expect(data.imp[0].bidfloor).to.equal(4); + + tempBiddRequest.mediaTypes.video.w = 300; + tempBiddRequest.mediaTypes.video.h = 600; + const request2 = spec.buildRequests([tempBiddRequest], bidderRequest); + const data2 = JSON.parse(request2[0].data); + expect(data2.imp[0].bidfloor).to.equal(4.5); + }); + it('should have sites first party data if present in bidderRequest ortb2', function () { bidderRequest = { ...bidderRequest, From caa2b8b2302ac6cb82bd4a70617d1549df470b22 Mon Sep 17 00:00:00 2001 From: Carlos Felix Date: Thu, 23 May 2024 19:35:04 -0500 Subject: [PATCH 14/46] User id module: Ability to store user IDs in cookie and localStorage simultaneously (#11482) * Refactoring - break functions that are handling multiple storage types. * user id: introduce the concept of enabled storage types * refactor the way enabled storage types are populated --- modules/userId/index.js | 232 +++++++++++++++++++++---------- test/spec/modules/userId_spec.js | 122 ++++++++++++++-- 2 files changed, 273 insertions(+), 81 deletions(-) diff --git a/modules/userId/index.js b/modules/userId/index.js index 90d377a816e..977af127534 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -239,6 +239,29 @@ function cookieSetter(submodule, storageMgr) { } } +function setValueInCookie(submodule, valueStr, expiresStr) { + const storage = submodule.config.storage; + const setCookie = cookieSetter(submodule); + + setCookie(null, valueStr, expiresStr); + setCookie('_cst', getConsentHash(), expiresStr); + if (typeof storage.refreshInSeconds === 'number') { + setCookie('_last', new Date().toUTCString(), expiresStr); + } +} + +function setValueInLocalStorage(submodule, valueStr, expiresStr) { + const storage = submodule.config.storage; + const mgr = submodule.storageMgr; + + mgr.setDataInLocalStorage(`${storage.name}_exp`, expiresStr); + mgr.setDataInLocalStorage(`${storage.name}_cst`, getConsentHash()); + mgr.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr)); + if (typeof storage.refreshInSeconds === 'number') { + mgr.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString()); + } +} + /** * @param {SubmoduleContainer} submodule * @param {(Object|string)} value @@ -248,54 +271,62 @@ export function setStoredValue(submodule, value) { * @type {SubmoduleStorage} */ const storage = submodule.config.storage; - const mgr = submodule.storageMgr; try { const expiresStr = (new Date(Date.now() + (storage.expires * (60 * 60 * 24 * 1000)))).toUTCString(); const valueStr = isPlainObject(value) ? JSON.stringify(value) : value; - if (storage.type === COOKIE) { - const setCookie = cookieSetter(submodule); - setCookie(null, valueStr, expiresStr); - setCookie('_cst', getConsentHash(), expiresStr); - if (typeof storage.refreshInSeconds === 'number') { - setCookie('_last', new Date().toUTCString(), expiresStr); - } - } else if (storage.type === LOCAL_STORAGE) { - mgr.setDataInLocalStorage(`${storage.name}_exp`, expiresStr); - mgr.setDataInLocalStorage(`${storage.name}_cst`, getConsentHash()); - mgr.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr)); - if (typeof storage.refreshInSeconds === 'number') { - mgr.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString()); + + submodule.enabledStorageTypes.forEach(storageType => { + switch (storageType) { + case COOKIE: + setValueInCookie(submodule, valueStr, expiresStr); + break; + case LOCAL_STORAGE: + setValueInLocalStorage(submodule, valueStr, expiresStr); + break; } - } + }); } catch (error) { logError(error); } } +function deleteValueFromCookie(submodule) { + const setCookie = cookieSetter(submodule, coreStorage); + const expiry = (new Date(Date.now() - 1000 * 60 * 60 * 24)).toUTCString(); + + ['', '_last', '_cst'].forEach(suffix => { + try { + setCookie(suffix, '', expiry); + } catch (e) { + logError(e); + } + }) +} + +function deleteValueFromLocalStorage(submodule) { + ['', '_last', '_exp', '_cst'].forEach(suffix => { + try { + coreStorage.removeDataFromLocalStorage(submodule.config.storage.name + suffix); + } catch (e) { + logError(e); + } + }); +} + export function deleteStoredValue(submodule) { - let deleter, suffixes; - switch (submodule.config?.storage?.type) { - case COOKIE: - const setCookie = cookieSetter(submodule, coreStorage); - const expiry = (new Date(Date.now() - 1000 * 60 * 60 * 24)).toUTCString(); - deleter = (suffix) => setCookie(suffix, '', expiry) - suffixes = ['', '_last', '_cst']; - break; - case LOCAL_STORAGE: - deleter = (suffix) => coreStorage.removeDataFromLocalStorage(submodule.config.storage.name + suffix) - suffixes = ['', '_last', '_exp', '_cst']; - break; - } - if (deleter) { - suffixes.forEach(suffix => { - try { - deleter(suffix) - } catch (e) { - logError(e); - } - }); - } + populateEnabledStorageTypes(submodule); + + submodule.enabledStorageTypes.forEach(storageType => { + switch (storageType) { + case COOKIE: + deleteValueFromCookie(submodule); + break; + case LOCAL_STORAGE: + deleteValueFromLocalStorage(submodule); + break; + } + }); } function setPrebidServerEidPermissions(initializedSubmodules) { @@ -305,30 +336,46 @@ function setPrebidServerEidPermissions(initializedSubmodules) { } } +function getValueFromCookie(submodule, storedKey) { + return submodule.storageMgr.getCookie(storedKey) +} + +function getValueFromLocalStorage(submodule, storedKey) { + const mgr = submodule.storageMgr; + const storage = submodule.config.storage; + const storedValueExp = mgr.getDataFromLocalStorage(`${storage.name}_exp`); + + // empty string means no expiration set + if (storedValueExp === '') { + return mgr.getDataFromLocalStorage(storedKey); + } else if (storedValueExp && ((new Date(storedValueExp)).getTime() - Date.now() > 0)) { + return decodeURIComponent(mgr.getDataFromLocalStorage(storedKey)); + } +} + /** * @param {SubmoduleContainer} submodule * @param {String|undefined} key optional key of the value * @returns {string} */ function getStoredValue(submodule, key = undefined) { - const mgr = submodule.storageMgr; const storage = submodule.config.storage; const storedKey = key ? `${storage.name}_${key}` : storage.name; let storedValue; try { - if (storage.type === COOKIE) { - storedValue = mgr.getCookie(storedKey); - } else if (storage.type === LOCAL_STORAGE) { - const storedValueExp = mgr.getDataFromLocalStorage(`${storage.name}_exp`); - // empty string means no expiration set - if (storedValueExp === '') { - storedValue = mgr.getDataFromLocalStorage(storedKey); - } else if (storedValueExp) { - if ((new Date(storedValueExp)).getTime() - Date.now() > 0) { - storedValue = decodeURIComponent(mgr.getDataFromLocalStorage(storedKey)); - } + submodule.enabledStorageTypes.find(storageType => { + switch (storageType) { + case COOKIE: + storedValue = getValueFromCookie(submodule, storedKey); + break; + case LOCAL_STORAGE: + storedValue = getValueFromLocalStorage(submodule, storedKey); + break; } - } + + return !!storedValue; + }); + // support storing a string or a stringified object if (typeof storedValue === 'string' && storedValue.trim().charAt(0) === '{') { storedValue = JSON.parse(storedValue); @@ -776,8 +823,10 @@ function populateSubmoduleId(submodule, forceRefresh, allSubmodules) { } if (!storedId || refreshNeeded || forceRefresh || consentChanged(submodule)) { + const extendedConfig = Object.assign({ enabledStorageTypes: submodule.enabledStorageTypes }, submodule.config); + // No id previously saved, or a refresh is needed, or consent has changed. Request a new id from the submodule. - response = submodule.submodule.getId(submodule.config, gdprConsent, storedId); + response = submodule.submodule.getId(extendedConfig, gdprConsent, storedId); } else if (typeof submodule.submodule.extendId === 'function') { // If the id exists already, give submodule a chance to decide additional actions that need to be taken response = submodule.submodule.extendId(submodule.config, gdprConsent, storedId); @@ -834,6 +883,8 @@ function initSubmodules(dest, submodules, forceRefresh = false) { return uidMetrics().fork().measureTime('userId.init.modules', function () { if (!submodules.length) return []; // to simplify log messages from here on + submodules.forEach(submod => populateEnabledStorageTypes(submod)); + /** * filter out submodules that: * @@ -884,6 +935,16 @@ function updateInitializedSubmodules(dest, submodule) { } } +function getConfiguredStorageTypes(config) { + return config?.storage?.type?.trim().split(/\s*&\s*/) || []; +} + +function hasValidStorageTypes(config) { + const storageTypes = getConfiguredStorageTypes(config); + + return storageTypes.every(storageType => ALL_STORAGE_TYPES.has(storageType)); +} + /** * list of submodule configurations with valid 'storage' or 'value' obj definitions * storage config: contains values for storing/retrieving User ID data in browser storage @@ -905,7 +966,7 @@ function getValidSubmoduleConfigs(configRegistry) { if (config.storage && !isEmptyStr(config.storage.type) && !isEmptyStr(config.storage.name) && - ALL_STORAGE_TYPES.has(config.storage.type)) { + hasValidStorageTypes(config)) { carry.push(config); } else if (isPlainObject(config.value)) { carry.push(config); @@ -918,28 +979,53 @@ function getValidSubmoduleConfigs(configRegistry) { const ALL_STORAGE_TYPES = new Set([LOCAL_STORAGE, COOKIE]); -function canUseStorage(submodule) { - switch (submodule.config?.storage?.type) { - case LOCAL_STORAGE: - if (submodule.storageMgr.localStorageIsEnabled()) { - if (coreStorage.getDataFromLocalStorage(PBJS_USER_ID_OPTOUT_NAME)) { - logInfo(`${MODULE_NAME} - opt-out localStorage found, storage disabled`); - return false - } - return true; - } - break; - case COOKIE: - if (submodule.storageMgr.cookiesAreEnabled()) { - if (coreStorage.getCookie(PBJS_USER_ID_OPTOUT_NAME)) { - logInfo(`${MODULE_NAME} - opt-out cookie found, storage disabled`); - return false; - } - return true - } - break; +function canUseLocalStorage(submodule) { + if (!submodule.storageMgr.localStorageIsEnabled()) { + return false; + } + + if (coreStorage.getDataFromLocalStorage(PBJS_USER_ID_OPTOUT_NAME)) { + logInfo(`${MODULE_NAME} - opt-out localStorage found, storage disabled`); + return false + } + + return true; +} + +function canUseCookies(submodule) { + if (!submodule.storageMgr.cookiesAreEnabled()) { + return false; + } + + if (coreStorage.getCookie(PBJS_USER_ID_OPTOUT_NAME)) { + logInfo(`${MODULE_NAME} - opt-out cookie found, storage disabled`); + return false; + } + + return true +} + +function populateEnabledStorageTypes(submodule) { + if (submodule.enabledStorageTypes) { + return; } - return false; + + const storageTypes = getConfiguredStorageTypes(submodule.config); + + submodule.enabledStorageTypes = storageTypes.filter(type => { + switch (type) { + case LOCAL_STORAGE: + return canUseLocalStorage(submodule); + case COOKIE: + return canUseCookies(submodule); + } + + return false; + }); +} + +function canUseStorage(submodule) { + return !!submodule.enabledStorageTypes.length; } function updateEIDConfig(submodules) { diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 2ff19424e09..0f7e9cec6ce 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -1837,6 +1837,88 @@ describe('User ID', function () { }, {adUnits}); }); + it('test hook from pubcommonid cookie&html5', function (done) { + const expiration = new Date(Date.now() + 100000).toUTCString(); + coreStorage.setCookie('pubcid', 'testpubcid', expiration); + localStorage.setItem('pubcid', 'testpubcid'); + localStorage.setItem('pubcid_exp', expiration); + + init(config); + setSubmoduleRegistry([sharedIdSystemSubmodule]); + config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie&html5'])); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.pubcid'); + expect(bid.userId.pubcid).to.equal('testpubcid'); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'pubcid.org', + uids: [{id: 'testpubcid', atype: 1}] + }); + }); + }); + + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + localStorage.removeItem('pubcid'); + localStorage.removeItem('pubcid_exp'); + + done(); + }, {adUnits}); + }); + + it('test hook from pubcommonid cookie&html5, no cookie present', function (done) { + localStorage.setItem('pubcid', 'testpubcid'); + localStorage.setItem('pubcid_exp', new Date(Date.now() + 100000).toUTCString()); + + init(config); + setSubmoduleRegistry([sharedIdSystemSubmodule]); + config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie&html5'])); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.pubcid'); + expect(bid.userId.pubcid).to.equal('testpubcid'); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'pubcid.org', + uids: [{id: 'testpubcid', atype: 1}] + }); + }); + }); + + localStorage.removeItem('pubcid'); + localStorage.removeItem('pubcid_exp'); + + done(); + }, {adUnits}); + }); + + it('test hook from pubcommonid cookie&html5, no local storage entry', function (done) { + coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 100000).toUTCString())); + + init(config); + setSubmoduleRegistry([sharedIdSystemSubmodule]); + config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie&html5'])); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.pubcid'); + expect(bid.userId.pubcid).to.equal('testpubcid'); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'pubcid.org', + uids: [{id: 'testpubcid', atype: 1}] + }); + }); + }); + + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + + done(); + }, {adUnits}); + }); + it('test hook from pubcommonid config value object', function (done) { init(config); setSubmoduleRegistry([sharedIdSystemSubmodule]); @@ -3197,7 +3279,8 @@ describe('User ID', function () { }, storageMgr: { setCookie: sinon.stub() - } + }, + enabledStorageTypes: [ 'cookie' ] } setStoredValue(submodule, 'bar'); expect(submodule.storageMgr.setCookie.getCall(0).args[4]).to.equal('foo.com'); @@ -3214,7 +3297,8 @@ describe('User ID', function () { }, storageMgr: { setCookie: sinon.stub() - } + }, + enabledStorageTypes: [ 'cookie' ] } setStoredValue(submodule, 'bar'); expect(submodule.storageMgr.setCookie.getCall(0).args[4]).to.equal(null); @@ -3361,6 +3445,20 @@ describe('User ID', function () { sinon.assert.calledOnce(mockExtendId); }); }); + + it('calls getId with the list of enabled storage types', function() { + setStorage({lastDelta: 1000}); + config.setConfig(userIdConfig); + + let innerAdUnits; + return runBidsHook((config) => { + innerAdUnits = config.adUnits + }, {adUnits}).then(() => { + sinon.assert.calledOnce(mockGetId); + + expect(mockGetId.getCall(0).args[0].enabledStorageTypes).to.deep.equal([ userIdConfig.userSync.userIds[0].storage.type ]); + }); + }); }); describe('requestDataDeletion', () => { @@ -3376,21 +3474,23 @@ describe('User ID', function () { onDataDeletionRequest: sinon.stub() } } - let mod1, mod2, mod3, cfg1, cfg2, cfg3; + let mod1, mod2, mod3, mod4, cfg1, cfg2, cfg3, cfg4; beforeEach(() => { init(config); mod1 = idMod('id1', 'val1'); mod2 = idMod('id2', 'val2'); mod3 = idMod('id3', 'val3'); + mod4 = idMod('id4', 'val4'); cfg1 = getStorageMock('id1', 'id1', 'cookie'); cfg2 = getStorageMock('id2', 'id2', 'html5'); - cfg3 = {name: 'id3', value: {id3: 'val3'}}; - setSubmoduleRegistry([mod1, mod2, mod3]); + cfg3 = getStorageMock('id3', 'id3', 'cookie&html5'); + cfg4 = {name: 'id4', value: {id4: 'val4'}}; + setSubmoduleRegistry([mod1, mod2, mod3, mod4]); config.setConfig({ auctionDelay: 1, userSync: { - userIds: [cfg1, cfg2, cfg3] + userIds: [cfg1, cfg2, cfg3, cfg4] } }); return getGlobal().refreshUserIds(); @@ -3399,16 +3499,21 @@ describe('User ID', function () { it('deletes stored IDs', () => { expect(coreStorage.getCookie('id1')).to.exist; expect(coreStorage.getDataFromLocalStorage('id2')).to.exist; + expect(coreStorage.getCookie('id3')).to.exist; + expect(coreStorage.getDataFromLocalStorage('id3')).to.exist; requestDataDeletion(sinon.stub()); expect(coreStorage.getCookie('id1')).to.not.exist; expect(coreStorage.getDataFromLocalStorage('id2')).to.not.exist; + expect(coreStorage.getCookie('id3')).to.not.exist; + expect(coreStorage.getDataFromLocalStorage('id3')).to.not.exist; }); it('invokes onDataDeletionRequest', () => { requestDataDeletion(sinon.stub()); sinon.assert.calledWith(mod1.onDataDeletionRequest, cfg1, {id1: 'val1'}); - sinon.assert.calledWith(mod2.onDataDeletionRequest, cfg2, {id2: 'val2'}) - sinon.assert.calledWith(mod3.onDataDeletionRequest, cfg3, {id3: 'val3'}) + sinon.assert.calledWith(mod2.onDataDeletionRequest, cfg2, {id2: 'val2'}); + sinon.assert.calledWith(mod3.onDataDeletionRequest, cfg3, {id3: 'val3'}); + sinon.assert.calledWith(mod4.onDataDeletionRequest, cfg4, {id4: 'val4'}); }); describe('does not choke when onDataDeletionRequest', () => { @@ -3423,6 +3528,7 @@ describe('User ID', function () { requestDataDeletion(next, arg); sinon.assert.calledOnce(mod2.onDataDeletionRequest); sinon.assert.calledOnce(mod3.onDataDeletionRequest); + sinon.assert.calledOnce(mod4.onDataDeletionRequest); sinon.assert.calledWith(next, arg); }) }) From abf0fd07ac0b537d7d98e4bfdbd157a121f89fab Mon Sep 17 00:00:00 2001 From: ahmadlob <109217988+ahmadlob@users.noreply.github.com> Date: Fri, 24 May 2024 14:17:02 +0300 Subject: [PATCH 15/46] Taboola Bid Adapter: support DChain (#11545) * cookie-look-up-logic-fix-gpp-fix * support dchain --------- Co-authored-by: aleskanderl <109285067+aleskanderl@users.noreply.github.com> --- modules/taboolaBidAdapter.js | 3 ++ test/spec/modules/taboolaBidAdapter_spec.js | 59 +++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index 6e6d89dc921..74a614bc6b0 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -113,6 +113,9 @@ const converter = ortbConverter({ const bidResponse = buildBidResponse(bid, context); bidResponse.nurl = bid.nurl; bidResponse.ad = replaceAuctionPrice(bid.adm, bid.price); + if (bid.ext && bid.ext.dchain) { + deepSetValue(bidResponse, 'meta.dchain', bid.ext.dchain); + } return bidResponse } }); diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index ca09fbbbcc9..2ea8c325989 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -692,6 +692,50 @@ describe('Taboola Adapter', function () { } }; + const serverResponseWithDchain = { + body: { + 'id': '49ffg4d58ef9a163a69fhgfghd4fad03621b9e036f24f7_15', + 'seatbid': [ + { + 'bid': [ + { + 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', + 'impid': request.data.imp[0].id, + 'price': 0.342068, + 'adid': '2785119545551083381', + 'adm': '\u003chtml\u003e\n\u003chead\u003e\n\u003cmeta charset\u003d"UTF-8"\u003e\n\u003cmeta http-equiv\u003d"Content-Type" content\u003d"text/html; charset\u003dutf-8"/\u003e\u003c/head\u003e\n\u003cbody style\u003d"margin: 0px; overflow:hidden;"\u003e \n\u003cscript type\u003d"text/javascript"\u003e\nwindow.tbl_trc_domain \u003d \u0027us-trc.taboola.com\u0027;\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({article:\u0027auto\u0027});\n!function (e, f, u, i) {\nif (!document.getElementById(i)){\ne.async \u003d 1;\ne.src \u003d u;\ne.id \u003d i;\nf.parentNode.insertBefore(e, f);\n}\n}(document.createElement(\u0027script\u0027),\ndocument.getElementsByTagName(\u0027script\u0027)[0],\n\u0027//cdn.taboola.com/libtrc/wattpad-placement-255/loader.js\u0027,\n\u0027tb_loader_script\u0027);\nif(window.performance \u0026\u0026 typeof window.performance.mark \u003d\u003d \u0027function\u0027)\n{window.performance.mark(\u0027tbl_ic\u0027);}\n\u003c/script\u003e\n\n\u003cdiv id\u003d"taboola-below-article-thumbnails" style\u003d"height: 250px; width: 300px;"\u003e\u003c/div\u003e\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({\nmode: \u0027Rbox_300x250_1x1\u0027,\ncontainer: \u0027taboola-below-article-thumbnails\u0027,\nplacement: \u0027wattpad.com_P18694_S257846_W300_H250_N1_TB\u0027,\ntarget_type: \u0027mix\u0027,\n"rtb-win":{ \nbi:\u002749ff4d58ef9a163a696d4fad03621b9e036f24f7_15\u0027,\ncu:\u0027USD\u0027,\nwp:\u0027${AUCTION_PRICE:BF}\u0027,\nwcb:\u0027~!audex-display-impression!~\u0027,\nrt:\u00271643227025284\u0027,\nrdc:\u0027us.taboolasyndication.com\u0027,\nti:\u00274212\u0027,\nex:\u0027MagniteSCoD\u0027,\nbs:\u0027xapi:257846:lvvSm6Ak7_wE\u0027,\nbp:\u002718694\u0027,\nbd:\u0027wattpad.com\u0027,\nsi:\u00279964\u0027\n} \n,\nrec: {"trc":{"si":"a69c7df43b2334f0aa337c37e2d80c21","sd":"v2_a69c7df43b2334f0aa337c37e2d80c21_3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD_1643227025_1643227025_CJS1tQEQ5NdWGPLA0d76xo-9ngEgASgEMCY4iegHQIroB0iB09kDUKPPB1gAYABop-G2i_Hl-eVucAA","ui":"3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD","plc":"PHON","wi":"-643136642229425433","cc":"CA","route":"US:US:V","el2r":["bulk-metrics","debug","social","metrics","perf"],"uvpw":"1","pi":"1420260","cpb":"GNO629MGIJz__________wEqGXVzLnRhYm9vbGFzeW5kaWNhdGlvbi5jb20yC3RyYy1zY29kMTI5OIDwmrUMQInoB0iK6AdQgdPZA1ijzwdjCN3__________wEQ3f__________ARgjZGMI3AoQoBAYFmRjCNIDEOAGGAhkYwiWFBCcHBgYZGMI9AUQiwoYC2RjCNkUEPkcGB1kYwj0FBCeHRgfZGorNDlmZjRkNThlZjlhMTYzYTY5NmQ0ZmFkMDM2MjFiOWUwMzZmMjRmN18xNXgCgAHpbIgBrPvTxQE","dcga":{"pubConfigOverride":{"border-color":"black","font-weight":"bold","inherit-title-color":"true","module-name":"cta-lazy-module","enable-call-to-action-creative-component":"true","disable-cta-on-custom-module":"true"}},"tslt":{"p-video-overlay":{"cancel":"סגור","goto":"עבור לדף"},"read-more":{"DEFAULT_CAPTION":"%D7%A7%D7%A8%D7%90%20%D7%A2%D7%95%D7%93"},"next-up":{"BTN_TEXT":"לקריאת התוכן הבא"},"time-ago":{"now":"עכשיו","today":"היום","yesterday":"אתמול","minutes":"לפני {0} דקות","hour":"לפני שעה","hours":"לפני {0} שעות","days":"לפני {0} ימים"},"explore-more":{"TITLE_TEXT":"המשיכו לקרוא","POPUP_TEXT":"אל תפספסו הזדמנות לקרוא עוד תוכן מעולה, רגע לפני שתעזבו"}},"evh":"-1964913910","vl":[{"ri":"185db6d274ce94b27caaabd9eed7915b","uip":"wattpad.com_P18694_S257846_W300_H250_N1_TB","ppb":"COIF","estimation_method":"EcpmEstimationMethodType_ESTIMATION","baseline_variant":"false","original_ecpm":"0.4750949889421463","v":[{"thumbnail":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg","all-thumbnails":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg!-#@1600x1000","origin":"default","thumb-size":"1600x1000","title":"Get Roofing Services At Prices You Can Afford In Edmonton","type":"text","published-date":"1641997069","branding-text":"Roofing Services | Search Ads","url":"https://inneth-conded.xyz/9ad2e613-8777-4fe7-9a52-386c88879289?site\u003dwattpad-placement-255\u0026site_id\u003d1420260\u0026title\u003dGet+Roofing+Services+At+Prices+You+Can+Afford+In+Edmonton\u0026platform\u003dSmartphone\u0026campaign_id\u003d15573949\u0026campaign_item_id\u003d3108610633\u0026thumbnail\u003dhttp%3A%2F%2Fcdn.taboola.com%2Flibtrc%2Fstatic%2Fthumbnails%2Fa2b272be514ca3ebe3f97a4a32a41db5.jpg\u0026cpc\u003d{cpc}\u0026click_id\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1\u0026tblci\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1#tblciGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1","duration":"0","sig":"328243c4127ff16e3fdcd7270bab908f6f3fc5b4c98d","item-id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","uploader":"","is-syndicated":"true","publisher":"search","id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","category":"home","views":"0","itp":[{"u":"https://trc.taboola.com/1326786/log/3/unip?en\u003dclickersusa","t":"c"}],"description":""}]}],"cpcud":{"upc":"0.0","upr":"0.0"}}}\n});\n\u003c/script\u003e\n\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({flush: true});\n\u003c/script\u003e\n\n\u003c/body\u003e\n\u003c/html\u003e', + 'adomain': [ + 'example.xyz' + ], + 'cid': '15744349', + 'crid': '278195503434041083381', + 'w': 300, + 'h': 250, + 'exp': 60, + 'lurl': 'http://us-trc.taboola.com/sample', + 'nurl': 'http://win.example.com/', + 'ext': { + 'dchain': { + 'complete': 1, + 'ver': '1.0', + 'nodes': [ + { + 'asi': 'taboola.com', + 'bsid': '1495' + } + ] + } + } + } + ], + 'seat': '14204545260' + } + ], + 'bidid': 'da43860a-4644-442a-b5e0-93f268cf8d19', + 'cur': 'USD' + } + }; + const serverResponseWithPa = { body: { 'id': '49ffg4d58ef9a163a69fhgfghd4fad03621b9e036f24f7_15', @@ -1023,6 +1067,21 @@ describe('Taboola Adapter', function () { expect(res).to.deep.equal(expectedRes) }); + it('should interpret display response with dchain', function () { + const expectedDchainRes = { + 'complete': 1, + 'ver': '1.0', + 'nodes': [ + { + 'asi': 'taboola.com', + 'bsid': '1495' + } + ] + } + const res = spec.interpretResponse(serverResponseWithDchain, request) + expect(res[0].meta.dchain).to.deep.equal(expectedDchainRes) + }); + it('should interpret display response with PA', function () { const [bid] = serverResponse.body.seatbid[0].bid; From 29a5b40507284d234488795d5be61ccd433eef6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bendeg=C3=BAz=20=C3=81cs?= <30595431+acsbendi@users.noreply.github.com> Date: Fri, 24 May 2024 15:56:51 +0200 Subject: [PATCH 16/46] Kobler adapter: Fix to avoid bidding with third-party creatives requiring consent or legitimate interest. (#11559) * Kobler adapter: Fix to avoid bidding with third-party creatives requiring consent or legitimate interest. * Fixed tests. --- modules/koblerBidAdapter.js | 38 ++++++++---- test/spec/modules/koblerBidAdapter_spec.js | 67 ++++++++++++---------- 2 files changed, 63 insertions(+), 42 deletions(-) diff --git a/modules/koblerBidAdapter.js b/modules/koblerBidAdapter.js index 596e5b2695f..3ef40c8a921 100644 --- a/modules/koblerBidAdapter.js +++ b/modules/koblerBidAdapter.js @@ -90,8 +90,6 @@ export const onTimeout = function (timeoutDataArray) { timeoutDataArray.forEach(timeoutData => { const query = parseQueryStringParameters({ ad_unit_code: timeoutData.adUnitCode, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auction_id: timeoutData.auctionId, bid_id: timeoutData.bidId, timeout: timeoutData.timeout, page_url: pageUrl, @@ -103,13 +101,6 @@ export const onTimeout = function (timeoutDataArray) { }; function getPageUrlFromRequest(validBidRequest, bidderRequest) { - // pageUrl is considered only when testing to ensure that non-test requests always contain the correct URL - if (isTest(validBidRequest) && config.getConfig('pageUrl')) { - // TODO: it's not clear what the intent is here - but all adapters should always respect pageUrl. - // With prebid 7, using `refererInfo.page` will do that automatically. - return config.getConfig('pageUrl'); - } - return (bidderRequest.refererInfo && bidderRequest.refererInfo.page) ? bidderRequest.refererInfo.page : window.location.href; @@ -125,7 +116,26 @@ function getPageUrlFromRefererInfo() { function buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest) { const imps = validBidRequests.map(buildOpenRtbImpObject); const timeout = bidderRequest.timeout; - const pageUrl = getPageUrlFromRequest(validBidRequests[0], bidderRequest) + const pageUrl = getPageUrlFromRequest(validBidRequests[0], bidderRequest); + // Kobler, a contextual advertising provider, does not process any personal data itself, so it is not part of TCF/GVL. + // However, it supports using select third-party creatives in its platform, some of which require certain permissions + // in order to be shown. Kobler's bidder checks if necessary permissions are present to avoid bidding + // with ineligible creatives. + let purpose2Given; + let purpose3Given; + if (bidderRequest.gdprConsent && bidderRequest.gdprConsent.vendorData) { + const vendorData = bidderRequest.gdprConsent.vendorData + const purposeData = vendorData.purpose; + const restrictions = vendorData.publisher ? vendorData.publisher.restrictions : null; + const restrictionForPurpose2 = restrictions ? (restrictions[2] ? Object.values(restrictions[2])[0] : null) : null; + purpose2Given = restrictionForPurpose2 === 1 ? ( + purposeData && purposeData.consents && purposeData.consents[2] + ) : ( + restrictionForPurpose2 === 0 + ? false : (purposeData && purposeData.legitimateInterests && purposeData.legitimateInterests[2]) + ); + purpose3Given = purposeData && purposeData.consents && purposeData.consents[3]; + } const request = { id: bidderRequest.bidderRequestId, at: 1, @@ -138,7 +148,13 @@ function buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest) { site: { page: pageUrl, }, - test: getTestAsNumber(validBidRequests[0]) + test: getTestAsNumber(validBidRequests[0]), + ext: { + kobler: { + tcf_purpose_2_given: purpose2Given, + tcf_purpose_3_given: purpose3Given + } + } }; return JSON.stringify(request); diff --git a/test/spec/modules/koblerBidAdapter_spec.js b/test/spec/modules/koblerBidAdapter_spec.js index 2b5830f68d2..74c0a1f5967 100644 --- a/test/spec/modules/koblerBidAdapter_spec.js +++ b/test/spec/modules/koblerBidAdapter_spec.js @@ -5,14 +5,37 @@ import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import {getRefererInfo} from 'src/refererDetection.js'; -function createBidderRequest(auctionId, timeout, pageUrl) { +function createBidderRequest(auctionId, timeout, pageUrl, addGdprConsent) { + const gdprConsent = addGdprConsent ? { + consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', + apiVersion: 2, + vendorData: { + purpose: { + consents: { + 1: false, + 2: true, + 3: false + } + }, + publisher: { + restrictions: { + '2': { + // require consent + '11': 1 + } + } + } + }, + gdprApplies: true + } : {}; return { bidderRequestId: 'mock-uuid', auctionId: auctionId || 'c1243d83-0bed-4fdb-8c76-42b456be17d0', timeout: timeout || 2000, refererInfo: { page: pageUrl || 'example.com' - } + }, + gdprConsent: gdprConsent }; } @@ -289,27 +312,6 @@ describe('KoblerAdapter', function () { expect(openRtbRequest.test).to.be.equal(1); }); - it('should read pageUrl from config when testing', function () { - config.setConfig({ - pageUrl: 'https://testing-url.com' - }); - const validBidRequests = [ - createValidBidRequest( - { - test: true - } - ) - ]; - const bidderRequest = createBidderRequest(); - - const result = spec.buildRequests(validBidRequests, bidderRequest); - expect(result.url).to.be.equal('https://bid-service.dev.essrtb.com/bid/prebid_rtb_call'); - - const openRtbRequest = JSON.parse(result.data); - expect(openRtbRequest.site.page).to.be.equal('https://testing-url.com'); - expect(openRtbRequest.test).to.be.equal(1); - }); - it('should not read pageUrl from config when not testing', function () { config.setConfig({ pageUrl: 'https://testing-url.com' @@ -439,7 +441,8 @@ describe('KoblerAdapter', function () { const bidderRequest = createBidderRequest( '9ff580cf-e10e-4b66-add7-40ac0c804e21', 4500, - 'bid.kobler.no' + 'bid.kobler.no', + true ); const result = spec.buildRequests(validBidRequests, bidderRequest); @@ -529,7 +532,13 @@ describe('KoblerAdapter', function () { site: { page: 'bid.kobler.no' }, - test: 0 + test: 0, + ext: { + kobler: { + tcf_purpose_2_given: true, + tcf_purpose_3_given: false + } + } }; expect(openRtbRequest).to.deep.equal(expectedOpenRtbRequest); @@ -702,14 +711,12 @@ describe('KoblerAdapter', function () { spec.onTimeout([ { adUnitCode: 'adunit-code', - auctionId: 'a1fba829-dd41-409f-acfb-b7b0ac5f30c6', bidId: 'ef236c6c-e934-406b-a877-d7be8e8a839a', timeout: 100, params: [], }, { adUnitCode: 'adunit-code-2', - auctionId: 'a1fba829-dd41-409f-acfb-b7b0ac5f30c6', bidId: 'ca4121c8-9a4a-46ba-a624-e9b64af206f2', timeout: 100, params: [], @@ -719,13 +726,11 @@ describe('KoblerAdapter', function () { expect(utils.triggerPixel.callCount).to.be.equal(2); expect(utils.triggerPixel.getCall(0).args[0]).to.be.equal( 'https://bid.essrtb.com/notify/prebid_timeout?ad_unit_code=adunit-code&' + - 'auction_id=a1fba829-dd41-409f-acfb-b7b0ac5f30c6&bid_id=ef236c6c-e934-406b-a877-d7be8e8a839a&timeout=100&' + - 'page_url=' + encodeURIComponent(getRefererInfo().page) + 'bid_id=ef236c6c-e934-406b-a877-d7be8e8a839a&timeout=100&page_url=' + encodeURIComponent(getRefererInfo().page) ); expect(utils.triggerPixel.getCall(1).args[0]).to.be.equal( 'https://bid.essrtb.com/notify/prebid_timeout?ad_unit_code=adunit-code-2&' + - 'auction_id=a1fba829-dd41-409f-acfb-b7b0ac5f30c6&bid_id=ca4121c8-9a4a-46ba-a624-e9b64af206f2&timeout=100&' + - 'page_url=' + encodeURIComponent(getRefererInfo().page) + 'bid_id=ca4121c8-9a4a-46ba-a624-e9b64af206f2&timeout=100&page_url=' + encodeURIComponent(getRefererInfo().page) ); }); }); From 1fc58448a42c191dc4e2365fb2146e0ffefbe467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zdravko=20Kosanovi=C4=87?= <41286499+zkosanovic@users.noreply.github.com> Date: Sat, 25 May 2024 14:02:08 +0200 Subject: [PATCH 17/46] MinuteMedia: Respond with the correct creativeId (#11565) --- modules/minutemediaBidAdapter.js | 2 +- test/spec/modules/minutemediaBidAdapter_spec.js | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/minutemediaBidAdapter.js b/modules/minutemediaBidAdapter.js index d14af07210e..a724a0446a4 100644 --- a/modules/minutemediaBidAdapter.js +++ b/modules/minutemediaBidAdapter.js @@ -76,7 +76,7 @@ export const spec = { width: adUnit.width, height: adUnit.height, ttl: adUnit.ttl || TTL, - creativeId: adUnit.requestId, + creativeId: adUnit.creativeId, netRevenue: adUnit.netRevenue || true, nurl: adUnit.nurl, mediaType: adUnit.mediaType, diff --git a/test/spec/modules/minutemediaBidAdapter_spec.js b/test/spec/modules/minutemediaBidAdapter_spec.js index cf50ad2cd0a..f2bdd3b6c9d 100644 --- a/test/spec/modules/minutemediaBidAdapter_spec.js +++ b/test/spec/modules/minutemediaBidAdapter_spec.js @@ -440,6 +440,8 @@ describe('minutemediaAdapter', function () { width: 640, height: 480, requestId: '21e12606d47ba7', + creativeId: 'creative-id', + nurl: 'http://example.com/win/1234', adomain: ['abc.com'], mediaType: VIDEO }, @@ -449,6 +451,8 @@ describe('minutemediaAdapter', function () { width: 300, height: 250, requestId: '21e12606d47ba7', + creativeId: 'creative-id', + nurl: 'http://example.com/win/1234', adomain: ['abc.com'], mediaType: BANNER }] @@ -461,7 +465,7 @@ describe('minutemediaAdapter', function () { width: 640, height: 480, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: VIDEO, @@ -476,10 +480,10 @@ describe('minutemediaAdapter', function () { requestId: '21e12606d47ba7', cpm: 12.5, currency: 'USD', - width: 640, - height: 480, + width: 300, + height: 250, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: BANNER, @@ -492,8 +496,8 @@ describe('minutemediaAdapter', function () { it('should get correct bid response', function () { const result = spec.interpretResponse({ body: response }); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedVideoResponse)); - expect(Object.keys(result[1])).to.deep.equal(Object.keys(expectedBannerResponse)); + expect(result[0]).to.deep.equal(expectedVideoResponse); + expect(result[1]).to.deep.equal(expectedBannerResponse); }); it('video type should have vastXml key', function () { From f6a214b259a2f232f589fbdb404b3f0709451f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zdravko=20Kosanovi=C4=87?= <41286499+zkosanovic@users.noreply.github.com> Date: Sat, 25 May 2024 14:34:48 +0200 Subject: [PATCH 18/46] Rise: Respond with the correct creativeId (#11564) --- modules/riseBidAdapter.js | 2 +- test/spec/modules/riseBidAdapter_spec.js | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js index b176ab08aaf..72170706fc0 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -82,7 +82,7 @@ export const spec = { width: adUnit.width, height: adUnit.height, ttl: adUnit.ttl || TTL, - creativeId: adUnit.requestId, + creativeId: adUnit.creativeId, netRevenue: adUnit.netRevenue || true, nurl: adUnit.nurl, mediaType: adUnit.mediaType, diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js index 3cb5cb5c154..cb879231237 100644 --- a/test/spec/modules/riseBidAdapter_spec.js +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -466,6 +466,8 @@ describe('riseAdapter', function () { height: 480, requestId: '21e12606d47ba7', adomain: ['abc.com'], + creativeId: 'creative-id', + nurl: 'http://example.com/win/1234', mediaType: VIDEO }, { @@ -475,6 +477,8 @@ describe('riseAdapter', function () { height: 250, requestId: '21e12606d47ba7', adomain: ['abc.com'], + creativeId: 'creative-id', + nurl: 'http://example.com/win/1234', mediaType: BANNER }] }; @@ -486,7 +490,7 @@ describe('riseAdapter', function () { width: 640, height: 480, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: VIDEO, @@ -501,10 +505,10 @@ describe('riseAdapter', function () { requestId: '21e12606d47ba7', cpm: 12.5, currency: 'USD', - width: 640, - height: 480, + width: 300, + height: 250, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: BANNER, @@ -517,8 +521,8 @@ describe('riseAdapter', function () { it('should get correct bid response', function () { const result = spec.interpretResponse({ body: response }); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedVideoResponse)); - expect(Object.keys(result[1])).to.deep.equal(Object.keys(expectedBannerResponse)); + expect(result[0]).to.deep.equal(expectedVideoResponse); + expect(result[1]).to.deep.equal(expectedBannerResponse); }); it('video type should have vastXml key', function () { From 229c968d3410e793d4ddc88cdad2a3ae017c2b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zdravko=20Kosanovi=C4=87?= <41286499+zkosanovic@users.noreply.github.com> Date: Sun, 26 May 2024 20:13:18 +0200 Subject: [PATCH 19/46] STN: Respond with the correct creativeId (#11566) --- modules/stnBidAdapter.js | 2 +- test/spec/modules/stnBidAdapter_spec.js | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/modules/stnBidAdapter.js b/modules/stnBidAdapter.js index 42b69ee7c2b..af4f4c45d38 100644 --- a/modules/stnBidAdapter.js +++ b/modules/stnBidAdapter.js @@ -75,7 +75,7 @@ export const spec = { width: adUnit.width, height: adUnit.height, ttl: adUnit.ttl || TTL, - creativeId: adUnit.requestId, + creativeId: adUnit.creativeId, netRevenue: adUnit.netRevenue || true, nurl: adUnit.nurl, mediaType: adUnit.mediaType, diff --git a/test/spec/modules/stnBidAdapter_spec.js b/test/spec/modules/stnBidAdapter_spec.js index 95cab32e41d..de851158ed0 100644 --- a/test/spec/modules/stnBidAdapter_spec.js +++ b/test/spec/modules/stnBidAdapter_spec.js @@ -440,8 +440,10 @@ describe('stnAdapter', function () { width: 640, height: 480, requestId: '21e12606d47ba7', + creativeId: 'creative-id-1', adomain: ['abc.com'], - mediaType: VIDEO + mediaType: VIDEO, + nurl: 'http://example.com/win/1234', }, { cpm: 12.5, @@ -449,8 +451,10 @@ describe('stnAdapter', function () { width: 300, height: 250, requestId: '21e12606d47ba7', + creativeId: 'creative-id-2', adomain: ['abc.com'], - mediaType: BANNER + mediaType: BANNER, + nurl: 'http://example.com/win/1234', }] }; @@ -461,7 +465,7 @@ describe('stnAdapter', function () { width: 640, height: 480, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id-1', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: VIDEO, @@ -476,10 +480,10 @@ describe('stnAdapter', function () { requestId: '21e12606d47ba7', cpm: 12.5, currency: 'USD', - width: 640, - height: 480, + width: 300, + height: 250, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id-2', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: BANNER, @@ -492,8 +496,8 @@ describe('stnAdapter', function () { it('should get correct bid response', function () { const result = spec.interpretResponse({ body: response }); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedVideoResponse)); - expect(Object.keys(result[1])).to.deep.equal(Object.keys(expectedBannerResponse)); + expect(result[0]).to.deep.equal(expectedVideoResponse); + expect(result[1]).to.deep.equal(expectedBannerResponse); }); it('video type should have vastXml key', function () { From e0e177f6921c67a01cd836580099c03980beed11 Mon Sep 17 00:00:00 2001 From: Denis Logachov Date: Mon, 27 May 2024 14:23:55 +0300 Subject: [PATCH 20/46] Adkernel Bid Adapter: add hyperbrainz alias (#11544) --- modules/adkernelBidAdapter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 0c353e4332a..151e08bd2bb 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -94,7 +94,8 @@ export const spec = { {code: 'adpluto'}, {code: 'headbidder'}, {code: 'digiad'}, - {code: 'monetix'} + {code: 'monetix'}, + {code: 'hyperbrainz'} ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], From 187e0d8e9b72cf2453b0daad9f50419edbb357da Mon Sep 17 00:00:00 2001 From: mkomorski Date: Mon, 27 May 2024 19:54:56 +0200 Subject: [PATCH 21/46] Prebid Core: Configurable maxbid (#11519) * 11252 Configurable maxbid * lint fixes & add docs * removing unnecessary logic --------- Co-authored-by: mkomorski Co-authored-by: Marcin Komorski --- src/auction.js | 22 ++++++++++++++++++++-- src/config.js | 4 ++++ src/constants.js | 3 ++- test/spec/auctionmanager_spec.js | 13 +++++++++++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/auction.js b/src/auction.js index 26845936797..881dee9f2de 100644 --- a/src/auction.js +++ b/src/auction.js @@ -94,7 +94,7 @@ import {auctionManager} from './auctionManager.js'; import {bidderSettings} from './bidderSettings.js'; import * as events from './events.js'; import adapterManager from './adapterManager.js'; -import { EVENTS, GRANULARITY_OPTIONS, JSON_MAPPING, S2S, TARGETING_KEYS } from './constants.js'; +import { EVENTS, GRANULARITY_OPTIONS, JSON_MAPPING, REJECTION_REASON, S2S, TARGETING_KEYS } from './constants.js'; import {defer, GreedyPromise} from './utils/promise.js'; import {useMetrics} from './utils/perfMetrics.js'; import {adjustCpm} from './utils/cpm.js'; @@ -421,7 +421,11 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a * @param {function(String): void} reject a function that, when called, rejects `bid` with the given reason. */ export const addBidResponse = hook('sync', function(adUnitCode, bid, reject) { - this.dispatch.call(null, adUnitCode, bid); + if (!isValidPrice(bid)) { + reject(REJECTION_REASON.PRICE_TOO_HIGH) + } else { + this.dispatch.call(null, adUnitCode, bid); + } }, 'addBidResponse'); /** @@ -974,3 +978,17 @@ function groupByPlacement(bidsByPlacement, bid) { bidsByPlacement[bid.adUnitCode].bids.push(bid); return bidsByPlacement; } + +/** + * isValidPrice is price validation function + * which checks if price from bid response + * is not higher than top limit set in config + * @type {Function} + * @param bid + * @returns {boolean} + */ +function isValidPrice(bid) { + const maxBidValue = config.getConfig('maxBid'); + if (!maxBidValue || !bid.cpm) return true; + return maxBidValue >= Number(bid.cpm); +} diff --git a/src/config.js b/src/config.js index f4dd0de9612..21c34cf34d2 100644 --- a/src/config.js +++ b/src/config.js @@ -36,6 +36,7 @@ const DEFAULT_DISABLE_AJAX_TIMEOUT = false; const DEFAULT_BID_CACHE = false; const DEFAULT_DEVICE_ACCESS = true; const DEFAULT_MAX_NESTED_IFRAMES = 10; +const DEFAULT_MAXBID_VALUE = 5000 const DEFAULT_TIMEOUTBUFFER = 400; @@ -160,6 +161,9 @@ export function newConfig() { // default max nested iframes for referer detection maxNestedIframes: DEFAULT_MAX_NESTED_IFRAMES, + + // default max bid + maxBid: DEFAULT_MAXBID_VALUE }; Object.defineProperties(newConfig, diff --git a/src/constants.js b/src/constants.js index b40b7ddb9b0..bb76083862b 100644 --- a/src/constants.js +++ b/src/constants.js @@ -135,7 +135,8 @@ export const REJECTION_REASON = { FLOOR_NOT_MET: 'Bid does not meet price floor', CANNOT_CONVERT_CURRENCY: 'Unable to convert currency', DSA_REQUIRED: 'Bid does not provide required DSA transparency info', - DSA_MISMATCH: 'Bid indicates inappropriate DSA rendering method' + DSA_MISMATCH: 'Bid indicates inappropriate DSA rendering method', + PRICE_TOO_HIGH: 'Bid price exceeds maximum value' }; export const PREBID_NATIVE_DATA_KEYS_TO_ORTB = { diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index 65c6256acdc..e5cdb66e75f 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -24,6 +24,9 @@ import {expect} from 'chai'; import {deepClone} from '../../src/utils.js'; import { IMAGE as ortbNativeRequest } from 'src/native.js'; import {PrebidServer} from '../../modules/prebidServerBidAdapter/index.js'; +import '../../modules/currency.js' +import { setConfig as setCurrencyConfig } from '../../modules/currency.js'; +import { REJECTION_REASON } from '../../src/constants.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -1467,6 +1470,16 @@ describe('auctionmanager.js', function () { config.getConfig.restore(); store.store.restore(); }); + + it('should reject bid for price higher than limit for the same currency', () => { + sinon.stub(auction, 'addBidRejected'); + config.setConfig({ + maxBid: 1 + }); + + auction.callBids(); + sinon.assert.calledWith(auction.addBidRejected, sinon.match({rejectionReason: REJECTION_REASON.PRICE_TOO_HIGH})); + }) }); describe('addBidRequests', function () { From af5502030eedf31d49ccc3b61748a807f4286702 Mon Sep 17 00:00:00 2001 From: vishal-dw <109065778+vishal-dw@users.noreply.github.com> Date: Tue, 28 May 2024 04:13:54 +0530 Subject: [PATCH 22/46] remove use of deprecated video.placement (#11573) --- modules/datawrkzBidAdapter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/datawrkzBidAdapter.js b/modules/datawrkzBidAdapter.js index b5a096521a1..2f8e397d959 100644 --- a/modules/datawrkzBidAdapter.js +++ b/modules/datawrkzBidAdapter.js @@ -227,7 +227,6 @@ function buildVideoRequest(bidRequest, bidderRequest) { maxbitrate: deepAccess(bidRequest, 'mediaTypes.video.maxbitrate'), delivery: deepAccess(bidRequest, 'mediaTypes.video.delivery'), linearity: deepAccess(bidRequest, 'mediaTypes.video.linearity'), - placement: deepAccess(bidRequest, 'mediaTypes.video.placement'), skip: deepAccess(bidRequest, 'mediaTypes.video.skip'), skipafter: deepAccess(bidRequest, 'mediaTypes.video.skipafter') }; From e093b2c875a4a7bb366e7bc81874d57253c0fefb Mon Sep 17 00:00:00 2001 From: asurovenko-zeta <80847074+asurovenko-zeta@users.noreply.github.com> Date: Tue, 28 May 2024 12:37:50 +0200 Subject: [PATCH 23/46] ZetaGlobalSpp adapter: remove onTimeout (#11576) --- modules/zeta_global_sspBidAdapter.js | 21 ------------------- .../modules/zeta_global_sspBidAdapter_spec.js | 5 ----- 2 files changed, 26 deletions(-) diff --git a/modules/zeta_global_sspBidAdapter.js b/modules/zeta_global_sspBidAdapter.js index f3e2e12c143..918d03861ae 100644 --- a/modules/zeta_global_sspBidAdapter.js +++ b/modules/zeta_global_sspBidAdapter.js @@ -3,7 +3,6 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {parseDomain} from '../src/refererDetection.js'; -import {ajax} from '../src/ajax.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -15,7 +14,6 @@ import {ajax} from '../src/ajax.js'; const BIDDER_CODE = 'zeta_global_ssp'; const ENDPOINT_URL = 'https://ssp.disqus.com/bid/prebid'; -const TIMEOUT_URL = 'https://ssp.disqus.com/timeout/prebid'; const USER_SYNC_URL_IFRAME = 'https://ssp.disqus.com/sync?type=iframe'; const USER_SYNC_URL_IMAGE = 'https://ssp.disqus.com/sync?type=image'; const DEFAULT_CUR = 'USD'; @@ -268,25 +266,6 @@ export const spec = { url: USER_SYNC_URL_IMAGE + syncurl }]; } - }, - - onTimeout: function(timeoutData) { - if (timeoutData) { - const payload = timeoutData.map(d => ({ - bidder: d?.bidder, - shortname: d?.params?.map(p => p?.tags?.shortname).find(p => p), - sid: d?.params?.map(p => p?.sid).find(p => p), - country: d?.ortb2?.device?.geo?.country, - devicetype: d?.ortb2?.device?.devicetype - })); - ajax(TIMEOUT_URL, null, JSON.stringify(payload), { - method: 'POST', - options: { - withCredentials: false, - contentType: 'application/json' - } - }); - } } } diff --git a/test/spec/modules/zeta_global_sspBidAdapter_spec.js b/test/spec/modules/zeta_global_sspBidAdapter_spec.js index 7b5c0278019..f6079f08460 100644 --- a/test/spec/modules/zeta_global_sspBidAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspBidAdapter_spec.js @@ -542,11 +542,6 @@ describe('Zeta Ssp Bid Adapter', function () { expect(payload.imp[0].bidfloor).to.eql(params.bidfloor); }); - it('Timeout should exists and be a function', function () { - expect(spec.onTimeout).to.exist.and.to.be.a('function'); - expect(spec.onTimeout([{bidder: '1'}])).to.be.undefined; - }); - it('Test schain provided', function () { const request = spec.buildRequests(bannerRequest, bannerRequest[0]); const payload = JSON.parse(request.data); From a5eaf635859ce387c8207f48cf5b7811e29edd28 Mon Sep 17 00:00:00 2001 From: AvivOpenWeb Date: Tue, 28 May 2024 15:21:46 +0300 Subject: [PATCH 24/46] OpenWeb: Respond with the correct creativeId (#11574) Co-authored-by: Zdravko Kosanovic --- modules/openwebBidAdapter.js | 2 +- test/spec/modules/openwebBidAdapter_spec.js | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/openwebBidAdapter.js b/modules/openwebBidAdapter.js index cf0334b7f29..5bc74ac6465 100644 --- a/modules/openwebBidAdapter.js +++ b/modules/openwebBidAdapter.js @@ -76,7 +76,7 @@ export const spec = { width: adUnit.width, height: adUnit.height, ttl: adUnit.ttl || TTL, - creativeId: adUnit.requestId, + creativeId: adUnit.creativeId, netRevenue: adUnit.netRevenue || true, nurl: adUnit.nurl, mediaType: adUnit.mediaType, diff --git a/test/spec/modules/openwebBidAdapter_spec.js b/test/spec/modules/openwebBidAdapter_spec.js index 4586ce78135..34f92a76c42 100644 --- a/test/spec/modules/openwebBidAdapter_spec.js +++ b/test/spec/modules/openwebBidAdapter_spec.js @@ -440,6 +440,8 @@ describe('openwebAdapter', function () { width: 640, height: 480, requestId: '21e12606d47ba7', + creativeId: 'creative-id-1', + nurl: 'http://example.com/win/1234', adomain: ['abc.com'], mediaType: VIDEO }, @@ -449,6 +451,8 @@ describe('openwebAdapter', function () { width: 300, height: 250, requestId: '21e12606d47ba7', + creativeId: 'creative-id-2', + nurl: 'http://example.com/win/1234', adomain: ['abc.com'], mediaType: BANNER }] @@ -461,7 +465,7 @@ describe('openwebAdapter', function () { width: 640, height: 480, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id-1', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: VIDEO, @@ -476,10 +480,10 @@ describe('openwebAdapter', function () { requestId: '21e12606d47ba7', cpm: 12.5, currency: 'USD', - width: 640, - height: 480, + width: 300, + height: 250, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id-2', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: BANNER, @@ -492,8 +496,8 @@ describe('openwebAdapter', function () { it('should get correct bid response', function () { const result = spec.interpretResponse({ body: response }); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedVideoResponse)); - expect(Object.keys(result[1])).to.deep.equal(Object.keys(expectedBannerResponse)); + expect(result[0]).to.deep.equal(expectedVideoResponse); + expect(result[1]).to.deep.equal(expectedBannerResponse); }); it('video type should have vastXml key', function () { From 69578436c07a9c90a054eea44824fdd135e16c98 Mon Sep 17 00:00:00 2001 From: John Salis Date: Tue, 28 May 2024 08:27:23 -0400 Subject: [PATCH 25/46] Beachfront Bid Adapter : add plcmt support (#11558) * change placement to plcmt * add placement param --------- Co-authored-by: John Salis --- modules/beachfrontBidAdapter.js | 4 ++-- test/spec/modules/beachfrontBidAdapter_spec.js | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index 658fc30b43b..8f4a9e3e46d 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -15,7 +15,7 @@ import {Renderer} from '../src/Renderer.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {find, includes} from '../src/polyfill.js'; -const ADAPTER_VERSION = '1.20'; +const ADAPTER_VERSION = '1.21'; const ADAPTER_NAME = 'BFIO_PREBID'; const OUTSTREAM = 'outstream'; const CURRENCY = 'USD'; @@ -26,7 +26,7 @@ export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/l export const SYNC_IFRAME_ENDPOINT = 'https://sync.bfmio.com/sync_iframe'; export const SYNC_IMAGE_ENDPOINT = 'https://sync.bfmio.com/syncb'; -export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'placement', 'skip', 'skipmin', 'skipafter']; +export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'placement', 'plcmt', 'skip', 'skipmin', 'skipafter']; export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; export const SUPPORTED_USER_IDS = [ diff --git a/test/spec/modules/beachfrontBidAdapter_spec.js b/test/spec/modules/beachfrontBidAdapter_spec.js index c0994985aae..2e766487951 100644 --- a/test/spec/modules/beachfrontBidAdapter_spec.js +++ b/test/spec/modules/beachfrontBidAdapter_spec.js @@ -245,14 +245,14 @@ describe('BeachfrontAdapter', function () { const mimes = ['video/webm']; const playbackmethod = 2; const maxduration = 30; - const placement = 4; + const plcmt = 4; const skip = 1; bidRequest.mediaTypes = { - video: { mimes, playbackmethod, maxduration, placement, skip } + video: { mimes, playbackmethod, maxduration, plcmt, skip } }; const requests = spec.buildRequests([ bidRequest ], {}); const data = requests[0].data; - expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, placement, skip }); + expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, plcmt, skip }); }); it('must override video params from the bidder object', function () { @@ -260,13 +260,13 @@ describe('BeachfrontAdapter', function () { const mimes = ['video/webm']; const playbackmethod = 2; const maxduration = 30; - const placement = 4; + const plcmt = 4; const skip = 1; - bidRequest.mediaTypes = { video: { placement: 3, skip: 0 } }; - bidRequest.params.video = { mimes, playbackmethod, maxduration, placement, skip }; + bidRequest.mediaTypes = { video: { plcmt: 3, skip: 0 } }; + bidRequest.params.video = { mimes, playbackmethod, maxduration, plcmt, skip }; const requests = spec.buildRequests([ bidRequest ], {}); const data = requests[0].data; - expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, placement, skip }); + expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, plcmt, skip }); }); it('must add US privacy data to the request', function () { From 646426f940c2b293a3cb90cf793b49cded8bef93 Mon Sep 17 00:00:00 2001 From: Nikhil <137479857+NikhilGopalChennissery@users.noreply.github.com> Date: Tue, 28 May 2024 18:31:18 +0530 Subject: [PATCH 26/46] Preciso Bid Adapter : update on valid request checks (#11161) * test logs added * Added precisoExample.html in integrationExamples * updated request Validation check * bid request data updated * bid request data updated * trailing spaces removed * precisoBidAdapter_spec.js updated * Auction_price macro replacing from response * Auction_price macro replacing from response * Auction_price macro replacing from response * Test logs removed * Bid Request valid check modified * Test logs removed * userid updated in usersync call * add back blank line * add blank line to end * bidFloor correction --------- Co-authored-by: Chris Huie --- integrationExamples/gpt/precisoExample.html | 168 +++++++++++++++++++ modules/precisoBidAdapter.js | 170 ++++++++++---------- test/spec/modules/precisoBidAdapter_spec.js | 73 ++++----- 3 files changed, 288 insertions(+), 123 deletions(-) create mode 100644 integrationExamples/gpt/precisoExample.html diff --git a/integrationExamples/gpt/precisoExample.html b/integrationExamples/gpt/precisoExample.html new file mode 100644 index 00000000000..1c4fa661edd --- /dev/null +++ b/integrationExamples/gpt/precisoExample.html @@ -0,0 +1,168 @@ + + + + + + + + + + + + + +

Basic Prebid.js Example with Preciso Bidder

+

Adslot-1

+
+ +
+ +
+

Adslot-2

+ +
+ + + + diff --git a/modules/precisoBidAdapter.js b/modules/precisoBidAdapter.js index 9125f6f3911..370591bfe91 100644 --- a/modules/precisoBidAdapter.js +++ b/modules/precisoBidAdapter.js @@ -1,15 +1,24 @@ -import { logMessage, isFn, deepAccess, logInfo } from '../src/utils.js'; +import { isFn, deepAccess, logInfo, replaceAuctionPrice } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +// import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; const BIDDER_CODE = 'preciso'; +const COOKIE_NAME = '_sharedid'; const AD_URL = 'https://ssp-bidder.mndtrk.com/bid_request/openrtb'; +// const AD_URL = 'http://localhost:80/bid_request/openrtb'; const URL_SYNC = 'https://ck.2trk.info/rtb/user/usersync.aspx?'; const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; const GVLID = 874; let userId = 'NA'; +let precisoId = 'NA'; +let sharedId = 'NA' + +export const storage2 = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: BIDDER_CODE }); +export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: 'sharedId' }); export const spec = { code: BIDDER_CODE, @@ -17,44 +26,40 @@ export const spec = { gvlid: GVLID, isBidRequestValid: (bid) => { - return Boolean(bid.bidId && bid.params && !isNaN(bid.params.publisherId) && bid.params.host == 'prebid'); + sharedId = readFromAllStorages(COOKIE_NAME); + let precisoBid = true; + const preCall = 'https://ssp-usersync.mndtrk.com/getUUID?sharedId=' + sharedId; + precisoId = window.localStorage.getItem('_pre|id'); + + if (Object.is(precisoId, 'NA') || Object.is(precisoId, null) || Object.is(precisoId, undefined)) { + if (!bid.precisoBid) { + precisoBid = false; + getapi(preCall); + } + } + + return Boolean(bid.bidId && bid.params && !isNaN(bid.params.publisherId) && precisoBid); }, buildRequests: (validBidRequests = [], bidderRequest) => { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - // userId = validBidRequests[0].userId.pubcid; - let winTop = window; - let location; - var offset = new Date().getTimezoneOffset(); - logInfo('timezone ' + offset); + if (validBidRequests !== 'undefined' && validBidRequests.length > 0) { + userId = validBidRequests[0].userId.pubcid; + } + // let winTop = window; + // let location; var city = Intl.DateTimeFormat().resolvedOptions().timeZone; - logInfo('location test' + city) - - const countryCode = getCountryCodeByTimezone(city); - logInfo(`The country code for ${city} is ${countryCode}`); - - // TODO: this odd try-catch block was copied in several adapters; it doesn't seem to be correct for cross-origin - try { - location = new URL(bidderRequest.refererInfo.page) - winTop = window.top; - } catch (e) { - location = winTop.location; - logMessage(e); - }; - let request = { - id: validBidRequests[0].bidderRequestId, - + // bidRequest: bidderRequest, + id: validBidRequests[0].auctionId, + cur: validBidRequests[0].params.currency || ['USD'], imp: validBidRequests.map(request => { - const { bidId, sizes, mediaType, ortb2 } = request + const { bidId, sizes } = request const item = { id: bidId, - region: request.params.region, - traffic: mediaType, bidFloor: getBidFloor(request), - ortb2: ortb2 - + bidfloorcur: request.params.currency } if (request.mediaTypes.banner) { @@ -62,38 +67,28 @@ export const spec = { format: (request.mediaTypes.banner.sizes || sizes).map(size => { return { w: size[0], h: size[1] } }), - } - } - - if (request.schain) { - item.schain = request.schain; - } - if (request.floorData) { - item.bidFloor = request.floorData.floorMin; + } } return item }), - auctionId: validBidRequests[0].auctionId, - 'deviceWidth': winTop.screen.width, - 'deviceHeight': winTop.screen.height, - 'language': (navigator && navigator.language) ? navigator.language : '', - geo: navigator.geolocation.getCurrentPosition(position => { - const { latitude, longitude } = position.coords; - return { - latitude: latitude, - longitude: longitude - } - // Show a map centered at latitude / longitude. - }) || { utcoffset: new Date().getTimezoneOffset() }, - city: city, - 'host': location.host, - 'page': location.pathname, - 'coppa': config.getConfig('coppa') === true ? 1 : 0 - // userId: validBidRequests[0].userId + user: { + id: validBidRequests[0].userId.pubcid || '', + buyeruid: window.localStorage.getItem('_pre|id'), + geo: { + region: validBidRequests[0].params.region || city, + }, + + }, + device: validBidRequests[0].ortb2.device, + site: validBidRequests[0].ortb2.site, + source: validBidRequests[0].ortb2.source, + bcat: validBidRequests[0].ortb2.bcat || validBidRequests[0].params.bcat, + badv: validBidRequests[0].ortb2.badv || validBidRequests[0].params.badv, + wlang: validBidRequests[0].ortb2.wlang || validBidRequests[0].params.wlang }; - request.language.indexOf('-') != -1 && (request.language = request.language.split('-')[0]) + // request.language.indexOf('-') != -1 && (request.language = request.language.split('-')[0]) if (bidderRequest) { if (bidderRequest.uspConsent) { request.ccpa = bidderRequest.uspConsent; @@ -127,21 +122,21 @@ export const spec = { width: bid.w, height: bid.h, creativeId: bid.crid, - ad: bid.adm, + ad: macroReplace(bid.adm, bid.price), currency: 'USD', netRevenue: true, ttl: 300, meta: { - advertiserDomains: bid.adomain || [], + advertiserDomains: bid.adomain || '', }, }) }) }) - return bids }, getUserSyncs: (syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '', gppConsent = '') => { + userId = sharedId; let syncs = []; let { gdprApplies, consentString = '' } = gdprConsent; @@ -165,31 +160,10 @@ export const spec = { }; -function getCountryCodeByTimezone(city) { - try { - const now = new Date(); - const options = { - timeZone: city, - timeZoneName: 'long', - }; - const [timeZoneName] = new Intl.DateTimeFormat('en-US', options) - .formatToParts(now) - .filter((part) => part.type === 'timeZoneName'); - - if (timeZoneName) { - // Extract the country code from the timezone name - const parts = timeZoneName.value.split('-'); - if (parts.length >= 2) { - return parts[1]; - } - } - } catch (error) { - // Handle errors, such as an invalid timezone city - logInfo(error); - } - - // Handle the case where the city is not found or an error occurred - return 'Unknown'; +/* replacing auction_price macro from adm */ +function macroReplace(adm, cpm) { + let replacedadm = replaceAuctionPrice(adm, cpm); + return replacedadm; } function getBidFloor(bid) { @@ -209,4 +183,34 @@ function getBidFloor(bid) { } } +async function getapi(url) { + try { + // Storing response + const response = await fetch(url); + + // Storing data in form of JSON + var data = await response.json(); + + const dataMap = new Map(Object.entries(data)); + + const uuidValue = dataMap.get('UUID'); + + if (!Object.is(uuidValue, null) && !Object.is(uuidValue, undefined)) { + storage2.setDataInLocalStorage('_pre|id', uuidValue); + logInfo('DEBUG nonNull uuidValue:' + uuidValue); + } + + return data; + } catch (error) { + logInfo('Error in preciso precall' + error); + } +} + +function readFromAllStorages(name) { + const fromCookie = storage.getCookie(name); + const fromLocalStorage = storage.getDataFromLocalStorage(name); + + return fromCookie || fromLocalStorage || undefined; +} + registerBidder(spec); diff --git a/test/spec/modules/precisoBidAdapter_spec.js b/test/spec/modules/precisoBidAdapter_spec.js index 78a1615a02e..4ac1c479bb9 100644 --- a/test/spec/modules/precisoBidAdapter_spec.js +++ b/test/spec/modules/precisoBidAdapter_spec.js @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { spec } from '../../../modules/precisoBidAdapter.js'; -import { config } from '../../../src/config.js'; +// simport { config } from '../../../src/config.js'; const DEFAULT_PRICE = 1 const DEFAULT_CURRENCY = 'USD' @@ -10,6 +10,7 @@ const BIDDER_CODE = 'preciso'; describe('PrecisoAdapter', function () { let bid = { + precisoBid: true, bidId: '23fhj33i987f', bidder: 'preciso', mediaTypes: { @@ -22,15 +23,33 @@ describe('PrecisoAdapter', function () { sourceid: '0', publisherId: '0', mediaType: 'banner', - region: 'prebid-eu' + region: 'IND' }, userId: { pubcid: '12355454test' }, - geo: 'NA', - city: 'Asia,delhi' + user: { + geo: { + region: 'IND', + } + }, + ortb2: { + device: { + w: 1920, + h: 166, + dnt: 0, + }, + site: { + domain: 'localHost' + }, + source: { + tid: 'eaff09b-a1ab-4ec6-a81e-c5a75bc1dde3' + } + + } + }; describe('isBidRequestValid', function () { @@ -59,43 +78,17 @@ describe('PrecisoAdapter', function () { }); it('Returns valid data if array of bids is valid', function () { let data = serverRequest.data; - // expect(data).to.be.an('object'); - - // expect(data).to.have.all.keys('bidId', 'imp', 'site', 'deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'coppa'); - - expect(data.deviceWidth).to.be.a('number'); - expect(data.deviceHeight).to.be.a('number'); - expect(data.coppa).to.be.a('number'); - expect(data.language).to.be.a('string'); - // expect(data.secure).to.be.within(0, 1); - expect(data.host).to.be.a('string'); - expect(data.page).to.be.a('string'); - - expect(data.city).to.be.a('string'); - expect(data.geo).to.be.a('object'); - // expect(data.userId).to.be.a('string'); - // expect(data.imp).to.be.a('object'); - }); - // it('Returns empty data if no valid requests are passed', function () { - /// serverRequest = spec.buildRequests([]); - // let data = serverRequest.data; - // expect(data.imp).to.be.an('array').that.is.empty; - // }); - }); - - describe('with COPPA', function () { - beforeEach(function () { - sinon.stub(config, 'getConfig') - .withArgs('coppa') - .returns(true); + expect(data).to.be.an('object'); + expect(data.device).to.be.a('object'); + expect(data.user).to.be.a('object'); + expect(data.source).to.be.a('object'); + expect(data.site).to.be.a('object'); }); - afterEach(function () { - config.getConfig.restore(); - }); - - it('should send the Coppa "required" flag set to "1" in the request', function () { - let serverRequest = spec.buildRequests([bid]); - expect(serverRequest.data.coppa).to.equal(1); + it('Returns empty data if no valid requests are passed', function () { + delete bid.ortb2.device; + serverRequest = spec.buildRequests([bid]); + let data = serverRequest.data; + expect(data.device).to.be.undefined; }); }); From 7851caf9171ec1e7108023744015dd65d6b2706d Mon Sep 17 00:00:00 2001 From: Chris Huie Date: Tue, 28 May 2024 07:48:55 -0600 Subject: [PATCH 27/46] Revert "Preciso Bid Adapter : update on valid request checks (#11161)" (#11578) This reverts commit 646426f940c2b293a3cb90cf793b49cded8bef93. --- integrationExamples/gpt/precisoExample.html | 168 ------------------- modules/precisoBidAdapter.js | 170 ++++++++++---------- test/spec/modules/precisoBidAdapter_spec.js | 73 +++++---- 3 files changed, 123 insertions(+), 288 deletions(-) delete mode 100644 integrationExamples/gpt/precisoExample.html diff --git a/integrationExamples/gpt/precisoExample.html b/integrationExamples/gpt/precisoExample.html deleted file mode 100644 index 1c4fa661edd..00000000000 --- a/integrationExamples/gpt/precisoExample.html +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - - - - - - - - -

Basic Prebid.js Example with Preciso Bidder

-

Adslot-1

-
- -
- -
-

Adslot-2

- -
- - - - diff --git a/modules/precisoBidAdapter.js b/modules/precisoBidAdapter.js index 370591bfe91..9125f6f3911 100644 --- a/modules/precisoBidAdapter.js +++ b/modules/precisoBidAdapter.js @@ -1,24 +1,15 @@ -import { isFn, deepAccess, logInfo, replaceAuctionPrice } from '../src/utils.js'; +import { logMessage, isFn, deepAccess, logInfo } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -// import { config } from '../src/config.js'; +import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { MODULE_TYPE_UID } from '../src/activities/modules.js'; const BIDDER_CODE = 'preciso'; -const COOKIE_NAME = '_sharedid'; const AD_URL = 'https://ssp-bidder.mndtrk.com/bid_request/openrtb'; -// const AD_URL = 'http://localhost:80/bid_request/openrtb'; const URL_SYNC = 'https://ck.2trk.info/rtb/user/usersync.aspx?'; const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; const GVLID = 874; let userId = 'NA'; -let precisoId = 'NA'; -let sharedId = 'NA' - -export const storage2 = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: BIDDER_CODE }); -export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: 'sharedId' }); export const spec = { code: BIDDER_CODE, @@ -26,40 +17,44 @@ export const spec = { gvlid: GVLID, isBidRequestValid: (bid) => { - sharedId = readFromAllStorages(COOKIE_NAME); - let precisoBid = true; - const preCall = 'https://ssp-usersync.mndtrk.com/getUUID?sharedId=' + sharedId; - precisoId = window.localStorage.getItem('_pre|id'); - - if (Object.is(precisoId, 'NA') || Object.is(precisoId, null) || Object.is(precisoId, undefined)) { - if (!bid.precisoBid) { - precisoBid = false; - getapi(preCall); - } - } - - return Boolean(bid.bidId && bid.params && !isNaN(bid.params.publisherId) && precisoBid); + return Boolean(bid.bidId && bid.params && !isNaN(bid.params.publisherId) && bid.params.host == 'prebid'); }, buildRequests: (validBidRequests = [], bidderRequest) => { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - if (validBidRequests !== 'undefined' && validBidRequests.length > 0) { - userId = validBidRequests[0].userId.pubcid; - } - // let winTop = window; - // let location; + // userId = validBidRequests[0].userId.pubcid; + let winTop = window; + let location; + var offset = new Date().getTimezoneOffset(); + logInfo('timezone ' + offset); var city = Intl.DateTimeFormat().resolvedOptions().timeZone; + logInfo('location test' + city) + + const countryCode = getCountryCodeByTimezone(city); + logInfo(`The country code for ${city} is ${countryCode}`); + + // TODO: this odd try-catch block was copied in several adapters; it doesn't seem to be correct for cross-origin + try { + location = new URL(bidderRequest.refererInfo.page) + winTop = window.top; + } catch (e) { + location = winTop.location; + logMessage(e); + }; + let request = { - // bidRequest: bidderRequest, - id: validBidRequests[0].auctionId, - cur: validBidRequests[0].params.currency || ['USD'], + id: validBidRequests[0].bidderRequestId, + imp: validBidRequests.map(request => { - const { bidId, sizes } = request + const { bidId, sizes, mediaType, ortb2 } = request const item = { id: bidId, + region: request.params.region, + traffic: mediaType, bidFloor: getBidFloor(request), - bidfloorcur: request.params.currency + ortb2: ortb2 + } if (request.mediaTypes.banner) { @@ -67,28 +62,38 @@ export const spec = { format: (request.mediaTypes.banner.sizes || sizes).map(size => { return { w: size[0], h: size[1] } }), - } } + + if (request.schain) { + item.schain = request.schain; + } + + if (request.floorData) { + item.bidFloor = request.floorData.floorMin; + } return item }), - user: { - id: validBidRequests[0].userId.pubcid || '', - buyeruid: window.localStorage.getItem('_pre|id'), - geo: { - region: validBidRequests[0].params.region || city, - }, - - }, - device: validBidRequests[0].ortb2.device, - site: validBidRequests[0].ortb2.site, - source: validBidRequests[0].ortb2.source, - bcat: validBidRequests[0].ortb2.bcat || validBidRequests[0].params.bcat, - badv: validBidRequests[0].ortb2.badv || validBidRequests[0].params.badv, - wlang: validBidRequests[0].ortb2.wlang || validBidRequests[0].params.wlang + auctionId: validBidRequests[0].auctionId, + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language : '', + geo: navigator.geolocation.getCurrentPosition(position => { + const { latitude, longitude } = position.coords; + return { + latitude: latitude, + longitude: longitude + } + // Show a map centered at latitude / longitude. + }) || { utcoffset: new Date().getTimezoneOffset() }, + city: city, + 'host': location.host, + 'page': location.pathname, + 'coppa': config.getConfig('coppa') === true ? 1 : 0 + // userId: validBidRequests[0].userId }; - // request.language.indexOf('-') != -1 && (request.language = request.language.split('-')[0]) + request.language.indexOf('-') != -1 && (request.language = request.language.split('-')[0]) if (bidderRequest) { if (bidderRequest.uspConsent) { request.ccpa = bidderRequest.uspConsent; @@ -122,21 +127,21 @@ export const spec = { width: bid.w, height: bid.h, creativeId: bid.crid, - ad: macroReplace(bid.adm, bid.price), + ad: bid.adm, currency: 'USD', netRevenue: true, ttl: 300, meta: { - advertiserDomains: bid.adomain || '', + advertiserDomains: bid.adomain || [], }, }) }) }) + return bids }, getUserSyncs: (syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '', gppConsent = '') => { - userId = sharedId; let syncs = []; let { gdprApplies, consentString = '' } = gdprConsent; @@ -160,10 +165,31 @@ export const spec = { }; -/* replacing auction_price macro from adm */ -function macroReplace(adm, cpm) { - let replacedadm = replaceAuctionPrice(adm, cpm); - return replacedadm; +function getCountryCodeByTimezone(city) { + try { + const now = new Date(); + const options = { + timeZone: city, + timeZoneName: 'long', + }; + const [timeZoneName] = new Intl.DateTimeFormat('en-US', options) + .formatToParts(now) + .filter((part) => part.type === 'timeZoneName'); + + if (timeZoneName) { + // Extract the country code from the timezone name + const parts = timeZoneName.value.split('-'); + if (parts.length >= 2) { + return parts[1]; + } + } + } catch (error) { + // Handle errors, such as an invalid timezone city + logInfo(error); + } + + // Handle the case where the city is not found or an error occurred + return 'Unknown'; } function getBidFloor(bid) { @@ -183,34 +209,4 @@ function getBidFloor(bid) { } } -async function getapi(url) { - try { - // Storing response - const response = await fetch(url); - - // Storing data in form of JSON - var data = await response.json(); - - const dataMap = new Map(Object.entries(data)); - - const uuidValue = dataMap.get('UUID'); - - if (!Object.is(uuidValue, null) && !Object.is(uuidValue, undefined)) { - storage2.setDataInLocalStorage('_pre|id', uuidValue); - logInfo('DEBUG nonNull uuidValue:' + uuidValue); - } - - return data; - } catch (error) { - logInfo('Error in preciso precall' + error); - } -} - -function readFromAllStorages(name) { - const fromCookie = storage.getCookie(name); - const fromLocalStorage = storage.getDataFromLocalStorage(name); - - return fromCookie || fromLocalStorage || undefined; -} - registerBidder(spec); diff --git a/test/spec/modules/precisoBidAdapter_spec.js b/test/spec/modules/precisoBidAdapter_spec.js index 4ac1c479bb9..78a1615a02e 100644 --- a/test/spec/modules/precisoBidAdapter_spec.js +++ b/test/spec/modules/precisoBidAdapter_spec.js @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { spec } from '../../../modules/precisoBidAdapter.js'; -// simport { config } from '../../../src/config.js'; +import { config } from '../../../src/config.js'; const DEFAULT_PRICE = 1 const DEFAULT_CURRENCY = 'USD' @@ -10,7 +10,6 @@ const BIDDER_CODE = 'preciso'; describe('PrecisoAdapter', function () { let bid = { - precisoBid: true, bidId: '23fhj33i987f', bidder: 'preciso', mediaTypes: { @@ -23,33 +22,15 @@ describe('PrecisoAdapter', function () { sourceid: '0', publisherId: '0', mediaType: 'banner', - region: 'IND' + region: 'prebid-eu' }, userId: { pubcid: '12355454test' }, - user: { - geo: { - region: 'IND', - } - }, - ortb2: { - device: { - w: 1920, - h: 166, - dnt: 0, - }, - site: { - domain: 'localHost' - }, - source: { - tid: 'eaff09b-a1ab-4ec6-a81e-c5a75bc1dde3' - } - - } - + geo: 'NA', + city: 'Asia,delhi' }; describe('isBidRequestValid', function () { @@ -78,17 +59,43 @@ describe('PrecisoAdapter', function () { }); it('Returns valid data if array of bids is valid', function () { let data = serverRequest.data; - expect(data).to.be.an('object'); - expect(data.device).to.be.a('object'); - expect(data.user).to.be.a('object'); - expect(data.source).to.be.a('object'); - expect(data.site).to.be.a('object'); + // expect(data).to.be.an('object'); + + // expect(data).to.have.all.keys('bidId', 'imp', 'site', 'deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'coppa'); + + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.coppa).to.be.a('number'); + expect(data.language).to.be.a('string'); + // expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + + expect(data.city).to.be.a('string'); + expect(data.geo).to.be.a('object'); + // expect(data.userId).to.be.a('string'); + // expect(data.imp).to.be.a('object'); }); - it('Returns empty data if no valid requests are passed', function () { - delete bid.ortb2.device; - serverRequest = spec.buildRequests([bid]); - let data = serverRequest.data; - expect(data.device).to.be.undefined; + // it('Returns empty data if no valid requests are passed', function () { + /// serverRequest = spec.buildRequests([]); + // let data = serverRequest.data; + // expect(data.imp).to.be.an('array').that.is.empty; + // }); + }); + + describe('with COPPA', function () { + beforeEach(function () { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + }); + afterEach(function () { + config.getConfig.restore(); + }); + + it('should send the Coppa "required" flag set to "1" in the request', function () { + let serverRequest = spec.buildRequests([bid]); + expect(serverRequest.data.coppa).to.equal(1); }); }); From 8620ae4464bd0a7bbe4e112d5b636715cc2d8021 Mon Sep 17 00:00:00 2001 From: SmartHubSolutions <87376145+SmartHubSolutions@users.noreply.github.com> Date: Tue, 28 May 2024 16:51:16 +0300 Subject: [PATCH 28/46] update adapter SmartHub: add aliases (#11553) --- modules/smarthubBidAdapter.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/smarthubBidAdapter.js b/modules/smarthubBidAdapter.js index 0be3e6831e7..ea2b62c95c9 100644 --- a/modules/smarthubBidAdapter.js +++ b/modules/smarthubBidAdapter.js @@ -5,10 +5,16 @@ import {config} from '../src/config.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; const BIDDER_CODE = 'smarthub'; -const ALIASES = [{code: 'markapp', skipPbsAliasing: true}]; +const ALIASES = [ + {code: 'markapp', skipPbsAliasing: true}, + {code: 'jdpmedia', skipPbsAliasing: true}, + {code: 'tredio', skipPbsAliasing: true}, +]; const BASE_URLS = { smarthub: 'https://prebid.smart-hub.io/pbjs', - markapp: 'https://markapp-prebid.smart-hub.io/pbjs' + markapp: 'https://markapp-prebid.smart-hub.io/pbjs', + jdpmedia: 'https://jdpmedia-prebid.smart-hub.io/pbjs', + tredio: 'https://tredio-prebid.smart-hub.io/pbjs' }; function getUrl(partnerName) { From b44deb8944a10465c16b1df19634420f04929e3c Mon Sep 17 00:00:00 2001 From: Andrea Tumbarello Date: Tue, 28 May 2024 16:46:56 +0200 Subject: [PATCH 29/46] AIDEM Bid Adapter : fixed getConfig cleanup of consent management (#11575) * AIDEM Bid Adapter * Added _spec.js * update * Fix Navigator in _spec.js * Removed timeout handler. * Added publisherId as required bidder params * moved publisherId into site publisher object * Added wpar to environment * Added placementId parameter * added unit tests for the wpar environment object * PlacementId is now a required parameter Added optional rateLimit parameter Added publisherId, siteId, placementId in win notice payload Added unit tests * Revert to optional placementId parameter Added missing semicolons * Extended win notice * Added arbitrary ext field to win notice * Moved aidemBidAdapter implementation to comply with ortbConverter * disabled video-specific tests * Fixed getConfig cleanup of consent management (Issue #10658) * Fixed getConfig cleanup of consent management (Issue #10658) * Fixed getConfig cleanup of consent management (Issue #10658) * Fixed getConfig cleanup of consent management (Issue #10658) --------- Co-authored-by: Giovanni Sollazzo Co-authored-by: darkstar Co-authored-by: AndreaC <67786179+darkstarac@users.noreply.github.com> --- modules/aidemBidAdapter.js | 18 ++-- test/spec/modules/aidemBidAdapter_spec.js | 120 +++++++++++++++------- 2 files changed, 93 insertions(+), 45 deletions(-) diff --git a/modules/aidemBidAdapter.js b/modules/aidemBidAdapter.js index c6a5cd96fb6..0730149e909 100644 --- a/modules/aidemBidAdapter.js +++ b/modules/aidemBidAdapter.js @@ -59,7 +59,7 @@ const converter = ortbConverter({ const request = buildRequest(imps, bidderRequest, context); deepSetValue(request, 'at', 1); setPrebidRequestEnvironment(request); - deepSetValue(request, 'regs', getRegs()); + deepSetValue(request, 'regs', getRegs(bidderRequest)); deepSetValue(request, 'site.publisher.id', bidderRequest.bids[0].params.publisherId); deepSetValue(request, 'site.id', bidderRequest.bids[0].params.siteId); return request; @@ -106,22 +106,22 @@ function recur(obj) { return result; } -function getRegs() { +function getRegs(bidderRequest) { let regs = {}; - const consentManagement = config.getConfig('consentManagement'); + const euConsentManagement = bidderRequest.gdprConsent; + const usConsentManagement = bidderRequest.uspConsent; const coppa = config.getConfig('coppa'); - if (consentManagement && !!(consentManagement.gdpr)) { - deepSetValue(regs, 'gdpr_applies', !!consentManagement.gdpr); + if (euConsentManagement && euConsentManagement.consentString) { + deepSetValue(regs, 'gdpr_applies', !!euConsentManagement.consentString); } else { deepSetValue(regs, 'gdpr_applies', false); } - if (consentManagement && deepAccess(consentManagement, 'usp.cmpApi') === 'static') { - deepSetValue(regs, 'usp_applies', !!deepAccess(consentManagement, 'usp')); - deepSetValue(regs, 'us_privacy', deepAccess(consentManagement, 'usp.consentData.getUSPData.uspString')); + if (usConsentManagement) { + deepSetValue(regs, 'usp_applies', true); + deepSetValue(regs, 'us_privacy', bidderRequest.uspConsent); } else { deepSetValue(regs, 'usp_applies', false); } - if (isBoolean(coppa)) { deepSetValue(regs, 'coppa_applies', !!coppa); } else { diff --git a/test/spec/modules/aidemBidAdapter_spec.js b/test/spec/modules/aidemBidAdapter_spec.js index 3de348197b2..c9d29ff09dd 100644 --- a/test/spec/modules/aidemBidAdapter_spec.js +++ b/test/spec/modules/aidemBidAdapter_spec.js @@ -168,7 +168,7 @@ const DEFAULT_VALID_BANNER_REQUESTS = [ }, params: { siteId: '1', - placementId: '13144370' + placementId: '13144370', }, src: 'client', transactionId: 'db739693-9b4a-4669-9945-8eab938783cc' @@ -193,7 +193,7 @@ const DEFAULT_VALID_VIDEO_REQUESTS = [ }, params: { siteId: '1', - placementId: '13144370' + placementId: '13144370', }, src: 'client', transactionId: 'db739693-9b4a-4669-9945-8eab938783cc' @@ -209,7 +209,50 @@ const VALID_BIDDER_REQUEST = { params: { placementId: '13144370', siteId: '23434', - publisherId: '7689670753' + publisherId: '7689670753', + }, + } + ], + refererInfo: { + page: 'test-page', + domain: 'test-domain', + ref: 'test-referer' + }, +} + +const VALID_GDPR_BIDDER_REQUEST = { + auctionId: '19c97f22-5bd1-4b16-a128-80f75fb0a8a0', + bidderCode: 'aidem', + bidderRequestId: '1bbb7854dfa0d8', + bids: [ + { + params: { + placementId: '13144370', + siteId: '23434', + publisherId: '7689670753', + }, + } + ], + refererInfo: { + page: 'test-page', + domain: 'test-domain', + ref: 'test-referer' + }, + gdprConsent: { + consentString: 'test-gdpr-string' + } +} + +const VALID_USP_BIDDER_REQUEST = { + auctionId: '19c97f22-5bd1-4b16-a128-80f75fb0a8a0', + bidderCode: 'aidem', + bidderRequestId: '1bbb7854dfa0d8', + bids: [ + { + params: { + placementId: '13144370', + siteId: '23434', + publisherId: '7689670753', }, } ], @@ -218,6 +261,7 @@ const VALID_BIDDER_REQUEST = { domain: 'test-domain', ref: 'test-referer' }, + uspConsent: '1YYY' } const SERVER_RESPONSE_BANNER = { @@ -601,47 +645,51 @@ describe('Aidem adapter', () => { }); it(`should set gdpr to true`, function () { - config.setConfig({ - consentManagement: { - gdpr: { - // any data here set gdpr to true - }, - } - }); - const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); + // config.setConfig({ + // consentManagement: { + // gdpr: { + // consentData: { + // getTCData: { + // tcString: 'test-gdpr-string' + // } + // } + // }, + // } + // }); + const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_GDPR_BIDDER_REQUEST); expect(data.regs.gdpr_applies).to.equal(true) }); it(`should set usp_consent string`, function () { - config.setConfig({ - consentManagement: { - usp: { - cmpApi: 'static', - consentData: { - getUSPData: { - uspString: '1YYY' - } - } - } - } - }); - const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); + // config.setConfig({ + // consentManagement: { + // usp: { + // cmpApi: 'static', + // consentData: { + // getUSPData: { + // uspString: '1YYY' + // } + // } + // } + // } + // }); + const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_USP_BIDDER_REQUEST); expect(data.regs.us_privacy).to.equal('1YYY') }); it(`should not set usp_consent string`, function () { - config.setConfig({ - consentManagement: { - usp: { - cmpApi: 'iab', - consentData: { - getUSPData: { - uspString: '1YYY' - } - } - } - } - }); + // config.setConfig({ + // consentManagement: { + // usp: { + // cmpApi: 'iab', + // consentData: { + // getUSPData: { + // uspString: '1YYY' + // } + // } + // } + // } + // }); const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); expect(data.regs.us_privacy).to.undefined }); From f6040c445428071cf4547b5663fd2d2e86bc74c6 Mon Sep 17 00:00:00 2001 From: Parth Shah Date: Tue, 28 May 2024 20:36:50 +0530 Subject: [PATCH 30/46] DeepIntent Bid Adapter : update video.placement to video.plcmt (#11577) * fix user ids not being passed on page reload due to extendId func miss * remove extendId and add function to read value for eids * handle inconsistent localstorage values in deepintent id * remove unused imports * revert to getValue changes * revert version in package-lock * update test suite for deepintent id system * fix - video.placement was deprecated in favor of video.plcmt --- modules/deepintentBidAdapter.js | 2 +- modules/deepintentBidAdapter.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/deepintentBidAdapter.js b/modules/deepintentBidAdapter.js index 0a64ed88ca5..f0c76ae557b 100644 --- a/modules/deepintentBidAdapter.js +++ b/modules/deepintentBidAdapter.js @@ -14,7 +14,7 @@ export const ORTB_VIDEO_PARAMS = { 'w': (value) => isInteger(value), 'h': (value) => isInteger(value), 'startdelay': (value) => isInteger(value), - 'placement': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 5), + 'plcmt': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 5), 'linearity': (value) => [1, 2].indexOf(value) !== -1, 'skip': (value) => [0, 1].indexOf(value) !== -1, 'skipmin': (value) => isInteger(value), diff --git a/modules/deepintentBidAdapter.md b/modules/deepintentBidAdapter.md index 84c375d69a4..0f017402d1d 100644 --- a/modules/deepintentBidAdapter.md +++ b/modules/deepintentBidAdapter.md @@ -66,7 +66,7 @@ var adVideoAdUnits = [ protocols: [ 2, 3 ], // optional battr: [ 13, 14 ], // optional linearity: 1, // optional - placement: 2, // optional + plcmt: 2, // optional minbitrate: 10, // optional maxbitrate: 10 // optional } From 6b446e602d300e7bf949259e29ef1ec5b3d0144a Mon Sep 17 00:00:00 2001 From: IQZoneAdx <88879712+IQZoneAdx@users.noreply.github.com> Date: Tue, 28 May 2024 18:22:04 +0300 Subject: [PATCH 31/46] IQzone Bid Adapter : update placement to plcmt and move coppa from getConfig (#11562) * add IQZone adapter * add endpointId param * add user sync * added gpp support * added support of transanctionId and eids * updated tests * changed placement to plcmt --- modules/iqzoneBidAdapter.js | 58 ++++++++++++----- test/spec/modules/iqzoneBidAdapter_spec.js | 75 ++++++++++++++++++++-- 2 files changed, 111 insertions(+), 22 deletions(-) diff --git a/modules/iqzoneBidAdapter.js b/modules/iqzoneBidAdapter.js index 52f3be7e4b4..3ce622dba10 100644 --- a/modules/iqzoneBidAdapter.js +++ b/modules/iqzoneBidAdapter.js @@ -1,4 +1,4 @@ -import { isFn, deepAccess, logMessage } from '../src/utils.js'; +import { logMessage, logError, deepAccess } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; @@ -9,8 +9,7 @@ const AD_URL = 'https://smartssp-us-east.iqzone.com/pbjs'; const SYNC_URL = 'https://cs.smartssp.iqzone.com'; function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { return false; } @@ -27,7 +26,7 @@ function isBidResponseValid(bid) { } function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; + const { params, bidId, mediaTypes, transactionId, userIdAsEids } = bid; const schain = bid.schain || {}; const { placementId, endpointId } = params; const bidfloor = getBidFloor(bid); @@ -57,7 +56,7 @@ function getPlacementReqData(bid) { placement.mimes = mediaTypes[VIDEO].mimes; placement.protocols = mediaTypes[VIDEO].protocols; placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; + placement.plcmt = mediaTypes[VIDEO].plcmt; placement.skip = mediaTypes[VIDEO].skip; placement.skipafter = mediaTypes[VIDEO].skipafter; placement.minbitrate = mediaTypes[VIDEO].minbitrate; @@ -71,14 +70,19 @@ function getPlacementReqData(bid) { placement.adFormat = NATIVE; } + if (transactionId) { + placement.ext = placement.ext || {}; + placement.ext.tid = transactionId; + } + + if (userIdAsEids && userIdAsEids.length) { + placement.eids = userIdAsEids; + } + return placement; } function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - try { const bidFloor = bid.getFloor({ currency: 'USD', @@ -86,8 +90,9 @@ function getBidFloor(bid) { size: '*', }); return bidFloor.floor; - } catch (_) { - return 0 + } catch (err) { + logError(err); + return 0; } } @@ -151,12 +156,28 @@ export const spec = { host, page, placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, + coppa: deepAccess(bidderRequest, 'ortb2.regs.coppa') ? 1 : 0, tmax: bidderRequest.timeout }; + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + + if (bidderRequest.gdprConsent) { + request.gdpr = { + consentString: bidderRequest.gdprConsent.consentString + }; + } + + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent.gppString; + request.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + request.gpp = bidderRequest.ortb2.regs.gpp; + request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } + const len = validBidRequests.length; for (let i = 0; i < len; i++) { const bid = validBidRequests[i]; @@ -184,9 +205,10 @@ export const spec = { return response; }, - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { if (typeof gdprConsent.gdprApplies === 'boolean') { syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; @@ -194,10 +216,16 @@ export const spec = { syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; } } + if (uspConsent && uspConsent.consentString) { syncUrl += `&ccpa_consent=${uspConsent.consentString}`; } + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + syncUrl += '&gpp=' + gppConsent.gppString; + syncUrl += '&gpp_sid=' + gppConsent.applicableSections.join(','); + } + const coppa = config.getConfig('coppa') ? 1 : 0; syncUrl += `&coppa=${coppa}`; diff --git a/test/spec/modules/iqzoneBidAdapter_spec.js b/test/spec/modules/iqzoneBidAdapter_spec.js index 2e920d3b769..9d012e526e2 100644 --- a/test/spec/modules/iqzoneBidAdapter_spec.js +++ b/test/spec/modules/iqzoneBidAdapter_spec.js @@ -6,6 +6,16 @@ import { getUniqueIdentifierStr } from '../../../src/utils.js'; const bidder = 'iqzone' describe('IQZoneBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -17,7 +27,8 @@ describe('IQZoneBidAdapter', function () { }, params: { placementId: 'testBanner', - } + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -31,7 +42,8 @@ describe('IQZoneBidAdapter', function () { }, params: { placementId: 'testVideo', - } + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -54,7 +66,8 @@ describe('IQZoneBidAdapter', function () { }, params: { placementId: 'testNative', - } + }, + userIdAsEids } ]; @@ -73,7 +86,9 @@ describe('IQZoneBidAdapter', function () { const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw' + }, refererInfo: { referer: 'https://test.com' }, @@ -129,7 +144,7 @@ describe('IQZoneBidAdapter', function () { expect(data.host).to.be.a('string'); expect(data.page).to.be.a('string'); expect(data.coppa).to.be.a('number'); - expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.be.a('object'); expect(data.ccpa).to.be.a('string'); expect(data.tmax).to.be.a('number'); expect(data.placements).to.have.lengthOf(3); @@ -145,6 +160,7 @@ describe('IQZoneBidAdapter', function () { expect(placement.schain).to.be.an('object'); expect(placement.bidfloor).to.exist.and.to.equal(0); expect(placement.type).to.exist.and.to.equal('publisher'); + expect(placement.eids).to.exist.and.to.be.deep.equal(userIdAsEids); if (placement.adFormat === BANNER) { expect(placement.sizes).to.be.an('array'); @@ -170,8 +186,8 @@ describe('IQZoneBidAdapter', function () { serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.gdpr).to.be.a('object'); + expect(data.gdpr.consentString).to.equal(bidderRequest.gdprConsent.consentString); expect(data.ccpa).to.not.exist; delete bidderRequest.gdprConsent; }); @@ -194,6 +210,38 @@ describe('IQZoneBidAdapter', function () { }); }); + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + delete bidderRequest.gppConsent; + }) + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2 = bidderRequest.ortb2 || {}; + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) + }); + describe('interpretResponse', function () { it('Should interpret banner response', function () { const banner = { @@ -370,6 +418,7 @@ describe('IQZoneBidAdapter', function () { expect(serverResponses).to.be.an('array').that.is.empty; }); }); + describe('getUserSyncs', function() { it('Should return array of objects with proper sync config , include GDPR', function() { const syncData = spec.getUserSyncs({}, {}, { @@ -394,5 +443,17 @@ describe('IQZoneBidAdapter', function () { expect(syncData[0].url).to.be.a('string') expect(syncData[0].url).to.equal('https://cs.smartssp.iqzone.com/image?pbjs=1&ccpa_consent=1---&coppa=0') }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.smartssp.iqzone.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); }); }); From 78f93cd29ee0955c6ddf2cf20f5251ac8e8c66e1 Mon Sep 17 00:00:00 2001 From: Brett Bloxom <38990705+BrettBlox@users.noreply.github.com> Date: Tue, 28 May 2024 09:51:41 -0600 Subject: [PATCH 32/46] Concert Bid Adapter : add dealId Property to Bid Responses (#11582) * collect EIDs for bid request * add ad slot positioning to payload * RPO-2012: Update local storage name-spacing for c_uid (#8) * Updates c_uid namespacing to be more specific for concert * fixes unit tests * remove console.log * RPO-2012: Add check for shared id (#9) * Adds check for sharedId * Updates cookie name * remove trailing comma * [RPO-3152] Enable Support for GPP Consent (#12) * Adds gpp consent integration to concert bid adapter * Update tests to check for gpp consent string param * removes user sync endpoint and tests * updates comment * cleans up consentAllowsPpid function * comment fix * rename variables for clarity * fixes conditional logic for consent allows function (#13) * [RPO-3262] Update getUid function to check for pubcid and sharedid (#14) * Update getUid function to check for pubcid and sharedid * updates adapter version * [RPO-3405] Add browserLanguage to request meta object * ConcertBidAdapter: Add TDID (#20) * Add tdid to meta object * Fix null handling and add tests * Concert Bid Adapter: Add dealId Property to Bid Responses (#22) * adds dealid property to bid responses * updates tests * use first bid for tests * adds dealid at the correct level --------- Co-authored-by: antoin Co-authored-by: Antoin Co-authored-by: Sam Ghitelman Co-authored-by: Sam Ghitelman --- modules/concertBidAdapter.js | 3 ++- test/spec/modules/concertBidAdapter_spec.js | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/modules/concertBidAdapter.js b/modules/concertBidAdapter.js index bd738a39bba..12dba194844 100644 --- a/modules/concertBidAdapter.js +++ b/modules/concertBidAdapter.js @@ -128,7 +128,8 @@ export const spec = { meta: { advertiserDomains: bid && bid.adomain ? bid.adomain : [] }, creativeId: bid.creativeId, netRevenue: bid.netRevenue, - currency: bid.currency + currency: bid.currency, + ...(bid.dealid && { dealId: bid.dealid }), }; }); diff --git a/test/spec/modules/concertBidAdapter_spec.js b/test/spec/modules/concertBidAdapter_spec.js index 0a76ed3e62d..2fb43236081 100644 --- a/test/spec/modules/concertBidAdapter_spec.js +++ b/test/spec/modules/concertBidAdapter_spec.js @@ -249,6 +249,22 @@ describe('ConcertAdapter', function () { }); }); + it('should include dealId when present in bidResponse', function() { + const bids = spec.interpretResponse({ + body: { + bids: [ + { ...bidResponse.body.bids[0], dealid: 'CON-123' } + ] + } + }, bidRequest); + expect(bids[0]).to.have.property('dealId'); + }); + + it('should exclude dealId when absent in bidResponse', function() { + const bids = spec.interpretResponse(bidResponse, bidRequest); + expect(bids[0]).to.not.have.property('dealId'); + }); + it('should return empty bids if there is no response from server', function() { const bids = spec.interpretResponse({ body: null }, bidRequest); expect(bids).to.have.lengthOf(0); From 8309f29c8691f1dde45f688b88d683842a00fca7 Mon Sep 17 00:00:00 2001 From: nouchy <33549554+nouchy@users.noreply.github.com> Date: Tue, 28 May 2024 20:17:50 +0200 Subject: [PATCH 33/46] Sirdata RTD Module : add eids and post content support and get ready for PBJS 9.0 (#11524) * Add eids and post content suport * multi-character sanitization Modify regular expression to match comments containing newlines. * recursively removing elements to avoid an HTML element injection vulnerability * fix expression vs expected assignment * fix return statement for loop * run completeSanitization instead of sanitizeContent * fix multi-character sanitization * Update sirdataRtdProvider.js * fix missing moduleConfig param * Add filtering eids based on whitelist * fixe default Eids --- modules/sirdataRtdProvider.js | 779 ++++++++++++++----- modules/sirdataRtdProvider.md | 216 +++-- test/spec/modules/sirdataRtdProvider_spec.js | 155 +++- 3 files changed, 827 insertions(+), 323 deletions(-) diff --git a/modules/sirdataRtdProvider.js b/modules/sirdataRtdProvider.js index 29d10a3a071..0ad3b891c40 100644 --- a/modules/sirdataRtdProvider.js +++ b/modules/sirdataRtdProvider.js @@ -7,21 +7,35 @@ * @module modules/sirdataRtdProvider * @requires module:modules/realTimeData */ -import {deepAccess, deepSetValue, isEmpty, logError, logInfo, mergeDeep} from '../src/utils.js'; -import {submodule} from '../src/hook.js'; -import {ajax} from '../src/ajax.js'; -import {findIndex} from '../src/polyfill.js'; -import {getRefererInfo} from '../src/refererDetection.js'; -import {config} from '../src/config.js'; -import {getGlobal} from '../src/prebidGlobal.js'; +import adapterManager from '../src/adapterManager.js'; +import { ajax } from '../src/ajax.js'; +import { + deepAccess, checkCookieSupport, deepSetValue, hasDeviceAccess, inIframe, isEmpty, + logError, logInfo, mergeDeep +} from '../src/utils.js'; +import { findIndex } from '../src/polyfill.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { getRefererInfo } from '../src/refererDetection.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { submodule } from '../src/hook.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'SirdataRTDModule'; const ORTB2_NAME = 'sirdata.com'; const LOG_PREFIX = 'Sirdata RTD: '; +const EUIDS_STORAGE_NAME = 'SDDAN'; +// Get the cookie domain from the referer info or fallback to window location hostname +const cookieDomain = getRefererInfo().domain || window.location.hostname; -const partnerIds = { +/** @type {number} */ +const GVLID = 53; + +/** @type {object} */ +const STORAGE = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }); +const bidderAliasRegistry = adapterManager.aliasRegistry || {}; +const biddersId = { // Partner IDs mapping for different SSPs and DSPs 'criteo': 27443, 'openx': 30342, 'pubmatic': 30345, @@ -30,25 +44,9 @@ const partnerIds = { 'yahoossp': 30339, 'rubicon': 27452, 'appnexus': 27446, - 'appnexusAst': 27446, - 'brealtime': 27446, - 'emetriq': 27446, - 'emxdigital': 27446, - 'pagescience': 27446, 'gourmetads': 33394, - 'matomy': 27446, - 'featureforward': 27446, - 'oftmedia': 27446, - 'districtm': 27446, - 'adasta': 27446, - 'beintoo': 27446, - 'gravity': 27446, - 'msq_classic': 27878, - 'msq_max': 27878, - '366_apx': 27878, 'mediasquare': 27878, 'smartadserver': 27440, - 'smart': 27440, 'proxistore': 27484, 'ix': 27248, 'sdRtdForGpt': 27449, @@ -65,57 +63,370 @@ const partnerIds = { 'zeta_global_ssp': 33385, }; -export function getSegmentsAndCategories(reqBidsConfigObj, onDone, moduleConfig, userConsent) { - logInfo(LOG_PREFIX, 'init'); - moduleConfig.params = moduleConfig.params || {}; - - let tcString = (userConsent && userConsent.gdpr && userConsent.gdpr.consentString ? userConsent.gdpr.consentString : ''); - let gdprApplies = (userConsent && userConsent.gdpr && userConsent.gdpr.gdprApplies ? userConsent.gdpr.gdprApplies : ''); - - moduleConfig.params.partnerId = moduleConfig.params.partnerId ? moduleConfig.params.partnerId : 1; - moduleConfig.params.key = moduleConfig.params.key ? moduleConfig.params.key : 1; - moduleConfig.params.actualUrl = moduleConfig.params.actualUrl || null; - - let sirdataDomain; - let sendWithCredentials; - - if (userConsent.coppa || (userConsent.usp && (userConsent.usp[0] === '1' && (userConsent.usp[1] === 'N' || userConsent.usp[2] === 'Y')))) { - // if children or "Do not Sell" management in California, no segments, page categories only whatever TCF signal - sirdataDomain = 'cookieless-data.com'; - sendWithCredentials = false; - gdprApplies = null; - tcString = ''; - } else if (config.getConfig('consentManagement.gdpr')) { - // Default endpoint for Contextual results only is cookieless if gdpr management is set. Needed because the cookie-based endpoint will fail and return error if user is located in Europe and no consent has been given - sirdataDomain = 'cookieless-data.com'; - sendWithCredentials = false; +const eidsProvidersMap = { + 'id5': 'id5-sync.com', + 'id5id': 'id5-sync.com', + 'id5_id': 'id5-sync.com', + 'pubprovided_id': 'pubProvidedId', + 'ppid': 'pubProvidedId', + 'first-id.fr': 'pubProvidedId', + 'sharedid': 'pubcid.org', + 'publishercommonid': 'pubcid.org', + 'pubcid.org': 'pubcid.org', +} + +// params +let params = { + partnerId: 1, + key: 1, + actualUrl: getRefererInfo().stack.pop() || getRefererInfo().page, + cookieAccessGranted: false, + setGptKeyValues: true, + contextualMinRelevancyScore: 30, + preprod: false, + authorizedEids: ['pubProvidedId', 'id5-sync.com', 'pubcid.org'], + avoidPostContent: false, + sirdataDomain: 'cookieless-data.com', + bidders: [] +}; + +/** + * Sets a cookie on the top-level domain + * @param {string} key - The cookie name + * @param {string} value - The cookie value + * @param {string} hostname - The hostname for setting the cookie + * @param {boolean} deleteCookie - The cookie must be deleted + * @returns {boolean} - True if the cookie was successfully set, otherwise false + */ +export function setCookieOnTopDomain(key, value, hostname, deleteCookie) { + const subDomains = hostname.split('.'); + let expTime = new Date(); + expTime.setTime(expTime.getTime() + (deleteCookie ? -1 : 365 * 24 * 60 * 60 * 1000)); // Set expiration time + for (let i = 0; i < subDomains.length; ++i) { + // Try to write the cookie on this subdomain (we want it to be stored only on the TLD+1) + const domain = subDomains.slice(subDomains.length - i - 1).join('.'); + try { + STORAGE.setCookie(key, value, expTime.toUTCString(), 'Lax', '.' + domain); + // Try to read the cookie to check if we wrote it + if (STORAGE.getCookie(key, null) === value) return true; // Check if the cookie was set, and if so top domain was found. If deletion with expire date -1 will parse until complete host + } catch (e) { + logError(LOG_PREFIX, e); + } } + return false; +} - // default global endpoint is cookie-based if no rules falls into cookieless or consent has been given or GDPR doesn't apply +/** + * Retrieves the UID from storage (cookies or local storage) + * @returns {Array|null} - Array of UID objects or null if no UID found + */ +export function getUidFromStorage() { + let cUid = STORAGE.getCookie(EUIDS_STORAGE_NAME, null); + let lsUid = STORAGE.getDataFromLocalStorage(EUIDS_STORAGE_NAME, null); + if (cUid && (!lsUid || cUid !== lsUid)) { + STORAGE.setDataInLocalStorage(EUIDS_STORAGE_NAME, cUid, null); + } else if (lsUid && !cUid) { + setCookieOnTopDomain(EUIDS_STORAGE_NAME, lsUid, cookieDomain, false); + cUid = lsUid; + } + return cUid ? [{ source: 'sddan.com', uids: [{ id: cUid, atype: 1 }] }] : null; +} - if (!sirdataDomain || !gdprApplies || (deepAccess(userConsent, 'gdpr.vendorData.vendor.consents') && userConsent.gdpr.vendorData.vendor.consents[53] && userConsent.gdpr.vendorData.purpose.consents[1] && userConsent.gdpr.vendorData.purpose.consents[4])) { - sirdataDomain = 'sddan.com'; - sendWithCredentials = true; +/** + * Sets the UID in storage (cookies and local storage) + * @param {string} sddanId - The UID to be set + * @returns {boolean} - True if the UID was successfully set, otherwise false + */ +export function setUidInStorage(sddanId) { + if (!sddanId) return false; + sddanId = encodeURI(sddanId.toString()); + setCookieOnTopDomain(EUIDS_STORAGE_NAME, sddanId, cookieDomain, false); + STORAGE.setDataInLocalStorage(EUIDS_STORAGE_NAME, sddanId, null); + return true; +} + +/** + * Merges and cleans objects from two eids arrays based on the 'source' and unique 'id' within the 'uids' array. + * Processes each array to add unique items or merge uids if the source already exists. + * @param {Array} euids1 - The first array to process and merge. + * @param {Array} euids2 - The second array to process and merge. + * @returns {Array} The merged array with unique sources and uid ids. + */ +export function mergeEuidsArrays(euids1, euids2) { + if (isEmpty(euids1)) return euids2; + if (isEmpty(euids2)) return euids1; + const mergedArray = []; + // Helper function to process each array + const processArray = (array) => { + array.forEach(item => { + if (item.uids) { + const foundIndex = findIndex(mergedArray, function (x) { + return x.source === item.source; + }); + if (foundIndex !== -1) { + // Merge uids if the source is found + item.uids.forEach(uid => { + if (!mergedArray[foundIndex].uids.some(u => u.id === uid.id)) { + mergedArray[foundIndex].uids.push(uid); + } + }); + } else { + // Add the entire item if the source does not exist + mergedArray.push({ ...item, uids: [...item.uids] }); + } + } + }); + }; + // Process both euids1 and euids2 + processArray(euids1); + processArray(euids2); + return mergedArray; +} + +/** + * Handles data deletion request by removing stored EU IDs + * @param {Object} moduleConfig - The module configuration + * @returns {boolean} - True if data was deleted successfully + */ +export function onDataDeletionRequest(moduleConfig) { + if (moduleConfig && moduleConfig.params) { + setCookieOnTopDomain(EUIDS_STORAGE_NAME, '', window.location.hostname, true); + STORAGE.removeDataFromLocalStorage(EUIDS_STORAGE_NAME, null); } + return !getUidFromStorage(); +} - let actualUrl = moduleConfig.params.actualUrl || getRefererInfo().stack.pop() || getRefererInfo().page; +/** + * Sends the page content for semantic analysis to Sirdata's server. + * @param {string} postContentToken - The token required to post content. + * @param {string} actualUrl - The actual URL of the current page. + * @returns {boolean} - True if the content was sent successfully + */ +export function postContentForSemanticAnalysis(postContentToken, actualUrl) { + if (!postContentToken || !actualUrl) return false; - const url = 'https://kvt.' + sirdataDomain + '/api/v1/public/p/' + moduleConfig.params.partnerId + '/d/' + moduleConfig.params.key + '/s?callback=&gdpr=' + gdprApplies + '&gdpr_consent=' + tcString + (actualUrl ? '&url=' + encodeURIComponent(actualUrl) : ''); + try { + let content = document.implementation.createHTMLDocument(''); + // Clone the current document content to avoid altering the original page content + content.documentElement.innerHTML = document.documentElement.innerHTML; + // Sanitize the cloned content to remove unnecessary elements and PII + content = sanitizeContent(content); + // Serialize the sanitized content to a string + const payload = new XMLSerializer().serializeToString(content.documentElement); + + if (payload && payload.length > 300 && payload.length < 300000) { + const url = `https://contextual.sirdata.io/api/v1/push/contextual?post_content_token=${postContentToken}&url=${encodeURIComponent(actualUrl)}`; + + // Use the Beacon API if supported to send the payload + if ('sendBeacon' in navigator) { + navigator.sendBeacon(url, payload); + } else { + // Fallback to using AJAX if Beacon API is not supported + ajax(url, {}, payload, { + contentType: 'text/plain', + method: 'POST', + withCredentials: false, // No user-specific data is tied to the request + referrerPolicy: 'unsafe-url', + crossOrigin: true + }); + } + } + } catch (e) { + logError(LOG_PREFIX, e); + return false; + } + return true; +} + +/** + * Executes a callback function when the document is fully loaded. + * @param {function} callback - The function to execute when the document is ready. + */ +export function onDocumentReady(callback) { + if (typeof callback !== 'function') return false; + try { + if (document.readyState && document.readyState !== 'loading') { + callback(); + } else if (typeof document.addEventListener === 'function') { + document.addEventListener('DOMContentLoaded', callback); + } + } catch (e) { + callback(); + } + return true; +} + +/** + * Removes Personally Identifiable Information (PII) from the content + * @param {string} content - The content to be sanitized + * @returns {string} - The sanitized content + */ +export function removePII(content) { + const patterns = [ + /\b(?:\d{4}[ -]?){3}\d{4}\b/g, // Credit card numbers + /\b\d{10,12}\b/g, // US bank account numbers + /\b\d{5}\d{5}\d{11}\d{2}\b/g, // EU bank account numbers + /\b(\d{3}-\d{2}-\d{4}|\d{9}|\d{13}|\d{2} \d{2} \d{2} \d{3} \d{3} \d{3})\b/g, // SSN + /\b[A-Z]{1,2}\d{6,9}\b/g, // Passport numbers + /\b(\d{8,10}|\d{3}-\d{3}-\d{3}-\d{3}|\d{2} \d{2} \d{2} \d{3} \d{3})\b/g, // ID card numbers + /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, // Email addresses + /(\+?\d{1,3}[-.\s]?)?(\(?\d{2,3}\)?[-.\s]?)(\d{2}[-.\s]?){3,4}\d{2}/g // Phone numbers + ]; + patterns.forEach(pattern => { + content = content.replace(pattern, ''); + }); + return content; +} - ajax(url, - { +/** + * Sanitizes the content by removing unnecessary elements and PII + * @param {Object} content - The content to be sanitized + * @returns {Object} - The sanitized content + */ +export function sanitizeContent(content) { + if (content && content.documentElement.innerText && content.documentElement.innerText.length > 500) { + // Reduce size by removing useless content + // Allowed tags + const allowedTags = [ + 'div', 'span', 'a', 'article', 'section', 'p', 'h1', 'h2', 'body', 'b', 'u', 'i', 'big', 'mark', 'ol', 'small', 'strong', 'blockquote', + 'nav', 'menu', 'li', 'ul', 'ins', 'head', 'title', 'main', 'var', 'table', 'caption', 'colgroup', 'col', 'tr', 'td', 'th', + 'summary', 'details', 'dl', 'dt', 'dd' + ]; + + const processElement = (element) => { + Array.from(element.childNodes).reverse().forEach(child => { + if (child.nodeType === Node.ELEMENT_NODE) { + processElement(child); + Array.from(child.attributes).forEach(attr => { + // keeps only id attributes and class attribute if useful for contextualisation + if (attr.name === 'class' && !/^(main|article|product)/.test(attr.value)) { + child.removeAttribute(attr.name); + } else if (attr.name !== 'id') { + child.removeAttribute(attr.name); + } + }); + if (!child.innerHTML.trim() || !allowedTags.includes(child.tagName.toLowerCase())) { // Keeps only allowed Tags (allowedTags) + child.remove(); + } + } + }); + }; + + const removeEmpty = (element) => { // remove empty tags + Array.from(element.childNodes).reverse().forEach(child => { + if (child.nodeType === Node.ELEMENT_NODE) { + removeEmpty(child); + if (!child.innerHTML.trim()) { + child.remove(); + } + } else if (child.nodeType === Node.TEXT_NODE && !child.textContent.trim()) { + child.remove(); + } + }); + }; + + processElement(content.documentElement); + removeEmpty(content.documentElement); + + // Clean any potential PII + content.documentElement.innerHTML = removePII(content.documentElement.innerHTML); + + let htmlContent = content.documentElement.innerHTML; + // Remove HTML comments + // This regex removes HTML comments, including those that might not be properly closed + htmlContent = htmlContent.replace(/|$)/g, ''); + // Remove multiple spaces + htmlContent = htmlContent.replace(/\s+/g, ' '); + // Remove spaces between tags + htmlContent = htmlContent.replace(/>\s+<'); + // Assign the cleaned content + content.documentElement.innerHTML = htmlContent; + } + return content; +} + +/** + * Fetches segments and categories from Sirdata server and processes the response + * @param {Object} reqBidsConfigObj - The bids configuration object + * @param {function} onDone - The callback function to be called upon completion + * @param {Object} moduleConfig - The module Config + * @param {Object} userConsent - The user consent information + */ +export function getSegmentsAndCategories(reqBidsConfigObj, onDone, moduleConfig, userConsent) { + logInfo(LOG_PREFIX, 'get Segments And Categories'); + const adUnits = (reqBidsConfigObj && reqBidsConfigObj.adUnits) || getGlobal().adUnits; + if (!adUnits) { + logInfo(LOG_PREFIX, 'no ad unit, RTD processing is useless'); + onDone(); + return; + } + + const gdprApplies = deepAccess(userConsent, 'gdpr.gdprApplies') ? userConsent.gdpr.gdprApplies : false; + const sirdataSubDomain = params.preprod ? 'kvt-preprod' : 'kvt'; + + let euids; // empty if no right to access device (publisher or user reject) + let privacySignals = ''; + + // Default global endpoint is cookie-based only if no rules falls into cookieless or consent has been given or GDPR doesn't apply + if (hasDeviceAccess() && !userConsent.coppa && (isEmpty(userConsent.usp) || userConsent.usp === -1 || (userConsent.usp[0] === '1' && (userConsent.usp[1] !== 'N' && userConsent.usp[2] !== 'Y'))) && (!gdprApplies || (deepAccess(userConsent, 'gdpr.vendorData.vendor.consents') && userConsent.gdpr.vendorData.vendor.consents[GVLID] && deepAccess(userConsent, 'gdpr.vendorData.purpose.consents') && userConsent.gdpr.vendorData.purpose.consents[1] && (userConsent.gdpr.vendorData.purpose.consents[2] || userConsent.gdpr.vendorData.purpose.consents[3]) && userConsent.gdpr.vendorData.purpose.consents[4])) && (isEmpty(userConsent.gpp) || userConsent.gpp.gppString) && checkCookieSupport()) { + params.sirdataDomain = 'sddan.com'; // cookie based domain + params.cookieAccessGranted = true; // cookies sent in request + + if (gdprApplies && deepAccess(userConsent, 'gdpr.consentString')) { + privacySignals = `&gdpr=${gdprApplies}&gdpr_consent=${userConsent.gdpr.consentString}`; + } else if (!isEmpty(userConsent.usp)) { + privacySignals = `&ccpa_consent=${userConsent.usp.toString()}`; + } else if (deepAccess(userConsent, 'gpp.gppString')) { + const sid = deepAccess(userConsent, 'gpp.applicableSections') ? `&gpp_sid=${userConsent.gpp.applicableSections.join(',')}` : ''; + privacySignals = `&gpp=${userConsent.gpp.gppString}${sid}`; + } + + // Authorized EUIDS from storage and sync global for graph + euids = getUidFromStorage(); // Sirdata Id + + if (!isEmpty(params.authorizedEids) && typeof getGlobal().getUserIds === 'function') { + let filteredEids = {}; + const authorizedEids = params.authorizedEids; + const globalUserIds = getGlobal().getUserIds(); + const globalUserIdsAsEids = getGlobal().getUserIdsAsEids(); + + const hasPubProvidedId = authorizedEids.indexOf('pubProvidedId') !== -1; + + if (hasPubProvidedId && !isEmpty(globalUserIds.pubProvidedId)) { // Publisher allows pubProvidedId + filteredEids = mergeEuidsArrays(filteredEids, globalUserIds.pubProvidedId); + } + + if (!hasPubProvidedId || authorizedEids.length > 1) { // Publisher allows other Id providers + const filteredGlobalEids = globalUserIdsAsEids.filter(entry => authorizedEids.includes(entry.source)); + if (!isEmpty(filteredGlobalEids)) { + filteredEids = mergeEuidsArrays(filteredEids, filteredGlobalEids); + } + } + + if (!isEmpty(filteredEids)) { + euids = mergeEuidsArrays(euids, filteredEids); // merge ids for graph id + } + } + } + + const url = `https://${sirdataSubDomain}.${params.sirdataDomain}/api/v1/public/p/${params.partnerId.toString()}/d/${params.key.toString()}/s?callback=&allowed_post_content=${!params.avoidPostContent}${privacySignals}${params.actualUrl ? `&url=${encodeURIComponent(params.actualUrl)}` : ''}`; + const method = isEmpty(euids) ? 'GET' : 'POST'; + const payload = isEmpty(euids) ? null : JSON.stringify({ external_ids: euids }); + + try { + ajax(url, { success: function (response, req) { if (req.status === 200) { try { const data = JSON.parse(response); if (data && data.segments) { - addSegmentData(reqBidsConfigObj, data, moduleConfig, onDone); + addSegmentData(reqBidsConfigObj, data, adUnits, onDone); } else { onDone(); } } catch (e) { onDone(); - logError('unable to parse Sirdata data' + e); + logError(LOG_PREFIX, 'unable to parse Sirdata data' + e); } } else if (req.status === 204) { onDone(); @@ -123,104 +434,136 @@ export function getSegmentsAndCategories(reqBidsConfigObj, onDone, moduleConfig, }, error: function () { onDone(); - logError('unable to get Sirdata data'); + logError(LOG_PREFIX, 'unable to get Sirdata data'); } - }, - null, - { + }, payload, { contentType: 'text/plain', - method: 'GET', - withCredentials: sendWithCredentials, + method: method, + withCredentials: params.cookieAccessGranted, referrerPolicy: 'unsafe-url', crossOrigin: true }); + } catch (e) { + logError(LOG_PREFIX, e); + } } +/** + * Pushes data to OpenRTB 2.5 fragments for the specified bidder + * @param {Object} ortb2Fragments - The OpenRTB 2.5 fragments + * @param {string} bidder - The bidder name + * @param {Object} data - The data to be pushed + * @param {number} segtaxid - The segment taxonomy ID + * @param {number} cattaxid - The category taxonomy ID + * @returns {boolean} - True if data was pushed successfully + */ export function pushToOrtb2(ortb2Fragments, bidder, data, segtaxid, cattaxid) { try { if (!isEmpty(data.segments)) { if (segtaxid) { setOrtb2Sda(ortb2Fragments, bidder, 'user', data.segments, segtaxid); } else { - setOrtb2(ortb2Fragments, bidder, 'user.ext.data', {sd_rtd: {segments: data.segments}}); + setOrtb2(ortb2Fragments, bidder, 'user.ext.data', { sd_rtd: { segments: data.segments } }); } } if (!isEmpty(data.categories)) { if (cattaxid) { setOrtb2Sda(ortb2Fragments, bidder, 'site', data.categories, cattaxid); } else { - setOrtb2(ortb2Fragments, bidder, 'site.ext.data', {sd_rtd: {categories: data.categories}}); + setOrtb2(ortb2Fragments, bidder, 'site.ext.data', { sd_rtd: { categories: data.categories } }); } } if (!isEmpty(data.categories_score) && !cattaxid) { - setOrtb2(ortb2Fragments, bidder, 'site.ext.data', {sd_rtd: {categories_score: data.categories_score}}); + setOrtb2(ortb2Fragments, bidder, 'site.ext.data', { sd_rtd: { categories_score: data.categories_score } }); } } catch (e) { - logError(e) + logError(LOG_PREFIX, e); } return true; } +/** + * Sets OpenRTB 2.5 Seller Defined Audiences (SDA) data + * @param {Object} ortb2Fragments - The OpenRTB 2.5 fragments + * @param {string} bidder - The bidder name + * @param {string} type - The type of data ('user' or 'site') + * @param {Array} segments - The segments to be set + * @param {number} segtaxValue - The segment taxonomy value + * @returns {boolean} - True if data was set successfully + */ export function setOrtb2Sda(ortb2Fragments, bidder, type, segments, segtaxValue) { try { - let ortb2Data = [{ - name: ORTB2_NAME, - segment: segments.map((segmentId) => ({ id: segmentId })), - }]; - if (segtaxValue) { - ortb2Data[0].ext = { segtax: segtaxValue }; - } - let ortb2Conf = (type === 'site' ? {site: {content: {data: ortb2Data}}} : {user: {data: ortb2Data}}); - if (bidder) { - ortb2Conf = {[bidder]: ortb2Conf}; - } + let ortb2Data = [{ name: ORTB2_NAME, segment: segments.map(segmentId => ({ id: segmentId })) }]; + if (segtaxValue) ortb2Data[0].ext = { segtax: segtaxValue }; + let ortb2Conf = (type === 'site') ? { site: { content: { data: ortb2Data } } } : { user: { data: ortb2Data } }; + if (bidder) ortb2Conf = { [bidder]: ortb2Conf }; mergeDeep(ortb2Fragments, ortb2Conf); } catch (e) { - logError(e) + logError(LOG_PREFIX, e); } return true; } +/** + * Sets OpenRTB 2.5 data at the specified path + * @param {Object} ortb2Fragments - The OpenRTB 2.5 fragments + * @param {string} bidder - The bidder name + * @param {string} path - The path to set the data at + * @param {Object} segments - The segments to be set + * @returns {boolean} - True if data was set successfully + */ export function setOrtb2(ortb2Fragments, bidder, path, segments) { try { - if (isEmpty(segments)) { return false; } + if (isEmpty(segments)) return false; let ortb2Conf = {}; - deepSetValue(ortb2Conf, path, segments || {}); - if (bidder) { - ortb2Conf = {[bidder]: ortb2Conf}; - } + deepSetValue(ortb2Conf, path, segments); + if (bidder) ortb2Conf = { [bidder]: ortb2Conf }; mergeDeep(ortb2Fragments, ortb2Conf); } catch (e) { - logError(e) + logError(LOG_PREFIX, e); } - return true; } +/** + * Loads a custom function for processing ad unit data + * @param {function} todo - The custom function to be executed + * @param {Object} adUnit - The ad unit object + * @param {Object} list - The list of data + * @param {Object} data - The data object + * @param {Object} bid - The bid object + * @returns {boolean} - True if the function was executed successfully + */ export function loadCustomFunction(todo, adUnit, list, data, bid) { try { - if (typeof todo == 'function') { - todo(adUnit, list, data, bid); - } + if (typeof todo === 'function') todo(adUnit, list, data, bid); } catch (e) { - logError(e); + logError(LOG_PREFIX, e); } return true; } +/** + * Gets segments and categories array from the data object + * @param {Object} data - The data object + * @param {number} minScore - The minimum score threshold for contextual relevancy + * @param {string} pid - The partner ID (attributed by Sirdata to bidder) + * @returns {Object} - The segments and categories data + */ export function getSegAndCatsArray(data, minScore, pid) { - let sirdataData = {'segments': [], 'categories': [], 'categories_score': {}}; - minScore = minScore && typeof minScore == 'number' ? minScore : 30; - let cattaxid = data.cattaxid || null; - let segtaxid = data.segtaxid || null; + let sirdataData = { segments: [], categories: [], categories_score: {} }; + minScore = typeof minScore === 'number' ? minScore : 30; + const { cattaxid, segtaxid, segments } = data; + const contextualCategories = data.contextual_categories || {}; + // parses contextual categories try { - if (data && data.contextual_categories) { - for (let catId in data.contextual_categories) { - if (data.contextual_categories.hasOwnProperty(catId) && data.contextual_categories[catId]) { - let value = data.contextual_categories[catId]; - if (value >= minScore && sirdataData.categories.indexOf(catId) === -1) { - if (pid && pid === '27440' && cattaxid) { // Equativ only - sirdataData.categories.push(pid.toString() + 'cc' + catId.toString()); + if (contextualCategories) { + for (let catId in contextualCategories) { + if (contextualCategories.hasOwnProperty(catId) && contextualCategories[catId]) { + let value = contextualCategories[catId]; + if (value >= minScore && !sirdataData.categories.includes(catId)) { + if (pid === '27440' && cattaxid) { // Equativ only + sirdataData.categories.push(`${pid}cc${catId}`); } else { sirdataData.categories.push(catId.toString()); sirdataData.categories_score[catId] = value; @@ -230,15 +573,16 @@ export function getSegAndCatsArray(data, minScore, pid) { } } } catch (e) { - logError(e); + logError(LOG_PREFIX, e); } + // parses user-centric segments (empty if no right to access device/process PII) try { - if (data && data.segments) { - for (let segId in data.segments) { - if (data.segments.hasOwnProperty(segId) && data.segments[segId]) { - let id = data.segments[segId].toString(); - if (pid && pid === '27440' && segtaxid) { // Equativ only - sirdataData.segments.push(pid.toString() + 'us' + id); + if (segments) { + for (let segId in segments) { + if (segments.hasOwnProperty(segId) && segments[segId]) { + let id = segments[segId].toString(); + if (pid === '27440' && segtaxid) { // Equativ only + sirdataData.segments.push(`${pid}us${id}`); } else { sirdataData.segments.push(id); } @@ -246,150 +590,177 @@ export function getSegAndCatsArray(data, minScore, pid) { } } } catch (e) { - logError(e); + logError(LOG_PREFIX, e); } return sirdataData; } -export function applySdaGetSpecificData(data, sirdataData, biddersParamsExist, minScore, reqBids, bid, moduleConfig, indexFound, bidderIndex, adUnit) { - // only share SDA data if whitelisted - if (!biddersParamsExist || indexFound) { +/** + * Applies Seller Defined Audience (SDA) data and specific data for the bidder + * @param {Object} data - The data object + * @param {Object} sirdataData - The Sirdata data object + * @param {boolean} biddersParamsExist - Flag indicating if bidder parameters exist + * @param {Object} reqBids - The request bids object + * @param {Object} bid - The bid object + * @param {number} bidderIndex - The bidder index + * @param {Object} adUnit - The ad unit object + * @param {string} aliasActualBidder - The bidder Alias + * @returns {Object} - The modified Sirdata data + */ +export function applySdaGetSpecificData(data, sirdataData, biddersParamsExist, reqBids, bid, bidderIndex, adUnit, aliasActualBidder) { + // Apply custom function or return Bidder Specific Data if publisher is ok + if (bidderIndex && params.bidders[bidderIndex]?.customFunction && typeof (params.bidders[bidderIndex]?.customFunction) === 'function') { + return loadCustomFunction(params.bidders[bidderIndex].customFunction, adUnit, sirdataData, data, bid); + } + + // Only share Publisher SDA data if whitelisted + if (!biddersParamsExist || bidderIndex) { // SDA Publisher - let sirdataDataForSDA = getSegAndCatsArray(data, minScore, moduleConfig.params.partnerId.toString()); + let sirdataDataForSDA = getSegAndCatsArray(data, params.contextualMinRelevancyScore, params.partnerId.toString()); pushToOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, sirdataDataForSDA, data.segtaxid, data.cattaxid); } - // always share SDA for curation - let curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : (partnerIds[bid.bidder] ? partnerIds[bid.bidder] : null)); - if (curationId) { - // seller defined audience & bidder specific data - if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { - // Get Bidder Specific Data - let curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore, curationId.toString()); - pushToOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, curationData, data.shared_taxonomy[curationId].segtaxid, data.shared_taxonomy[curationId].cattaxid); + // Always share SDA for curation + if (!isEmpty(data.shared_taxonomy)) { + let curationId = (bidderIndex && params.bidders[bidderIndex]?.curationId) || biddersId[aliasActualBidder]; + if (curationId && data.shared_taxonomy[curationId]) { + // Seller defined audience & bidder specific data + let curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], params.contextualMinRelevancyScore, curationId.toString()); + if (!isEmpty(curationData)) { + pushToOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, curationData, data.shared_taxonomy[curationId].segtaxid, data.shared_taxonomy[curationId].cattaxid); + mergeDeep(sirdataData, curationData); + } } } - // Apply custom function or return Bidder Specific Data if publisher is ok - if (!biddersParamsExist || indexFound) { - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - return loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataData, data, bid); - } else { - return sirdataData; - } - } + return sirdataData; } -export function addSegmentData(reqBids, data, moduleConfig, onDone) { - const adUnits = (reqBids && reqBids.adUnits) || getGlobal().adUnits; - if (!adUnits) { - onDone(); - return; - } - - moduleConfig = moduleConfig || {}; - moduleConfig.params = moduleConfig.params || {}; - const globalMinScore = moduleConfig.params.hasOwnProperty('contextualMinRelevancyScore') ? moduleConfig.params.contextualMinRelevancyScore : 30; - let sirdataData = getSegAndCatsArray(data, globalMinScore, null); +/** + * Adds segment data to the request bids object and processes the data + * @param {Object} reqBids - The request bids object + * @param {Object} data - The data object + * @param {Array} adUnits - The ad units array + * @param {function} onDone - The callback function to be called upon completion + * @returns {Array} - The ad units array + */ +export function addSegmentData(reqBids, data, adUnits, onDone) { + logInfo(LOG_PREFIX, 'Dispatch Segments And Categories'); + const minScore = params.contextualMinRelevancyScore || 30; + let sirdataData = getSegAndCatsArray(data, minScore, ''); - const biddersParamsExist = (!!(moduleConfig.params && moduleConfig.params.bidders && moduleConfig.params.bidders.length > 0)); + const biddersParamsExist = params.bidders.length > 0; // Global ortb2 SDA - if (data.global_taxonomy && !isEmpty(data.global_taxonomy)) { - let globalData = {'segments': [], 'categories': [], 'categories_score': []}; + if (!isEmpty(data.global_taxonomy)) { for (let i in data.global_taxonomy) { + let globalData; if (!isEmpty(data.global_taxonomy[i])) { - globalData = getSegAndCatsArray(data.global_taxonomy[i], globalMinScore, null); - pushToOrtb2(reqBids.ortb2Fragments?.global, null, globalData, data.global_taxonomy[i].segtaxid, data.global_taxonomy[i].cattaxid); + globalData = getSegAndCatsArray(data.global_taxonomy[i], params.contextualMinRelevancyScore, ''); + if (!isEmpty(globalData)) { + pushToOrtb2(reqBids.ortb2Fragments?.global, '', globalData, data.global_taxonomy[i].segtaxid, data.global_taxonomy[i].cattaxid); + } } } } // Google targeting - if (typeof window.googletag !== 'undefined' && (moduleConfig.params.setGptKeyValues || !moduleConfig.params.hasOwnProperty('setGptKeyValues'))) { + if (typeof window.googletag !== 'undefined' && params.setGptKeyValues) { try { - let gptCurationId = (moduleConfig.params.gptCurationId ? moduleConfig.params.gptCurationId : (partnerIds['sdRtdForGpt'] ? partnerIds['sdRtdForGpt'] : null)); - let sirdataMergedList = []; - if (gptCurationId && data.shared_taxonomy && data.shared_taxonomy[gptCurationId]) { - let gamCurationData = getSegAndCatsArray(data.shared_taxonomy[gptCurationId], globalMinScore, null); - sirdataMergedList = sirdataMergedList.concat(gamCurationData.segments).concat(gamCurationData.categories); - } else { - sirdataMergedList = sirdataData.segments.concat(sirdataData.categories); + const gptCurationId = params.gptCurationId || biddersId.sdRtdForGpt; + let sirdataMergedList = [...sirdataData.segments, ...sirdataData.categories]; + + if (gptCurationId && data.shared_taxonomy?.[gptCurationId]) { + const gamCurationData = getSegAndCatsArray(data.shared_taxonomy[gptCurationId], params.contextualMinRelevancyScore, ''); + sirdataMergedList = [...sirdataMergedList, ...gamCurationData.segments, ...gamCurationData.categories]; } - window.googletag.cmd.push(function() { - window.googletag.pubads().getSlots().forEach(function (n) { - if (typeof n.setTargeting !== 'undefined' && sirdataMergedList && sirdataMergedList.length > 0) { - n.setTargeting('sd_rtd', sirdataMergedList); + + window.googletag.cmd.push(() => { + window.googletag.pubads().getSlots().forEach(slot => { + if (typeof slot.setTargeting !== 'undefined' && sirdataMergedList.length > 0) { + slot.setTargeting('sd_rtd', sirdataMergedList); } }); }); } catch (e) { - logError(e); + logError(LOG_PREFIX, e); } } - // Bid targeting level for FPD non-generic biders - let bidderIndex = ''; - let indexFound = false; - adUnits.forEach(adUnit => { - adUnit.hasOwnProperty('bids') && adUnit.bids.forEach(bid => { - bidderIndex = (moduleConfig.params.hasOwnProperty('bidders') ? findIndex(moduleConfig.params.bidders, function (i) { - return i.bidder === bid.bidder; - }) : false); - indexFound = (!!(typeof bidderIndex == 'number' && bidderIndex >= 0)); + return adUnit.bids?.forEach(bid => { + const bidderIndex = findIndex(params.bidders, function (i) { return i.bidder === bid.bidder; }); try { - let minScore = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('contextualMinRelevancyScore') ? moduleConfig.params.bidders[bidderIndex].contextualMinRelevancyScore : globalMinScore); - switch (bid.bidder) { - case 'appnexus': - case 'appnexusAst': - case 'brealtime': - case 'emetriq': - case 'emxdigital': - case 'pagescience': - case 'gourmetads': - case 'matomy': - case 'featureforward': - case 'oftmedia': - case 'districtm': - case 'adasta': - case 'beintoo': - case 'gravity': - case 'msq_classic': - case 'msq_max': - case '366_apx': - sirdataData = applySdaGetSpecificData(data, sirdataData, biddersParamsExist, minScore, reqBids, bid, moduleConfig, indexFound, bidderIndex, adUnit); - if (sirdataData.segments && sirdataData.segments.length > 0) { - setOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, 'user.keywords', 'sd_rtd=' + sirdataData.segments.join(',sd_rtd=')); - } - if (sirdataData.categories && sirdataData.categories.length > 0) { - setOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, 'site.content.keywords', 'sd_rtd=' + sirdataData.categories.join(',sd_rtd=')); - } - break; - - default: - if (!biddersParamsExist || (indexFound && (!moduleConfig.params.bidders[bidderIndex].hasOwnProperty('adUnitCodes') || moduleConfig.params.bidders[bidderIndex].adUnitCodes.indexOf(adUnit.code) !== -1))) { - applySdaGetSpecificData(data, sirdataData, biddersParamsExist, minScore, reqBids, bid, moduleConfig, indexFound, bidderIndex, adUnit); - } + const aliasActualBidder = bidderAliasRegistry[bid.bidder] || bid.bidder; + if (aliasActualBidder === 'appnexus') { + let xandrData = applySdaGetSpecificData(data, sirdataData, biddersParamsExist, reqBids, bid, bidderIndex, adUnit, aliasActualBidder); + // Surprisingly, to date Xandr doesn't support SDA, we need to set specific 'keywords' entries + if (xandrData.segments.length > 0) { + setOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, 'user.keywords', `sd_rtd=${xandrData.segments.join(',sd_rtd=')}`); + } + if (xandrData.categories.length > 0) { + setOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, 'site.content.keywords', `sd_rtd=${xandrData.categories.join(',sd_rtd=')}`); + } + } else { + applySdaGetSpecificData(data, sirdataData, biddersParamsExist, reqBids, bid, bidderIndex, adUnit, aliasActualBidder); } } catch (e) { - logError(e); + logError(LOG_PREFIX, e); } - }) + }); }); + + // Trigger onDone onDone(); + + // Postprocessing: should we send async content to categorize content and/or store Sirdata ID (stored in 3PC) within local scope ? + if (params.sirdataDomain === 'sddan.com') { // Means consent has been given for device and content access + if (!isEmpty(data.sddan_id) && params.cookieAccessGranted) { // Device access allowed by publisher and cookies are supported + setUidInStorage(data.sddan_id); // Save Sirdata user ID + } + if (!params.avoidPostContent && params.actualUrl && !inIframe() && !isEmpty(data.post_content_token)) { + onDocumentReady(() => postContentForSemanticAnalysis(data.post_content_token, params.actualUrl)); + } + } return adUnits; } -export function init(config) { - logInfo(LOG_PREFIX, config); +/** + * Initializes the module with the given configuration + * @param {Object} moduleConfig - The module configuration + * @returns {boolean} - True if the initialization was successful + */ +export function init(moduleConfig) { + logInfo(LOG_PREFIX, moduleConfig); + if (typeof (moduleConfig.params) !== 'object' || !moduleConfig.params.key) return false; + if (typeof (moduleConfig.params.authorizedEids) !== 'object' || !Array.isArray(moduleConfig.params.authorizedEids)) { + delete (moduleConfig.params.authorizedEids); // must be array of strings + } else { + // we need it if the publishers uses user Id module name instead of key or source + const resultSet = new Set( + moduleConfig.params.authorizedEids.map(item => { + const formattedItem = item.toLowerCase().replace(/\s+/g, '_'); // Normalize + return eidsProvidersMap[formattedItem] || formattedItem; + }) + ); + moduleConfig.params.authorizedEids = Array.from(resultSet); + } + if (typeof (moduleConfig.params.bidders) !== 'object' || !Array.isArray(moduleConfig.params.bidders)) delete (moduleConfig.params.bidders); // must be array of objects + delete (moduleConfig.params.sirdataDomain); // delete cookieless domain if specified => shouldn't be overridden by publisher + params = Object.assign({}, params, moduleConfig.params); return true; } +/** + * The Sirdata submodule definition + * @type {Object} + */ export const sirdataSubmodule = { name: SUBMODULE_NAME, + gvlid: GVLID, init: init, getBidRequestData: getSegmentsAndCategories }; +// Register the Sirdata submodule with the real time data module submodule(MODULE_NAME, sirdataSubmodule); diff --git a/modules/sirdataRtdProvider.md b/modules/sirdataRtdProvider.md index f67e34db43a..9e712e5fbd6 100644 --- a/modules/sirdataRtdProvider.md +++ b/modules/sirdataRtdProvider.md @@ -1,30 +1,28 @@ -# Sirdata Real-Time Data Submodule +# Sirdata RTD/SDA Module -Module Name: Sirdata Rtd Provider +Module Name: Sirdata Real-time SDA Module Module Type: Rtd Provider Maintainer: bob@sirdata.com # Description -Sirdata provides a disruptive API that allows its partners to leverage its -cutting-edge contextualization technology and its audience segments based on -cookies and consent or without cookies nor consent! +Sirdata provides a disruptive API that allows publishers and SDA compliant SSPs/DSPs to leverage its cutting-edge contextualization technology and its audience segments! -User-based segments and page-level automatic contextual categories will be -attached to bid request objects sent to different SSPs in order to optimize -targeting. +User-based segments and page-level automatic contextual categories will be attached to bid request objects sent to different bidders in order to optimize targeting. -Automatic integration with Google Ad Manager and major bidders like Xandr/Appnexus, -Smartadserver, Index Exchange, Proxistore, Magnite/Rubicon or Triplelift ! +Automatic or custom integration with Google Ad Manager and major bidders like Xandr's, Equativ's, Index Exchange's, Proxistore's, Magnite's or Triplelift's ! -User's country and choice management are included in the module, so it's 100% -compliant with local and regional laws like GDPR and CCPA/CPRA. +User's country and choice management are included in the module, so it's 100% compliant with local and regional laws like GDPR and CCPA/CPRA. ORTB2 compliant and FPD support for Prebid versions < 4.29 -Contact bob@sirdata.com for information. +Fully supports Seller Defined Audience ! Please find the full SDA taxonomy ids list here. -### Publisher Usage +Please contact for more information. + +## Publisher Usage + +### Configure Prebid.js Compile the Sirdata RTD module into your Prebid build: @@ -32,15 +30,39 @@ Compile the Sirdata RTD module into your Prebid build: Add the Sirdata RTD provider to your Prebid config. -Segments ids (user-centric) and category ids (page-centric) will be provided -salted and hashed : you can use them with a dedicated and private matching table. -Should you want to allow a SSP or a partner to curate your media and operate -cross-publishers campaigns with our data, please ask Sirdata (bob@sirdata.com) to -open it for you account. +`actualUrl` MUST be set with actual location of parent page if prebid.js is loaded in an iframe (e.g. hosted). It can be left blank ('') or removed otherwise. + +`partnerId` and `key` should be provided by your partnering SSP or get one and your dedicated taxonomy from Sirdata (). Segments ids (user-centric) and category ids (page-centric) will be provided salted and hashed : you can use them with a dedicated and private matching table. + +Should you want to allow any SSP or a partner to curate your media and operate cross-publishers campaigns with our data, please ask Sirdata () to whitelist him it in your account. + +#### Typical configuration +```javascript +pbjs.setConfig({ + // ... + realTimeData: { + auctionDelay: 1000, + dataProviders: [ + { + name: "SirdataRTDModule", + waitForIt: true, + params: { + partnerId: 1, + key: 1, + } + } + ] + } + // ... +}); ``` -pbjs.setConfig( - ... + +#### Advanced configuration + +```javascript +pbjs.setConfig({ + // ... realTimeData: { auctionDelay: 1000, dataProviders: [ @@ -48,85 +70,114 @@ pbjs.setConfig( name: "SirdataRTDModule", waitForIt: true, params: { - partnerId: 1, + partnerId: 1, key: 1, - setGptKeyValues: true, - contextualMinRelevancyScore: 50, //Min score to filter contextual category globally (0-100 scale) - actualUrl: actual_url, //top location url, for contextual categories - bidders: [{ - bidder: 'appnexus', - adUnitCodes: ['adUnit-1','adUnit-2'], - customFunction: overrideAppnexus, - curationId: '111', - },{ - bidder: 'ix', - sizeLimit: 1200 //specific to Index Exchange, - contextualMinRelevancyScore: 50, //Min score to filter contextual category for curation in the bidder (0-100 scale) - }] + setGptKeyValues: true, + contextualMinRelevancyScore: 50, //Min score to filter contextual category globally (0-100 scale) + actualUrl: '' //top location url, for contextual categories } } ] } - ... -} + // ... +}); ``` ### Parameter Descriptions for the Sirdata Configuration Section -| Name |Type | Description | Notes | -| :------------ | :------------ | :------------ |:------------ | -| name | String | Real time data module name | Mandatory. Always 'SirdataRTDModule' | -| waitForIt | Boolean | Mandatory. Required to ensure that the auction is delayed until prefetch is complete | Optional. Defaults to false but recommended to true | -| params | Object | | Optional | -| params.partnerId | Integer | Partner ID, required to get results and provided by Sirdata. Use 1 for tests and get one running at bob@sirdata.com | Mandatory. Defaults 1. | -| params.key | Integer | Key linked to Partner ID, required to get results and provided by Sirdata. Use 1 for tests and get one running at bob@sirdata.com | Mandatory. Defaults 1. | -| params.setGptKeyValues | Boolean | This parameter Sirdata to set Targeting for GPT/GAM | Optional. Defaults to true. | -| params.contextualMinRelevancyScore | Integer | Min score to keep filter category in the bidders (0-100 scale). Optional. Defaults to 30. | -| params.bidders | Object | Dictionary of bidders you would like to supply Sirdata data for. | Optional. In case no bidder is specified Sirdata will atend to ad data custom and ortb2 to all bidders, adUnits & Globalconfig | +| Name | Type | Description | Notes | +|:-----------------------------------|:--------------------------|:----------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------| +| name | String | Real time data module name | Mandatory. Always 'SirdataRTDModule' | +| waitForIt | Boolean | Required to ensure that the auction is delayed until prefetch is complete | Optional. Default to false but recommended to true | +| params | Object | Settings | Optional | +| params.partnerId | Integer | Partner ID, required to get results and provided by Sirdata. Use 1 for tests and request your Id at | Mandatory. Default 1 | +| params.key | Integer | Key linked to Partner ID, required to get results and provided by Sirdata. Use 1 for tests and request your key at | Mandatory. Default 1 | +| params.setGptKeyValues | Boolean | Sets Targeting for GPT/GAM | Optional. Default to true | +| params.authorizedEids | Array of String | List of authorised Eids for graph. Set [] to prevent all Eids usage | Optional. Default to : ID5, pubProvidedId and sharedId | +| params.avoidPostContent | Boolean | Block contextual data POST from user's device (a crawler is use instead) | Optional. Default to false, and setting it to true results in your content downloaded by Sirdata crawler | +| params.contextualMinRelevancyScore | Integer | Min relevancy score to filter categories sent to the bidders (0-100 scale). | Optional. Defaults to 30. | +| params.bidders | Array of Object | Bidders you want to supply your own data to (works only with your private data bought to Sirdata) | Optional | Bidders can receive common setting : -| Name |Type | Description | Notes | -| :------------ | :------------ | :------------ |:------------ | -| bidder | String | Bidder name | Mandatory if params.bidders are specified | -| adUnitCodes | Array of String | Use if you want to limit data injection to specified adUnits for the bidder | Optional. Default is false and data shared with the bidder isn't filtered | -| customFunction | Function | Use it to override the way data is shared with a bidder | Optional. Default is false | -| curationId | String | Specify the curation ID of the bidder. Provided by Sirdata, request it at bob@sirdata.com | Optional. Default curation ids are specified for main bidders | -| contextualMinRelevancyScore | Integer | Min score to filter contextual categories for curation in the bidder (0-100 scale). Optional. Defaults to 30 or global params.contextualMinRelevancyScore if exits. | -| sizeLimit | Integer | used only for bidder 'ix' to limit the size of the get parameter in Index Exchange ad call | Optional. Default is 1000 | +| Name | Type | Description | Notes | +|:---------------|:----------------|:-----------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------| +| bidder | String | Bidder name | Mandatory if params.bidders are specified | +| adUnitCodes | Array of String | Use if you want to limit data injection to specified adUnits for the bidder | Optional. Default is false and data shared with the bidder isn't filtered | +| customFunction | Function | Use it to override the way data is shared with a bidder | Optional. Default is false | +| curationId | String | Specify the curation ID of the bidder. Provided by Sirdata, request it at | Optional. Default curation ids are specified for main bidders | ### Overriding data sharing function -As indicated above, it is possible to provide your own bid augmentation -functions. This is useful if you know a bid adapter's API supports segment -fields which aren't specifically being added to request objects in the Prebid -bid adapter. -Please see the following example, which provides a function to modify bids for -a bid adapter called ix and overrides the appnexus. +As indicated above, it is possible to provide your own bid augmentation functions. This is useful if you know a bid adapter's API supports segment fields which aren't specifically being added to request objects in the Prebid bid adapter. +Please see the following example, which provides a function to modify bids for a bid adapter called ix and overrides the appnexus. data Object format for usage in this kind of function : + +```json { - "segments":[111111,222222], - "contextual_categories":{"333333":100}, - "shared_taxonomy":{ - "27446":{ //CurationId - "segments":[444444,555555], - "contextual_categories":{"666666":100} - } - } + "segments": [ + 111111, + 222222 + ], + "segtaxid": null, + "cattaxid": null, + "contextual_categories": { + "333333": 100 + }, + "shared_taxonomy": { + "27440": { + "segments": [ + 444444, + 555555 + ], + "segtaxid": 552, + "cattaxid": 553, + "contextual_categories": { + "666666": 100 + } + } + }, + "global_taxonomy": { + "9998": { + "segments": [ + 123, + 234 + ], + "segtaxid": 4, + "cattaxid": 7, + "contextual_categories": { + "345": 100, + "456": 100 + } + }, + "9999": { + "segments": [ + 12345, + 23456 + ], + "segtaxid": 550, + "cattaxid": 551, + "contextual_categories": { + "34567": 100, + "45678": 100 + } + } + } } - ``` + +```javascript function overrideAppnexus (adUnit, segmentsArray, dataObject, bid) { - for (var i = 0; i < segmentsArray.length; i++) { + for (var i = 0; i < segmentsArray.length; i++) { if (segmentsArray[i]) { bid.params.user.segments.push(segmentsArray[i]); } } } -pbjs.setConfig( - ... +pbjs.setConfig({ + // ... realTimeData: { auctionDelay: 1000, dataProviders: [ @@ -134,18 +185,17 @@ pbjs.setConfig( name: "SirdataRTDModule", waitForIt: true, params: { - partnerId: 1, + partnerId: 1, key: 1, - setGptKeyValues: true, - contextualMinRelevancyScore: 50, //Min score to keep contextual category in the bidders (0-100 scale) - actualUrl: actual_url, //top location url, for contextual categories + setGptKeyValues: true, + contextualMinRelevancyScore: 50, //Min score to keep contextual category in the bidders (0-100 scale) + actualUrl: actual_url, //top location url, for contextual categories bidders: [{ bidder: 'appnexus', customFunction: overrideAppnexus, curationId: '111' },{ bidder: 'ix', - sizeLimit: 1200, //specific to Index Exchange customFunction: function(adUnit, segmentsArray, dataObject, bid) { bid.params.contextual.push(dataObject.contextual_categories); }, @@ -153,17 +203,19 @@ pbjs.setConfig( } } ] - } + }, ... -} +}); ``` ### Testing To view an example of available segments returned by Sirdata's backends: -`gulp serve --modules=rtdModule,sirdataRtdProvider,appnexusBidAdapter` +```bash +gulp serve --modules=rtdModule,sirdataRtdProvider,appnexusBidAdapter +``` and then point your browser at: -`http://localhost:9999/integrationExamples/gpt/sirdataRtdProvider_example.html` \ No newline at end of file +[http://localhost:9999/integrationExamples/gpt/sirdataRtdProvider_example.html] diff --git a/test/spec/modules/sirdataRtdProvider_spec.js b/test/spec/modules/sirdataRtdProvider_spec.js index fbb5967bc20..9f6bb30e0b0 100644 --- a/test/spec/modules/sirdataRtdProvider_spec.js +++ b/test/spec/modules/sirdataRtdProvider_spec.js @@ -1,21 +1,123 @@ -import {addSegmentData, getSegmentsAndCategories, sirdataSubmodule, setOrtb2} from 'modules/sirdataRtdProvider.js'; +import { + addSegmentData, + getSegmentsAndCategories, + getUidFromStorage, + loadCustomFunction, + mergeEuidsArrays, + onDataDeletionRequest, + onDocumentReady, + postContentForSemanticAnalysis, + removePII, + sanitizeContent, + setOrtb2, + setUidInStorage, + sirdataSubmodule +} from 'modules/sirdataRtdProvider.js'; +import {expect} from 'chai'; +import {deepSetValue} from 'src/utils.js'; import {server} from 'test/mocks/xhr.js'; const responseHeader = {'Content-Type': 'application/json'}; describe('sirdataRtdProvider', function () { - describe('sirdataSubmodule', function () { + describe('sirdata Submodule init', function () { it('exists', function () { expect(sirdataSubmodule.init).to.be.a('function'); }); it('successfully instantiates', function () { - expect(sirdataSubmodule.init()).to.equal(true); + const moduleConfig = { + params: { + partnerId: 1, + key: 1, + } + }; + expect(sirdataSubmodule.init(moduleConfig)).to.equal(true); }); it('has the correct module name', function () { expect(sirdataSubmodule.name).to.equal('SirdataRTDModule'); }); }); + describe('Sanitize content', function () { + it('removes PII from content', function () { + let doc = document.implementation.createHTMLDocument(''); + let div = doc.createElement('div'); + div.className = 'test'; + div.setAttribute('test', 'test'); + div.textContent = 'My email is test@test.com, My bank account number is 123456789012, my SSN is 123-45-6789, and my credit card number is 1234 5678 9101 1121.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'; + let div2 = doc.createElement('div'); + let div3 = doc.createElement('div'); + div3.innerText = 'hello'; + div2.appendChild(div3); + div.appendChild(div2); + doc.body.appendChild(div); + const cleanedDom = removePII(doc.documentElement.innerHTML); + const sanitizedDom = sanitizeContent(doc); + expect(cleanedDom).to.equal('
My email is , My bank account number is , my SSN is , and my credit card number is .Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
hello
'); + expect(sanitizedDom.documentElement.innerHTML).to.equal('
My email is , My bank account number is , my SSN is , and my credit card number is .Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
hello
'); + }); + }); + + describe('setUidInStorage', function () { + it('sets Id in Storage', function () { + setUidInStorage('123456789'); + let val = getUidFromStorage(); + expect(val).to.deep.equal([{source: 'sddan.com', uids: [{id: '123456789', atype: 1}]}]); + }); + }); + + describe('mergeEuidsArrays', function () { + it('merges Euids Arrays', function () { + const object1 = [{source: 'sddan.com', uids: [{id: '123456789', atype: 1}]}]; + const object2 = [{source: 'sddan.com', uids: [{id: '987654321', atype: 1}]}]; + const object3 = mergeEuidsArrays(object1, object2); + expect(object3).to.deep.equal([{source: 'sddan.com', uids: [{id: '123456789', atype: 1}, {id: '987654321', atype: 1}]}]); + }); + }); + + describe('onDocumentReady', function () { + it('on Document Ready function execution', function () { + const testString = ''; + const testFunction = function() { return true; }; + let resString; + try { + resString = onDocumentReady(testString); + } catch (e) {} + expect(resString).to.be.false; + let resFunction = onDocumentReady(testFunction); + expect(resFunction).to.be.true; + }); + }); + + describe('postContentForSemanticAnalysis', function () { + it('gets content for analysis', function () { + let res = postContentForSemanticAnalysis('1223456', 'https://www.sirdata.com/'); + let resEmpty = postContentForSemanticAnalysis('1223456', ''); + expect(res).to.be.true; + expect(resEmpty).to.be.false; + }); + }); + + describe('loadCustomFunction', function () { + it('load function', function () { + const res = loadCustomFunction(function(...args) { return true; }, {}, {}, {}, {}); + expect(res).to.be.true; + }); + }); + + describe('onDataDeletionRequest', function () { + it('destroy id', function () { + const moduleConfig = { + params: { + partnerId: 1, + key: 1, + } + }; + const res = onDataDeletionRequest(moduleConfig); + expect(res).to.be.true; + }); + }); + describe('Add Segment Data', function () { it('adds segment data', function () { const firstConfig = { @@ -25,9 +127,12 @@ describe('sirdataRtdProvider', function () { setGptKeyValues: true, gptCurationId: 27449, contextualMinRelevancyScore: 50, + actualUrl: 'https://www.sirdata.com/', + cookieAccessGranted: true, bidders: [] } }; + sirdataSubmodule.init(firstConfig); let adUnits = [ { @@ -68,13 +173,12 @@ describe('sirdataRtdProvider', function () { 'segtaxid': 4, 'cattaxid': 7, 'contextual_categories': {'345': 100, '456': 100} - } + }, + 'sddan_id': '123456789', + 'post_content_token': '987654321' } - }; - - addSegmentData(firstReqBidsConfigObj, firstData, firstConfig, () => { - }); - + } + addSegmentData(firstReqBidsConfigObj, firstData, adUnits, function() { return true; }); expect(firstReqBidsConfigObj.ortb2Fragments.global.user.data[0].ext.segtax).to.equal(4); }); }); @@ -109,6 +213,7 @@ describe('sirdataRtdProvider', function () { }] } }; + sirdataSubmodule.init(config); let reqBidsConfigObj = { adUnits: [{ @@ -197,11 +302,13 @@ describe('sirdataRtdProvider', function () { 'cattaxid': 7, 'contextual_categories': {'345': 100, '456': 100} } - } + }, + 'sddan_id': '123456789', + 'post_content_token': '987654321' }; getSegmentsAndCategories(reqBidsConfigObj, () => { - }, config, {}); + }, {}, {}); let request = server.requests[0]; request.respond(200, responseHeader, JSON.stringify(data)); @@ -228,16 +335,6 @@ describe('sirdataRtdProvider', function () { describe('Set ortb2 for bidder', function () { it('set ortb2 for a givent bidder', function () { - const config = { - params: { - setGptKeyValues: false, - contextualMinRelevancyScore: 50, - bidders: [{ - bidder: 'appnexus', - }] - } - }; - let reqBidsConfigObj = { adUnits: [{ bids: [{ @@ -252,22 +349,6 @@ describe('sirdataRtdProvider', function () { } }; - let data = { - 'segments': [111111, 222222], - 'segtaxid': null, - 'cattaxid': null, - 'contextual_categories': {'333333': 100}, - 'shared_taxonomy': { - '27440': { - 'segments': [444444, 555555], - 'segtaxid': null, - 'cattaxid': null, - 'contextual_categories': {'666666': 100} - } - }, - 'global_taxonomy': {} - }; - window.googletag = window.googletag || {}; window.googletag.cmd = window.googletag.cmd || []; From 0f91affebac045efd2470fafb76a0f138ea15f20 Mon Sep 17 00:00:00 2001 From: Aymeric Le Corre Date: Wed, 29 May 2024 00:13:02 +0200 Subject: [PATCH 34/46] Lucead Bid Adapter: Update (#11488) * Lucead Bid Adapter: Support Single Request Architecture mode + enhanced reporting + updated PAAPI auction config * PB9 compliance * Added TCF Global Vendor List ID * Add GVLID to Lucead RTD Provider * Add domain in impression tracking * remove RTD related files * remove useless code * remove lucead from adloader --- modules/luceadBidAdapter.js | 167 ++++++++++++--------- modules/luceadBidAdapter.md | 54 ++++--- src/adloader.js | 1 - test/spec/modules/luceadBidAdapter_spec.js | 97 ++++++------ 4 files changed, 186 insertions(+), 133 deletions(-) mode change 100644 => 100755 modules/luceadBidAdapter.js mode change 100644 => 100755 modules/luceadBidAdapter.md mode change 100644 => 100755 test/spec/modules/luceadBidAdapter_spec.js diff --git a/modules/luceadBidAdapter.js b/modules/luceadBidAdapter.js old mode 100644 new mode 100755 index ab7f96c4e60..b95dfc08732 --- a/modules/luceadBidAdapter.js +++ b/modules/luceadBidAdapter.js @@ -1,40 +1,42 @@ +/** + * @module modules/luceadBidAdapter + */ + import {ortbConverter} from '../libraries/ortbConverter/converter.js'; -import {loadExternalScript} from '../src/adloader.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {getUniqueIdentifierStr, logInfo, deepSetValue} from '../src/utils.js'; +import {getUniqueIdentifierStr, deepSetValue, logInfo} from '../src/utils.js'; import {fetch} from '../src/ajax.js'; +const gvlid = 1309; const bidderCode = 'lucead'; -const bidderName = 'Lucead'; -let baseUrl = 'https://lucead.com'; -let staticUrl = 'https://s.lucead.com'; -let companionUrl = 'https://cdn.jsdelivr.net/gh/lucead/prebid-js-external-js-lucead@master/dist/prod.min.js'; -let endpointUrl = 'https://prebid.lucead.com/go'; const defaultCurrency = 'EUR'; const defaultTtl = 500; const aliases = ['adliveplus']; +const defaultRegion = 'eu'; +const domain = 'lucead.com' +let baseUrl = `https://${domain}`; +let staticUrl = `https://s.${domain}`; +let endpointUrl = baseUrl; function isDevEnv() { - return location.hash.includes('prebid-dev') || location.href.startsWith('https://ayads.io/test'); + return location.hash.includes('prebid-dev'); } function isBidRequestValid(bidRequest) { return !!bidRequest?.params?.placementId; } -export function log(msg, obj) { - logInfo(`${bidderName} - ${msg}`, obj); -} - function buildRequests(bidRequests, bidderRequest) { + const region = bidRequests[0]?.params?.region || defaultRegion; + endpointUrl = `https://${region}.${domain}`; + if (isDevEnv()) { baseUrl = location.origin; staticUrl = baseUrl; - companionUrl = `${staticUrl}/dist/prebid-companion.js`; - endpointUrl = `${baseUrl}/go`; + endpointUrl = `${baseUrl}`; } - log('buildRequests', { + logInfo('buildRequests', { bidRequests, bidderRequest, }); @@ -50,107 +52,134 @@ function buildRequests(bidRequests, bidderRequest) { getUniqueIdentifierStr, ortbConverter, deepSetValue, + is_sra: true, + region, }; - loadExternalScript(companionUrl, bidderCode, () => window.ayads_prebid && window.ayads_prebid(companionData)); + window.lucead_prebid_data = companionData; + const fn = window.lucead_prebid; + + if (fn && typeof fn === 'function') { + fn(companionData); + } - return bidRequests.map(bidRequest => ({ + return { method: 'POST', - url: `${endpointUrl}/prebid/sub`, + url: `${endpointUrl}/go/prebid/sra`, data: JSON.stringify({ request_id: bidderRequest.bidderRequestId, domain: location.hostname, - bid_id: bidRequest.bidId, - sizes: bidRequest.sizes, - media_types: bidRequest.mediaTypes, - fledge_enabled: bidderRequest.fledgeEnabled, - enable_contextual: bidRequest?.params?.enableContextual !== false, - enable_pa: bidRequest?.params?.enablePA !== false, - params: bidRequest.params, + bid_requests: bidRequests.map(bidRequest => { + return { + bid_id: bidRequest.bidId, + sizes: bidRequest.sizes, + media_types: bidRequest.mediaTypes, + placement_id: bidRequest.params.placementId, + schain: bidRequest.schain, + }; + }), }), options: { contentType: 'text/plain', withCredentials: false }, - })); + }; } function interpretResponse(serverResponse, bidRequest) { // @see required fields https://docs.prebid.org/dev-docs/bidder-adaptor.html - const response = serverResponse.body; - const bidRequestData = JSON.parse(bidRequest.data); - - const bids = response.enable_contextual !== false ? [{ - requestId: response?.bid_id || '1', // bid request id, the bid id - cpm: response?.cpm || 0, - width: (response?.size && response?.size?.width) || 300, - height: (response?.size && response?.size?.height) || 250, - currency: response?.currency || defaultCurrency, - ttl: response?.ttl || defaultTtl, - creativeId: response.ssp ? `ssp:${response.ssp}` : (response?.ad_id || '0'), - netRevenue: response?.netRevenue || true, - ad: response?.ad || '', + const response = serverResponse?.body; + const bidRequestData = JSON.parse(bidRequest?.data); + + const bids = (response?.bids || []).map(bid => ({ + requestId: bid?.bid_id || '1', // bid request id, the bid id + cpm: bid?.cpm || 0, + width: (bid?.size && bid?.size?.width) || 300, + height: (bid?.size && bid?.size?.height) || 250, + currency: bid?.currency || defaultCurrency, + ttl: bid?.ttl || defaultTtl, + creativeId: bid?.ssp ? `ssp:${bid.ssp}` : `${bid?.ad_id || 0}:${bid?.ig_id || 0}`, + netRevenue: bid?.net_revenue || true, + ad: bid?.ad || '', meta: { - advertiserDomains: response?.advertiserDomains || [], + advertiserDomains: bid?.advertiser_domains || [], }, - }] : null; - - log('interpretResponse', {serverResponse, bidRequest, bidRequestData, bids}); - - if (response.enable_pa === false) { return bids; } - - const fledgeAuctionConfig = { - seller: baseUrl, - decisionLogicUrl: `${baseUrl}/js/ssp.js`, - interestGroupBuyers: [baseUrl], - perBuyerSignals: {}, - auctionSignals: { - size: bidRequestData.sizes ? {width: bidRequestData?.sizes[0][0] || 300, height: bidRequestData?.sizes[0][1] || 250} : null, - }, - }; + })); - const fledgeAuctionConfigs = [{bidId: response.bid_id, config: fledgeAuctionConfig}]; + logInfo('interpretResponse', {serverResponse, bidRequest, bidRequestData, bids}); + + if (response?.enable_pa === false) { return bids; } + + const fledgeAuctionConfigs = (response.bids || []).map(bid => ({ + bidId: bid?.bid_id, + config: { + seller: baseUrl, + decisionLogicUrl: `${baseUrl}/js/ssp.js`, + interestGroupBuyers: [baseUrl], + requestedSize: bid?.size, + auctionSignals: { + size: bid?.size, + }, + perBuyerSignals: { + [baseUrl]: { + prebid_paapi: true, + prebid_bid_id: bid?.bid_id, + prebid_request_id: bidRequestData.request_id, + placement_id: bid.placement_id, + // floor, + is_sra: true, + endpoint_url: endpointUrl, + }, + } + } + })); return {bids, fledgeAuctionConfigs}; } -function report(type = 'impression', data = {}) { +function report(type, data) { // noinspection JSCheckFunctionSignatures - return fetch(`${endpointUrl}/report/${type}`, { - body: JSON.stringify(data), + return fetch(`${endpointUrl}/go/report/${type}`, { + body: JSON.stringify({ + ...data, + domain: location.hostname, + }), method: 'POST', - contentType: 'text/plain' + contentType: 'text/plain', }); } function onBidWon(bid) { - log('Bid won', bid); + logInfo('Bid won', bid); let data = { bid_id: bid?.bidId, - placement_id: bid?.params ? bid?.params[0]?.placementId : 0, + placement_id: bid.params ? (bid?.params[0]?.placementId || '0') : '0', spent: bid?.cpm, currency: bid?.currency, }; - if (bid.creativeId) { - if (bid.creativeId.toString().startsWith('ssp:')) { - data.ssp = bid.creativeId.split(':')[1]; + if (bid?.creativeId) { + const parts = bid.creativeId.toString().split(':'); + + if (parts[0] === 'ssp') { + data.ssp = parts[1]; } else { - data.ad_id = bid.creativeId; + data.ad_id = parts[0] + data.ig_id = parts[1] } } - return report(`impression`, data); + return report('impression', data); } function onTimeout(timeoutData) { - log('Timeout from adapter', timeoutData); + logInfo('Timeout from adapter', timeoutData); } export const spec = { code: bidderCode, - // gvlid: BIDDER_GVLID, + gvlid, aliases, isBidRequestValid, buildRequests, diff --git a/modules/luceadBidAdapter.md b/modules/luceadBidAdapter.md old mode 100644 new mode 100755 index 953c911cd2b..41e3730897a --- a/modules/luceadBidAdapter.md +++ b/modules/luceadBidAdapter.md @@ -1,29 +1,41 @@ -# Overview +# Lucead Bid Adapter -Module Name: Lucead Bidder Adapter +- Module Name: Lucead Bidder Adapter +- Module Type: Bidder Adapter +- Maintainer: prebid@lucead.com -Module Type: Bidder Adapter +## Description -Maintainer: prebid@lucead.com +Module that connects to Lucead demand source. -# Description +## Adapter configuration -Module that connects to Lucead demand source to fetch bids. +## Ad units parameters -# Test Parameters +### Type definition + +```typescript +type Params = { + placementId: string; + region?: 'eu' | 'us' | 'ap'; +}; ``` -const adUnits = [ - { - code: 'test-div', - sizes: [[300, 250]], - bids: [ - { - bidder: 'lucead', - params: { - placementId: '2', - } - } - ] - } - ]; + +### Example code +```javascript +const adUnits=[ + { + code:'test-div', + sizes:[[300,250]], + bids:[ + { + bidder: 'lucead', + params:{ + placementId: '1', + region: 'us', // optional: 'eu', 'us', 'ap' + } + } + ] + } +]; ``` diff --git a/src/adloader.js b/src/adloader.js index 30693560133..b746c59a1cc 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -33,7 +33,6 @@ const _approvedLoadExternalJSList = [ 'dynamicAdBoost', 'contxtful', 'id5', - 'lucead', '51Degrees', ]; diff --git a/test/spec/modules/luceadBidAdapter_spec.js b/test/spec/modules/luceadBidAdapter_spec.js old mode 100644 new mode 100755 index 72bc7cc2d6e..6f61071b653 --- a/test/spec/modules/luceadBidAdapter_spec.js +++ b/test/spec/modules/luceadBidAdapter_spec.js @@ -28,6 +28,7 @@ describe('Lucead Adapter', () => { bidder: 'lucead', params: { placementId: '1', + region: 'eu', }, }; }); @@ -39,7 +40,10 @@ describe('Lucead Adapter', () => { describe('onBidWon', function () { let sandbox; - const bid = { foo: 'bar', creativeId: 'ssp:improve' }; + const bids = [ + { foo: 'bar', creativeId: 'ssp:improve' }, + { foo: 'bar', creativeId: '123:456' }, + ]; beforeEach(function () { sandbox = sinon.sandbox.create(); @@ -47,8 +51,11 @@ describe('Lucead Adapter', () => { it('should trigger impression pixel', function () { sandbox.spy(ajax, 'fetch'); - spec.onBidWon(bid); - expect(ajax.fetch.args[0][0]).to.match(/report\/impression$/); + + for (const bid of bids) { + spec.onBidWon(bid); + expect(ajax?.fetch?.args[0][0]).to.match(/report\/impression$/); + } }); afterEach(function () { @@ -77,49 +84,62 @@ describe('Lucead Adapter', () => { it('should have a post method', function () { const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request[0].method).to.equal('POST'); + expect(request.method).to.equal('POST'); }); it('should contains a request id equals to the bid id', function () { const request = spec.buildRequests(bidRequests, bidderRequest); - expect(JSON.parse(request[0].data).bid_id).to.equal(bidRequests[0].bidId); + expect(JSON.parse(request.data).bid_requests[0].bid_id).to.equal(bidRequests[0].bidId); }); - it('should have an url that contains sub keyword', function () { + it('should have an url that contains sra keyword', function () { const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request[0].url).to.match(/sub/); + expect(request.url).to.contain('/prebid/sra'); }); }); describe('interpretResponse', function () { - const serverResponse = { - body: { - 'bid_id': '2daf899fbe4c52', - 'request_id': '13aaa3df18bfe4', - 'ad': 'Ad', - 'ad_id': '3890677904', - 'cpm': 3.02, - 'currency': 'USD', - 'time': 1707257712095, - 'size': {'width': 300, 'height': 250}, - } + const serverResponseBody = { + 'request_id': '17548f887fb722', + 'bids': [ + { + 'bid_id': '2d663fdd390b49', + 'ad': '\u003chtml lang="en"\u003e\u003cbody style="margin:0;background-color:#FFF"\u003e\u003ciframe src="urn:uuid:fb81a0f9-b83a-4f27-8676-26760d090f1c" style="width:300px;height:250px;border:none" seamless \u003e\u003c/iframe\u003e\u003c/body\u003e\u003c/html\u003e', + 'size': { + 'width': 300, + 'height': 250 + }, + 'ad_id': '1', + 'ig_id': '1', + 'cpm': 1, + 'currency': 'EUR', + 'time': 0, + 'ssp': '', + 'placement_id': '1', + 'is_pa': true + } + ] }; - const bidRequest = {data: JSON.stringify({ - 'request_id': '13aaa3df18bfe4', - 'domain': '7cdb-2a02-8429-e4a0-1701-bc69-d51c-86e-b279.ngrok-free.app', - 'bid_id': '2daf899fbe4c52', - 'sizes': [[300, 250]], - 'media_types': {'banner': {'sizes': [[300, 250]]}}, - 'fledge_enabled': true, - 'enable_contextual': true, - 'enable_pa': true, - 'params': {'placementId': '1'}, - })}; + const serverResponse = {body: serverResponseBody}; + + const bidRequest = { + data: JSON.stringify({ + 'request_id': '17548f887fb722', + 'domain': 'lucead.com', + 'bid_requests': [{ + 'bid_id': '2d663fdd390b49', + 'sizes': [[300, 250], [300, 150]], + 'media_types': {'banner': {'sizes': [[300, 250], [300, 150]]}}, + 'placement_id': '1' + }], + }), + }; it('should get correct bid response', function () { const result = spec.interpretResponse(serverResponse, bidRequest); + // noinspection JSCheckFunctionSignatures expect(Object.keys(result.bids[0])).to.have.members([ 'requestId', 'cpm', @@ -135,7 +155,7 @@ describe('Lucead Adapter', () => { }); it('should return bid empty response', function () { - const serverResponse = {body: {cpm: 0}}; + const serverResponse = {body: {bids: [{cpm: 0}]}}; const bidRequest = {data: '{}'}; const result = spec.interpretResponse(serverResponse, bidRequest); expect(result.bids[0].ad).to.be.equal(''); @@ -154,18 +174,11 @@ describe('Lucead Adapter', () => { expect(Object.keys(result.bids[0].meta)).to.include.members(['advertiserDomains']); }); - it('should support disabled contextual bids', function () { - const serverResponseWithDisabledContectual = deepClone(serverResponse); - serverResponseWithDisabledContectual.body.enable_contextual = false; - const result = spec.interpretResponse(serverResponseWithDisabledContectual, bidRequest); - expect(result.bids).to.be.null; - }); - - it('should support disabled Protected Audience', function () { - const serverResponseWithEnablePaFalse = deepClone(serverResponse); - serverResponseWithEnablePaFalse.body.enable_pa = false; - const result = spec.interpretResponse(serverResponseWithEnablePaFalse, bidRequest); - expect(result.fledgeAuctionConfigs).to.be.undefined; + it('should support enable_pa = false', function () { + serverResponse.body.enable_pa = false; + const result = spec.interpretResponse(serverResponse, bidRequest); + expect(result).to.be.an('array'); + expect(result[0].cpm).to.be.greaterThan(0); }); }); }); From e1b028fd270192f2176e20a77728902a6ad9898b Mon Sep 17 00:00:00 2001 From: nouchy <33549554+nouchy@users.noreply.github.com> Date: Wed, 29 May 2024 14:30:24 +0200 Subject: [PATCH 35/46] new lint rule for Prebid 9 fix : use textContent instead of innerText (#11598) --- modules/sirdataRtdProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/sirdataRtdProvider.js b/modules/sirdataRtdProvider.js index 0ad3b891c40..9a222e20f9d 100644 --- a/modules/sirdataRtdProvider.js +++ b/modules/sirdataRtdProvider.js @@ -284,7 +284,7 @@ export function removePII(content) { * @returns {Object} - The sanitized content */ export function sanitizeContent(content) { - if (content && content.documentElement.innerText && content.documentElement.innerText.length > 500) { + if (content && content.documentElement.textContent && content.documentElement.textContent.length > 500) { // Reduce size by removing useless content // Allowed tags const allowedTags = [ From deeeb9e7c116a3c4d74d2c0b7e429c68ff3528aa Mon Sep 17 00:00:00 2001 From: Shubham <127132399+shubhamc-ins@users.noreply.github.com> Date: Wed, 29 May 2024 19:09:11 +0530 Subject: [PATCH 36/46] update placement logic, deprecated in 9.0 prebid version (#11600) --- modules/insticatorBidAdapter.js | 14 ++++------- .../spec/modules/insticatorBidAdapter_spec.js | 25 +++---------------- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/modules/insticatorBidAdapter.js b/modules/insticatorBidAdapter.js index bff74f0755b..636c0162a02 100644 --- a/modules/insticatorBidAdapter.js +++ b/modules/insticatorBidAdapter.js @@ -102,7 +102,7 @@ function buildVideo(bidRequest) { let w = deepAccess(bidRequest, 'mediaTypes.video.w'); let h = deepAccess(bidRequest, 'mediaTypes.video.h'); const mimes = deepAccess(bidRequest, 'mediaTypes.video.mimes'); - const placement = deepAccess(bidRequest, 'mediaTypes.video.placement') || 3; + const placement = deepAccess(bidRequest, 'mediaTypes.video.placement'); const plcmt = deepAccess(bidRequest, 'mediaTypes.video.plcmt') || undefined; const playerSize = deepAccess(bidRequest, 'mediaTypes.video.playerSize'); const context = deepAccess(bidRequest, 'mediaTypes.video.context'); @@ -136,6 +136,10 @@ function buildVideo(bidRequest) { } } + if (placement && typeof placement !== 'undefined' && typeof placement === 'number') { + optionalParams['placement'] = placement; + } + if (plcmt) { optionalParams['plcmt'] = plcmt; } @@ -145,7 +149,6 @@ function buildVideo(bidRequest) { } let videoObj = { - placement, mimes, w, h, @@ -595,13 +598,6 @@ function validateVideo(bid) { return false; } - const placement = deepAccess(bid, 'mediaTypes.video.placement'); - - if (typeof placement !== 'undefined' && typeof placement !== 'number') { - logError('insticator: video placement is not a number'); - return false; - } - const plcmt = deepAccess(bid, 'mediaTypes.video.plcmt'); if (typeof plcmt !== 'undefined' && typeof plcmt !== 'number') { diff --git a/test/spec/modules/insticatorBidAdapter_spec.js b/test/spec/modules/insticatorBidAdapter_spec.js index 489e213b0fa..72ee132f6f6 100644 --- a/test/spec/modules/insticatorBidAdapter_spec.js +++ b/test/spec/modules/insticatorBidAdapter_spec.js @@ -175,25 +175,6 @@ describe('InsticatorBidAdapter', function () { })).to.be.true; }) - it('should return false if video placement is not a number', () => { - expect(spec.isBidRequestValid({ - ...bidRequest, - ...{ - mediaTypes: { - video: { - mimes: [ - 'video/mp4', - 'video/mpeg', - ], - w: 250, - h: 300, - placement: 'NaN', - }, - } - } - })).to.be.false; - }); - it('should return false if video plcmt is not a number', () => { expect(spec.isBidRequestValid({ ...bidRequest, @@ -224,7 +205,7 @@ describe('InsticatorBidAdapter', function () { 'video/mpeg', ], playerSize: [250, 300], - placement: 1, + plcmt: 1, }, } } @@ -293,7 +274,7 @@ describe('InsticatorBidAdapter', function () { 'video/mpeg', ], playerSize: [250, 300], - placement: 1, + plcmt: 1, }, } }, @@ -306,7 +287,7 @@ describe('InsticatorBidAdapter', function () { 'video/x-flv', 'video/webm', ], - placement: 2, + plcmt: 2, }, } })).to.be.true; From 1cad24dddebc08947391b2cb901f4a9aa675ce7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Sok=C3=B3=C5=82?= <88041828+krzysztofequativ@users.noreply.github.com> Date: Wed, 29 May 2024 15:51:56 +0200 Subject: [PATCH 37/46] Smartadserver Bid Adapter : refactor gpid logic as part of Prebid 9.0 (#11602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Smartadserver Bid Adapter: Add support for SDA user and site * Smartadserver Bid Adapter: Fix SDA support getConfig and add to unit testing * support floors per media type * Add GPP support * Rework payloads enriching * Add gpid support * Support additional video params * vpmt as array of numbers * Fix comment * Update default startDelay * Include videoMediaType's startdelay * Handle specified midroll * add smartadserver topics iframe * gpid first, then pbadslot as fallback * drop topics iframe --------- Co-authored-by: Meven Courouble Co-authored-by: Krzysztof Sokół <88041828+smart-adserver@users.noreply.github.com> Co-authored-by: Dariusz O --- modules/smartadserverBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index 7edaaa36957..415061d0eda 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -196,7 +196,7 @@ export const spec = { sdc: sellerDefinedContext }; - const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); if (gpid) { payload.gpid = gpid; } From 2fef9c29351d2b4ada4a3ba6b008e616a15a0af8 Mon Sep 17 00:00:00 2001 From: Dmitry Sinev Date: Wed, 29 May 2024 18:36:48 +0300 Subject: [PATCH 38/46] Appnexus Bid Adapter: parse the currency from the bid if specified (#11581) * Appnexus Bid Adapter: parse the currency from the bid if specified * Appnexus Bid Adapter: parse the currency from the bid if specified, change code to codename --- modules/appnexusBidAdapter.js | 2 +- test/spec/modules/appnexusBidAdapter_spec.js | 33 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 5a81f272db5..b0c91a14a46 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -597,7 +597,7 @@ function newBid(serverBid, rtbBid, bidderRequest) { cpm: rtbBid.cpm, creativeId: rtbBid.creative_id, dealId: rtbBid.deal_id, - currency: 'USD', + currency: rtbBid.publisher_currency_codename || 'USD', netRevenue: true, ttl: 300, adUnitCode: bidRequest.adUnitCode, diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index c2da2f36223..393768c3063 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -1748,6 +1748,7 @@ describe('AppNexusAdapter', function () { 'cpm': 0.5, 'cpm_publisher_currency': 0.5, 'publisher_currency_code': '$', + 'publisher_currency_codename': 'USD', 'client_initiated_ad_counting': true, 'viewability': { 'config': '' @@ -1832,6 +1833,38 @@ describe('AppNexusAdapter', function () { expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); }); + it('should parse non-default currency', function () { + let eurCpmResponse = deepClone(response); + eurCpmResponse.tags[0].ads[0].publisher_currency_codename = 'EUR'; + + let bidderRequest = { + bidderCode: 'appnexus', + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + }; + + let result = spec.interpretResponse({ body: eurCpmResponse }, { bidderRequest }); + expect(result[0].currency).to.equal('EUR'); + }); + + it('should parse default currency', function () { + let defaultCpmResponse = deepClone(response); + delete defaultCpmResponse.tags[0].ads[0].publisher_currency_codename; + + let bidderRequest = { + bidderCode: 'appnexus', + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + }; + + let result = spec.interpretResponse({ body: defaultCpmResponse }, { bidderRequest }); + expect(result[0].currency).to.equal('USD'); + }); + it('should reject 0 cpm bids', function () { let zeroCpmResponse = deepClone(response); zeroCpmResponse.tags[0].ads[0].cpm = 0; From 45a77ef37480c621268e73ed66d6dcf612fd87c4 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 29 May 2024 11:32:27 -0700 Subject: [PATCH 39/46] PBS adapter: do not set bidder schain in source.ext.schain (#11467) --- .../prebidServerBidAdapter/ortbConverter.js | 11 +----- .../modules/prebidServerBidAdapter_spec.js | 35 +++++++++++-------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/modules/prebidServerBidAdapter/ortbConverter.js b/modules/prebidServerBidAdapter/ortbConverter.js index e0f038767c2..7f4ffc84070 100644 --- a/modules/prebidServerBidAdapter/ortbConverter.js +++ b/modules/prebidServerBidAdapter/ortbConverter.js @@ -197,9 +197,7 @@ const PBS_CONVERTER = ortbConverter({ context.actualBidderRequests.forEach(req => orig(ortbRequest, req, context)); }, sourceExtSchain(orig, ortbRequest, proxyBidderRequest, context) { - // pass schains in ext.prebid.schains, with the most commonly used one in source.ext.schain - let mainChain; - + // pass schains in ext.prebid.schains let chains = (deepAccess(ortbRequest, 'ext.prebid.schains') || []); const chainBidders = new Set(chains.flatMap((item) => item.bidders)); @@ -218,17 +216,10 @@ const PBS_CONVERTER = ortbConverter({ chains[key] = {bidders: new Set(), schain}; } bidders.forEach((bidder) => chains[key].bidders.add(bidder)); - if (mainChain == null || chains[key].bidders.size > mainChain.bidders.size) { - mainChain = chains[key] - } return chains; }, {}) ).map(({bidders, schain}) => ({bidders: Array.from(bidders), schain})); - if (mainChain != null) { - deepSetValue(ortbRequest, 'source.ext.schain', mainChain.schain); - } - if (chains.length) { deepSetValue(ortbRequest, 'ext.prebid.schains', chains); } diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 9c2ac8a23a9..8e92cecd36b 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -2377,23 +2377,28 @@ describe('S2S Adapter', function () { ]); }); - it('should "promote" the most reused bidder schain to source.ext.schain', () => { - const bidderReqs = [ - {...deepClone(BID_REQUESTS[0]), bidderCode: 'A'}, - {...deepClone(BID_REQUESTS[0]), bidderCode: 'B'}, - {...deepClone(BID_REQUESTS[0]), bidderCode: 'C'} - ]; - const chain1 = {chain: 1}; - const chain2 = {chain: 2}; + Object.entries({ + 'set': {}, + 'override': {source: {ext: {schain: 'pub-provided'}}} + }).forEach(([t, fpd]) => { + it(`should not ${t} source.ext.schain`, () => { + const bidderReqs = [ + {...deepClone(BID_REQUESTS[0]), bidderCode: 'A'}, + {...deepClone(BID_REQUESTS[0]), bidderCode: 'B'}, + {...deepClone(BID_REQUESTS[0]), bidderCode: 'C'} + ]; + const chain1 = {chain: 1}; + const chain2 = {chain: 2}; - bidderReqs[0].bids[0].schain = chain1; - bidderReqs[1].bids[0].schain = chain2; - bidderReqs[2].bids[0].schain = chain2; + bidderReqs[0].bids[0].schain = chain1; + bidderReqs[1].bids[0].schain = chain2; + bidderReqs[2].bids[0].schain = chain2; - adapter.callBids(REQUEST, bidderReqs, addBidResponse, done, ajax); - const req = JSON.parse(server.requests[0].requestBody); - expect(req.source.ext.schain).to.eql(chain2); - }); + adapter.callBids({...REQUEST, ortb2Fragments: {global: fpd}}, bidderReqs, addBidResponse, done, ajax); + const req = JSON.parse(server.requests[0].requestBody); + expect(req.source?.ext?.schain).to.eql(fpd?.source?.ext?.schain); + }) + }) it('passes multibid array in request', function () { const bidRequests = utils.deepClone(BID_REQUESTS); From 2928473698507417b8ee4098060ae85d386ecdd9 Mon Sep 17 00:00:00 2001 From: Gabriel Chicoye Date: Wed, 29 May 2024 20:42:59 +0200 Subject: [PATCH 40/46] Nexx360 Bid Adapter: Additional localStorage information (#11466) * ext.vastxml to adm * amxId added * test fix --------- Co-authored-by: Gabriel Chicoye --- modules/nexx360BidAdapter.js | 36 ++++++++++++++++++-- test/spec/modules/nexx360BidAdapter_spec.js | 37 +++++++++++++++++++-- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/modules/nexx360BidAdapter.js b/modules/nexx360BidAdapter.js index c31c3d81aeb..b4f7cf50ffe 100644 --- a/modules/nexx360BidAdapter.js +++ b/modules/nexx360BidAdapter.js @@ -22,7 +22,7 @@ const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstre const BIDDER_CODE = 'nexx360'; const REQUEST_URL = 'https://fast.nexx360.io/booster'; const PAGE_VIEW_ID = generateUUID(); -const BIDDER_VERSION = '4.0'; +const BIDDER_VERSION = '4.1'; const GVLID = 965; const NEXXID_KEY = 'nexx360_storage'; @@ -69,6 +69,19 @@ function getAdContainer(container) { } } +/** + * Get the AMX ID + * @return {string | false } false if localstorageNotEnabled + */ +export function getAmxId() { + if (!storage.localStorageIsEnabled()) { + logInfo(`localstorage not enabled for Nexx360`); + return false; + } + const amxId = storage.getDataFromLocalStorage('__amuidpb'); + return amxId || false; +} + const converter = ortbConverter({ context: { netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false @@ -107,13 +120,30 @@ const converter = ortbConverter({ request(buildRequest, imps, bidderRequest, context) { const request = buildRequest(imps, bidderRequest, context); const nexx360LocalStorage = getNexx360LocalStorage(); - if (nexx360LocalStorage) deepSetValue(request, 'ext.nexx360Id', nexx360LocalStorage.nexx360Id); + if (nexx360LocalStorage) { + deepSetValue(request, 'ext.nexx360Id', nexx360LocalStorage.nexx360Id); + deepSetValue(request, 'ext.localStorage.nexx360Id', nexx360LocalStorage.nexx360Id); + } + const amxId = getAmxId(); + if (amxId) deepSetValue(request, 'ext.localStorage.amxId', amxId()); deepSetValue(request, 'ext.version', '$prebid.version$'); deepSetValue(request, 'ext.source', 'prebid.js'); deepSetValue(request, 'ext.pageViewId', PAGE_VIEW_ID); deepSetValue(request, 'ext.bidderVersion', BIDDER_VERSION); deepSetValue(request, 'cur', [config.getConfig('currency.adServerCurrency') || 'USD']); - if (!request.user) deepSetValue(request, 'user', {}); + if (!request.user) request.user = {}; + if (getAmxId()) { + if (!request.user.ext) request.user.ext = {}; + if (!request.user.ext.eids) request.user.ext.eids = []; + request.user.ext.eids.push({ + source: 'amxdt.net', + uids: [{ + id: `${getAmxId()}`, + atype: 1 + }] + }); + } + return request; }, }); diff --git a/test/spec/modules/nexx360BidAdapter_spec.js b/test/spec/modules/nexx360BidAdapter_spec.js index f18e0365226..06cbec347ff 100644 --- a/test/spec/modules/nexx360BidAdapter_spec.js +++ b/test/spec/modules/nexx360BidAdapter_spec.js @@ -3,6 +3,7 @@ import { spec, storage, getNexx360LocalStorage, } from 'modules/nexx360BidAdapter.js'; import { sandbox } from 'sinon'; +import { getAmxId } from '../../../modules/nexx360BidAdapter'; const instreamResponse = { 'id': '2be64380-ba0c-405a-ab53-51f51c7bde51', @@ -220,7 +221,7 @@ describe('Nexx360 bid adapter tests', function () { after(function () { sandbox.restore() }); - }) + }); describe('getNexx360LocalStorage enabled', function () { before(function () { @@ -235,7 +236,37 @@ describe('Nexx360 bid adapter tests', function () { after(function () { sandbox.restore() }); - }) + }); + + describe('getAmxId() with localStorage enabled and data not set', function() { + before(function () { + sandbox.stub(storage, 'localStorageIsEnabled').callsFake(() => true); + sandbox.stub(storage, 'setDataInLocalStorage'); + sandbox.stub(storage, 'getDataFromLocalStorage').callsFake((key) => null); + }); + it('We test if we get the amxId', function() { + const output = getAmxId(); + expect(output).to.be.eql(false); + }); + after(function () { + sandbox.restore() + }); + }); + + describe('getAmxId() with localStorage enabled and data set', function() { + before(function () { + sandbox.stub(storage, 'localStorageIsEnabled').callsFake(() => true); + sandbox.stub(storage, 'setDataInLocalStorage'); + sandbox.stub(storage, 'getDataFromLocalStorage').callsFake((key) => 'abcdef'); + }); + it('We test if we get the amxId', function() { + const output = getAmxId(); + expect(output).to.be.eql('abcdef'); + }); + after(function () { + sandbox.restore() + }); + }); describe('buildRequests()', function() { before(function () { @@ -374,7 +405,7 @@ describe('Nexx360 bid adapter tests', function () { expect(requestContent.imp[1].tagid).to.be.eql('div-2-abcd'); expect(requestContent.imp[1].ext.adUnitCode).to.be.eql('div-2-abcd'); expect(requestContent.imp[1].ext.divId).to.be.eql('div-2-abcd'); - expect(requestContent.ext.bidderVersion).to.be.eql('4.0'); + expect(requestContent.ext.bidderVersion).to.be.eql('4.1'); expect(requestContent.ext.source).to.be.eql('prebid.js'); }); From 6b7c86ef41f20b3fefda87484d7270fdf22239fb Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 29 May 2024 21:54:33 +0200 Subject: [PATCH 41/46] Adagio Rtd Provider: initial release (#11509) * AdagioRtdProvider: add Adagio Rtd Provider * AdagioRtdProvider: missing callback() call * AdagioRtdProvider: set signals for all --- modules/adagioRtdProvider.js | 690 ++++++++++++++++++++ modules/adagioRtdProvider.md | 37 ++ test/spec/modules/adagioRtdProvider_spec.js | 532 +++++++++++++++ 3 files changed, 1259 insertions(+) create mode 100644 modules/adagioRtdProvider.js create mode 100644 modules/adagioRtdProvider.md create mode 100644 test/spec/modules/adagioRtdProvider_spec.js diff --git a/modules/adagioRtdProvider.js b/modules/adagioRtdProvider.js new file mode 100644 index 00000000000..a901c2c489d --- /dev/null +++ b/modules/adagioRtdProvider.js @@ -0,0 +1,690 @@ +/** + * This module adds the adagio provider to the Real Time Data module (rtdModule). + * The {@link module:modules/realTimeData} module is required. + * @module modules/adagioRtdProvider + * @requires module:modules/realTimeData + */ +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import adapterManager from '../src/adapterManager.js'; +import { loadExternalScript } from '../src/adloader.js'; +import { submodule } from '../src/hook.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { + canAccessWindowTop, + deepAccess, + deepSetValue, + generateUUID, + getUniqueIdentifierStr, + getWindowSelf, + getWindowTop, + inIframe, + isNumber, + isSafeFrameWindow, + isStr, + prefixLog +} from '../src/utils.js'; +import { getGptSlotInfoForAdUnitCode } from '../libraries/gptUtils/gptUtils.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + * @typedef {import('../modules/rtdModule/index.js').adUnit} adUnit + */ +const SUBMODULE_NAME = 'adagio'; +const ADAGIO_BIDDER_CODE = 'adagio'; +const GVLID = 617; +const SCRIPT_URL = 'https://script.4dex.io/a/latest/adagio.js'; +const SESS_DURATION = 30 * 60 * 1000; +export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }); + +const { logError, logWarn } = prefixLog('AdagioRtdProvider:'); + +// Guard to avoid storing the same bid data several times. +const guard = new Set(); + +/** + * Returns the window.ADAGIO global object used to store Adagio data. + * This object is created in window.top if possible, otherwise in window.self. + */ +const _ADAGIO = (function() { + const w = (canAccessWindowTop()) ? getWindowTop() : getWindowSelf(); + + w.ADAGIO = w.ADAGIO || {}; + w.ADAGIO.pageviewId = w.ADAGIO.pageviewId || generateUUID(); + w.ADAGIO.adUnits = w.ADAGIO.adUnits || {}; + w.ADAGIO.pbjsAdUnits = w.ADAGIO.pbjsAdUnits || []; + w.ADAGIO.queue = w.ADAGIO.queue || []; + w.ADAGIO.windows = w.ADAGIO.windows || []; + + return w.ADAGIO; +})(); + +/** + * Store the sampling data. + * This data is used to determine if beacons should be sent to adagio. + * The sampling data + */ +const _SESSION = (function() { + /** + * @type {SessionData} + */ + const data = { + session: {} + }; + + return { + init: () => { + // helper function to determine if the session is new. + const isNewSession = (lastActivity) => { + const now = Date.now(); + return (!isNumber(lastActivity) || (now - lastActivity) > SESS_DURATION); + }; + + storage.getDataFromLocalStorage('adagio', (storageValue) => { + // session can be an empty object + const { rnd, new: isNew, vwSmplg, vwSmplgNxt, lastActivityTime } = _internal.getSessionFromLocalStorage(storageValue); + + data.session = { + rnd, + new: isNew || false, // legacy: `new` was used but the choosen name is not good. + // Don't use values if they are not defined. + ...(vwSmplg !== undefined && { vwSmplg }), + ...(vwSmplgNxt !== undefined && { vwSmplgNxt }), + ...(lastActivityTime !== undefined && { lastActivityTime }) + }; + + if (isNewSession(lastActivityTime)) { + data.session.new = true; + data.session.rnd = Math.random(); + } + + _internal.getAdagioNs().queue.push({ + action: 'session', + ts: Date.now(), + data: { + session: { + ...data.session + } + } + }); + }); + }, + get: function() { + return data.session; + } + }; +})(); + +const _FEATURES = (function() { + /** + * @type {Features} + */ + const features = { + initialized: false, + data: {}, + }; + + return { + // reset is used for testing purpose + reset: function() { + features.initialized = false; + features.data = {}; + }, + get: function() { + if (!features.initialized) { + features.data = { + page_dimensions: getPageDimensions().toString(), + viewport_dimensions: getViewPortDimensions().toString(), + user_timestamp: getTimestampUTC().toString(), + dom_loading: getDomLoadingDuration().toString(), + }; + features.initialized = true; + } + + return { ...features.data }; + } + }; +})(); + +export const _internal = { + getAdagioNs: function() { + return _ADAGIO; + }, + + getSession: function() { + return _SESSION; + }, + + getFeatures: function() { + return _FEATURES; + }, + + getGuard: function() { + return guard; + }, + + /** + * Ensure that the bidder is Adagio. + * + * @param {string} alias + * @returns {boolean} + */ + isAdagioBidder: function (alias) { + if (!alias) { + return false; + } + return (alias + adapterManager.aliasRegistry[alias]).toLowerCase().includes(ADAGIO_BIDDER_CODE); + }, + + /** + * Returns the session data from the localStorage. + * + * @param {string} storageValue - The value stored in the localStorage. + * @returns {Session} + */ + getSessionFromLocalStorage: function(storageValue) { + const _default = { + new: true, + rnd: Math.random() + }; + + const obj = JSON.parse(storageValue, function(name, value) { + if (name.charAt(0) !== '_' || name === '') { + return value; + } + }); + + return (!obj || !obj.session) ? _default : obj.session; + } +}; + +function loadAdagioScript(config) { + storage.localStorageIsEnabled(isValid => { + if (!isValid) { + return; + } + + loadExternalScript(SCRIPT_URL, SUBMODULE_NAME, undefined, undefined, { id: `adagiojs-${getUniqueIdentifierStr()}`, 'data-pid': config.params.organizationId }); + }); +} + +/** + * Initialize the Adagio RTD Module. + * @param {Object} config + * @param {Object} _userConsent + * @returns {boolean} + */ +function init(config, _userConsent) { + if (!isStr(config.params?.organizationId) || !isStr(config.params?.site)) { + logError('organizationId is required and must be a string.'); + return false; + } + + _internal.getAdagioNs().hasRtd = true; + + _internal.getSession().init(); + + registerEventsForAdServers(config); + + loadAdagioScript(config); + + return true; +} + +/** + * onBidRequest is called for each bidder during an auction and contains the bids for that bidder. + * + * @param {*} bidderRequest + * @param {*} config + * @param {*} _userConsent + */ +function onBidRequest(bidderRequest, config, _userConsent) { + // setTimeout trick to ensure that the `bidderRequest.params` values updated by a bidder adapter are taken into account. + // @todo: Check why we have to do it like this, and if there is a better way. Check how the event is dispatched in rtdModule/index.js + setTimeout(() => { + bidderRequest.bids.forEach(bid => { + const uid = deepAccess(bid, 'ortb2.site.ext.data.adg_rtd.uid'); + if (!uid) { + logError('The `uid` is required to store the request in the ADAGIO namespace.'); + return; + } + + // No need to store the same info several times. + // `uid` is unique as it is generated by the RTD module itself for each auction. + const key = `${bid.adUnitCode}-${uid}`; + if (_internal.getGuard().has(key)) { + return; + } + + _internal.getGuard().add(key); + storeRequestInAdagioNS(bid, config); + }); + }, 1); +} + +/** + * onGetBidRequestData is called once per auction. + * Update both the `ortb2Fragments` and `ortb2Imp` objects with features computed for Adagio. + * + * @param {*} bidReqConfig + * @param {*} callback + * @param {*} config + */ +function onGetBidRequestData(bidReqConfig, callback, config) { + const { site: ortb2Site } = bidReqConfig.ortb2Fragments.global; + const features = _internal.getFeatures().get(); + const ext = { + uid: generateUUID(), + features: { ...features }, + session: { ..._SESSION.get() } + }; + + deepSetValue(ortb2Site, `ext.data.adg_rtd`, ext); + + const adUnits = bidReqConfig.adUnits || getGlobal().adUnits || []; + adUnits.forEach(adUnit => { + const ortb2Imp = deepAccess(adUnit, 'ortb2Imp'); + // A divId is required to compute the slot position and later to track viewability. + // If nothing has been explicitly set, we try to get the divId from the GPT slot and fallback to the adUnit code in last resort. + if (!deepAccess(ortb2Imp, 'ext.data.divId')) { + const divId = getGptSlotInfoForAdUnitCode(adUnit.code).divId; + deepSetValue(ortb2Imp, `ext.data.divId`, divId || adUnit.code); + } + + const slotPosition = getSlotPosition(adUnit); + deepSetValue(ortb2Imp, `ext.data.adg_rtd.adunit_position`, slotPosition); + + // We expect `pagetype` `category` are defined in FPD `ortb2.site.ext.data` object. + // `placement` is expected in FPD `adUnits[].ortb2Imp.ext.data` object. (Please note that this `placement` is not related to the oRTB video property.) + // Btw, we have to ensure compatibility with publishers that use the "legacy" adagio params at the adUnit.params level. + const adagioBid = adUnit.bids.find(bid => _internal.isAdagioBidder(bid.bidder)); + if (adagioBid) { + // ortb2 level + let mustWarnOrtb2 = false; + if (!deepAccess(ortb2Site, 'ext.data.pagetype') && adagioBid.params.pagetype) { + deepSetValue(ortb2Site, 'ext.data.pagetype', adagioBid.params.pagetype); + mustWarnOrtb2 = true; + } + if (!deepAccess(ortb2Site, 'ext.data.category') && adagioBid.params.category) { + deepSetValue(ortb2Site, 'ext.data.category', adagioBid.params.category); + mustWarnOrtb2 = true; + } + + // ortb2Imp level + let mustWarnOrtb2Imp = false; + if (!deepAccess(ortb2Imp, 'ext.data.placement')) { + if (adagioBid.params.placement) { + deepSetValue(ortb2Imp, 'ext.data.placement', adagioBid.params.placement); + mustWarnOrtb2Imp = true; + } else { + // If the placement is not defined, we fallback to the adUnit code. + deepSetValue(ortb2Imp, 'ext.data.placement', adUnit.code); + } + } + + if (mustWarnOrtb2) { + logWarn('`pagetype` and `category` must be defined in the FPD `ortb2.site.ext.data` object. Relying on `adUnits[].bids.adagio.params` is deprecated.'); + } + if (mustWarnOrtb2Imp) { + logWarn('`placement` must be defined in the FPD `adUnits[].ortb2Imp.ext.data` object. Relying on `adUnits[].bids.adagio.params` is deprecated.'); + } + } + }); + + callback(); +} + +export const adagioRtdSubmodule = { + name: SUBMODULE_NAME, + gvlid: GVLID, + init: init, + getBidRequestData: onGetBidRequestData, + onBidRequestEvent: onBidRequest, +}; + +submodule('realTimeData', adagioRtdSubmodule); + +// --- +// +// internal functions moved from adagioBidAdapter.js to adagioRtdProvider.js. +// +// Several of these functions could be redistribued in Prebid.js core or in a library +// +// --- + +/** + * storeRequestInAdagioNS store ad-units in the ADAGIO namespace for further usage. + * Not all the properties are stored, only the ones that are useful for adagio.js. + * + * @param {*} bid - The bid object. Correspond to the bidRequest.bids[i] object. + * @param {*} config - The RTD module configuration. + * @returns {void} + */ +function storeRequestInAdagioNS(bid, config) { + try { + const { bidder, adUnitCode, mediaTypes, params, auctionId, bidderRequestsCount, ortb2, ortb2Imp } = bid; + + const { organizationId, site } = config.params; + + const ortb2Data = deepAccess(ortb2, 'site.ext.data', {}); + const ortb2ImpData = deepAccess(ortb2Imp, 'ext.data', {}); + + // TODO: `bidderRequestsCount` must be incremented with s2s context, actually works only for `client` context + // see: https://github.com/prebid/Prebid.js/pull/11295/files#diff-d5c9b255c545e5097d1cd2f49e7dad309b731e34d788f9c28432ad43ebcd7785L114 + const data = { + bidder, + adUnitCode, + mediaTypes, + params, + auctionId, + bidderRequestsCount, + ortb2: ortb2Data, + ortb2Imp: ortb2ImpData, + localPbjs: '$$PREBID_GLOBAL$$', + localPbjsRef: getGlobal(), + organizationId, + site + }; + + _internal.getAdagioNs().queue.push({ + action: 'store', + ts: Date.now(), + data + }); + } catch (e) { + logError(e); + } +} + +function getElementFromTopWindow(element, currentWindow) { + try { + if (getWindowTop() === currentWindow) { + if (!element.getAttribute('id')) { + element.setAttribute('id', `adg-${getUniqueIdentifierStr()}`); + } + return element; + } else { + const frame = currentWindow.frameElement; + const frameClientRect = frame.getBoundingClientRect(); + const elementClientRect = element.getBoundingClientRect(); + + if (frameClientRect.width !== elementClientRect.width || frameClientRect.height !== elementClientRect.height) { + return false; + } + + return getElementFromTopWindow(frame, currentWindow.parent); + } + } catch (err) { + logWarn(err); + return false; + } +}; + +function getSlotPosition(adUnit) { + if (!isSafeFrameWindow() && !canAccessWindowTop()) { + return ''; + } + + const position = { x: 0, y: 0 }; + + if (isSafeFrameWindow()) { + const ws = getWindowSelf(); + + const sfGeom = (typeof ws.$sf.ext.geom === 'function') ? ws.$sf.ext.geom() : null; + + if (!sfGeom || !sfGeom.self) { + return ''; + } + + position.x = Math.round(sfGeom.self.t); + position.y = Math.round(sfGeom.self.l); + } else { + try { + // window.top based computing + const wt = getWindowTop(); + const d = wt.document; + const adUnitElementId = deepAccess(adUnit, 'ortb2Imp.ext.data.divId'); + + let domElement; + + if (inIframe() === true) { + const ws = getWindowSelf(); + const currentElement = ws.document.getElementById(adUnitElementId); + domElement = getElementFromTopWindow(currentElement, ws); + } else { + domElement = wt.document.getElementById(adUnitElementId); + } + + if (!domElement) { + return ''; + } + + let box = domElement.getBoundingClientRect(); + + const docEl = d.documentElement; + const body = d.body; + const clientTop = d.clientTop || body.clientTop || 0; + const clientLeft = d.clientLeft || body.clientLeft || 0; + const scrollTop = wt.pageYOffset || docEl.scrollTop || body.scrollTop; + const scrollLeft = wt.pageXOffset || docEl.scrollLeft || body.scrollLeft; + + const elComputedStyle = wt.getComputedStyle(domElement, null); + const mustDisplayElement = elComputedStyle.display === 'none'; + + if (mustDisplayElement) { + logWarn('The element is hidden. The slot position cannot be computed.'); + } + + position.x = Math.round(box.left + scrollLeft - clientLeft); + position.y = Math.round(box.top + scrollTop - clientTop); + } catch (err) { + logError(err); + return ''; + } + } + + return `${position.x}x${position.y}`; +} + +function getPageDimensions() { + if (isSafeFrameWindow() || !canAccessWindowTop()) { + return ''; + } + + // the page dimension can be computed on window.top only. + const wt = getWindowTop(); + const body = wt.document.querySelector('body'); + + if (!body) { + return ''; + } + const html = wt.document.documentElement; + const pageWidth = Math.max(body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth); + const pageHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight); + + return `${pageWidth}x${pageHeight}`; +} + +function getViewPortDimensions() { + if (!isSafeFrameWindow() && !canAccessWindowTop()) { + return ''; + } + + const viewportDims = { w: 0, h: 0 }; + + if (isSafeFrameWindow()) { + const ws = getWindowSelf(); + + const sfGeom = (typeof ws.$sf.ext.geom === 'function') ? ws.$sf.ext.geom() : null; + + if (!sfGeom || !sfGeom.win) { + return ''; + } + + viewportDims.w = Math.round(sfGeom.win.w); + viewportDims.h = Math.round(sfGeom.win.h); + } else { + // window.top based computing + const wt = getWindowTop(); + viewportDims.w = wt.innerWidth; + viewportDims.h = wt.innerHeight; + } + + return `${viewportDims.w}x${viewportDims.h}`; +} + +function getTimestampUTC() { + // timestamp returned in seconds + return Math.floor(new Date().getTime() / 1000) - new Date().getTimezoneOffset() * 60; +} + +function getDomLoadingDuration() { + const w = (canAccessWindowTop()) ? getWindowTop() : getWindowSelf(); + const performance = w.performance; + + let domLoadingDuration = -1; + + if (performance && performance.timing && performance.timing.navigationStart > 0) { + const val = performance.timing.domLoading - performance.timing.navigationStart; + if (val > 0) { + domLoadingDuration = val; + } + } + + return domLoadingDuration; +} + +/** + * registerEventsForAdServers bind adagio listeners to ad-server events. + * Theses events are used to track the viewability and attention. + * + * @param {*} config + * @returns {void} + */ +function registerEventsForAdServers(config) { + const GPT_EVENTS = new Set([ + 'impressionViewable', + 'slotRenderEnded', + 'slotVisibilityChanged', + ]); + + const SAS_EVENTS = new Set([ + 'noad', + 'setHeaderBiddingWinner', + ]); + + const AST_EVENTS = new Set([ + 'adLoaded', + ]); + + // Listen to ad-server events in current window + // as we can be safe in a Post-Bid scenario. + const ws = getWindowSelf(); + + // Keep a reference to the window on which the listener is attached. + // this is used to avoid to bind event several times. + if (!Array.isArray(_internal.getAdagioNs().windows)) { + _internal.getAdagioNs().windows = []; + } + + let selfStoredWindow = _internal.getAdagioNs().windows.find(_w => _w.self === ws); + if (!selfStoredWindow) { + selfStoredWindow = { self: ws }; + _internal.getAdagioNs().windows.push(selfStoredWindow); + } + + const register = (namespace, command, selfWindow, adserver, cb) => { + try { + if (selfWindow.adserver === adserver) { + return; + } + ws[namespace] = ws[namespace] || {}; + ws[namespace][command] = ws[namespace][command] || []; + cb(); + } catch (e) { + logError(e); + } + }; + + register('googletag', 'cmd', ws, 'gpt', () => { + ws.googletag.cmd.push(() => { + GPT_EVENTS.forEach(eventName => { + ws.googletag.pubads().addEventListener(eventName, (args) => { + _internal.getAdagioNs().queue.push({ + action: 'gpt-event', + data: { eventName, args, _window: ws }, + ts: Date.now(), + }); + }); + }); + selfStoredWindow.adserver = 'gpt'; + }); + }); + + register('sas', 'cmd', ws, 'sas', () => { + ws.sas.cmd.push(() => { + SAS_EVENTS.forEach(eventName => { + ws.sas.events.on(eventName, (args) => { + _internal.getAdagioNs().queue.push({ + action: 'sas-event', + data: { eventName, args, _window: ws }, + ts: Date.now(), + }); + }); + }); + selfStoredWindow.adserver = 'sas'; + }); + }); + + // https://learn.microsoft.com/en-us/xandr/seller-tag/on-event + register('apntag', 'anq', ws, 'ast', () => { + ws.apntag.anq.push(() => { + AST_EVENTS.forEach(eventName => { + ws.apntag.onEvent(eventName, () => { + _internal.getAdagioNs().queue.push({ + action: 'ast-event', + data: { eventName, args: arguments, _window: ws }, + ts: Date.now(), + }); + }); + }); + selfStoredWindow.adserver = 'ast'; + }); + }); +}; + +// --- end of internal functions ----- // + +/** + * @typedef {Object} AdagioWindow + * @property {Window} self + * @property {string} adserver - 'gpt', 'sas', 'ast' + */ + +/** + * @typedef {Object} AdagioGlobal + * @property {Object} adUnits + * @property {Array} pbjsAdUnits + * @property {Array} queue + * @property {Array} windows + */ + +/** + * @typedef {Object} Session + * @property {boolean} new - True if the session is new. + * @property {number} rnd - Random number used to determine if the session is new. + * @property {number} vwSmplg - View sampling rate. + * @property {number} vwSmplgNxt - Next view sampling rate. + * @property {number} lastActivityTime - Last activity time. + */ + +/** + * @typedef {Object} SessionData + * @property {Session} session - the session data. + */ + +/** + * @typedef {Object} Features + * @property {boolean} initialized - True if the features are initialized. + * @property {Object} data - the features data. + */ diff --git a/modules/adagioRtdProvider.md b/modules/adagioRtdProvider.md new file mode 100644 index 00000000000..f05521ec54a --- /dev/null +++ b/modules/adagioRtdProvider.md @@ -0,0 +1,37 @@ +# Overview + +Module Name: Adagio Rtd Provider +Module Type: Rtd Provider +Maintainer: dev@adagio.io + +# Description + +This module is exclusively used in combination with Adagio Bidder Adapter (SSP) and/or with Adagio prebid server endpoint, and mandatory for Adagio customers. +It computes and collects data required to leverage Adagio viewability and attention prediction engine. + +Features are computed for the Adagio bidder only and placed into `ortb2.ext` and `AdUnit.ortb2Imp.ext.data`. + +To collect data, an external script is loaded by the provider. +It relies on the listening of ad-server events. +Supported ad-servers are GAM, Smart Ad Server, Xandr. Custom ad-server can also be used, +please contact [contact@adagio.io](contact@adagio.io) for more information. + +# Integration + +```bash +gulp build --modules=adagioBidAdapter,rtdModule,adagioRtdProvider +``` + +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders:[{ + name: 'adagio', + params: { + organizationId: '1000' // Required. Provided by Adagio + site: 'my-site' // Required. Provided by Adagio + } + }] + } +}); +``` diff --git a/test/spec/modules/adagioRtdProvider_spec.js b/test/spec/modules/adagioRtdProvider_spec.js new file mode 100644 index 00000000000..2c1612f2e83 --- /dev/null +++ b/test/spec/modules/adagioRtdProvider_spec.js @@ -0,0 +1,532 @@ +import { adagioRtdSubmodule, _internal, storage } from 'modules/adagioRtdProvider.js'; +import * as utils from 'src/utils.js'; +import { loadExternalScript } from '../../../src/adloader.js'; +import { expect } from 'chai'; +import { getGlobal } from '../../../src/prebidGlobal.js'; + +describe('Adagio Rtd Provider', function () { + const SUBMODULE_NAME = 'adagio'; + + function getElementByIdMock(width, height, x, y) { + const obj = { + x: x || 800, + y: y || 300, + width: width || 300, + height: height || 250, + }; + + return { + ...obj, + getBoundingClientRect: () => { + return { + width: obj.width, + height: obj.height, + left: obj.x, + top: obj.y, + right: obj.x + obj.width, + bottom: obj.y + obj.height + }; + } + }; + } + + let sandbox; + let clock; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + clock = sandbox.useFakeTimers(); + }); + + afterEach(function () { + clock.restore(); + sandbox.restore(); + }); + + describe('submodule `init`', function () { + const config = { + name: SUBMODULE_NAME, + params: { + organizationId: '1000', + site: 'mysite' + } + }; + + it('exists', function () { + expect(adagioRtdSubmodule.init).to.be.a('function'); + }); + + it('returns false missing config params', function () { + const value = adagioRtdSubmodule.init({ + name: SUBMODULE_NAME, + }); + expect(value).to.equal(false); + }); + + it('returns false if missing providers param', function () { + const value = adagioRtdSubmodule.init({ + name: SUBMODULE_NAME, + params: {} + }); + expect(value).to.equal(false); + }); + + it('returns false if organizationId param is not a string', function () { + const value = adagioRtdSubmodule.init({ + name: SUBMODULE_NAME, + params: { + organizationId: 1000, + site: 'mysite' + } + }); + expect(value).to.equal(false); + }); + + it('returns false if `site` param is not a string', function () { + const value = adagioRtdSubmodule.init({ + name: SUBMODULE_NAME, + params: { + organizationId: '1000', + site: 123 + } + }); + expect(value).to.equal(false); + }); + + it('returns true if `organizationId` and `site` params included', function () { + const value = adagioRtdSubmodule.init(config); + expect(value).to.equal(true); + }); + + it('load an external script if localStorageIsEnabled is enabled', function () { + sandbox.stub(storage, 'localStorageIsEnabled').callsArgWith(0, true) + adagioRtdSubmodule.init(config); + expect(loadExternalScript.called).to.be.true; + }); + + it('do not load an external script if localStorageIsEnabled is disabled', function () { + sandbox.stub(storage, 'localStorageIsEnabled').callsArgWith(0, false) + adagioRtdSubmodule.init(config); + expect(loadExternalScript.called).to.be.false; + }); + + describe('store session data in localStorage', function () { + const session = { + lastActivityTime: 1714116520700, + rnd: 0.5697, + vwSmplg: 0.1, + vwSmplgNxt: 0.1 + }; + + it('store new session data for further usage', function () { + const storageValue = null; + sandbox.stub(storage, 'getDataFromLocalStorage').callsArgWith(1, storageValue); + sandbox.stub(Date, 'now').returns(1714116520710); + sandbox.stub(Math, 'random').returns(0.8); + + const spy = sandbox.spy(_internal.getAdagioNs().queue, 'push') + + adagioRtdSubmodule.init(config); + + const expected = { + session: { + new: true, + rnd: Math.random() + } + } + + expect(spy.withArgs({ + action: 'session', + ts: Date.now(), + data: expected, + }).calledOnce).to.be.true; + }); + + it('store existing session data for further usage', function () { + const storageValue = JSON.stringify({session: session}); + sandbox.stub(storage, 'getDataFromLocalStorage').callsArgWith(1, storageValue); + sandbox.stub(Date, 'now').returns(1714116520710); + sandbox.stub(Math, 'random').returns(0.8); + + const spy = sandbox.spy(_internal.getAdagioNs().queue, 'push') + + adagioRtdSubmodule.init(config); + + const expected = { + session: { + ...session, + new: false, + } + } + + expect(spy.withArgs({ + action: 'session', + ts: Date.now(), + data: expected, + }).calledOnce).to.be.true; + }); + + it('store new session if old session has expired data for further usage', function () { + const storageValue = JSON.stringify({session: session}); + sandbox.stub(Date, 'now').returns(1715679344351); + sandbox.stub(storage, 'getDataFromLocalStorage').callsArgWith(1, storageValue); + sandbox.stub(Math, 'random').returns(0.8); + + const spy = sandbox.spy(_internal.getAdagioNs().queue, 'push') + + adagioRtdSubmodule.init(config); + + const expected = { + session: { + ...session, + new: true, + rnd: Math.random(), + } + } + + expect(spy.withArgs({ + action: 'session', + ts: Date.now(), + data: expected, + }).calledOnce).to.be.true; + }); + }); + }); + + describe('submodule `getBidRequestData`', function () { + const bidReqConfig = { + 'timeout': 700, + 'adUnits': [ + { + 'code': 'div-gpt-ad-1460505748561-0', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'ortb2Imp': {}, + 'bids': [ + { + 'bidder': 'adagio', + 'params': { + 'organizationId': '1004', + 'site': 'maville', + 'useAdUnitCodeAsPlacement': true, + 'adUnitElementId': 'div-gpt-ad-1460505748561-0', + 'pagetype': 'article', + } + }, + { + 'bidder': 'another', + 'params': { + 'pubid': 'xxx', + } + } + ] + } + ], + 'adUnitCodes': [ + 'div-gpt-ad-1460505748561-0' + ], + 'ortb2Fragments': { + 'global': { + 'regs': { + 'ext': { + 'gdpr': 1 + } + }, + 'site': { + 'domain': 'example.com', + 'publisher': { + 'domain': 'example.com' + }, + 'page': 'http://example.com/page.html', + }, + 'device': { + 'w': 1359, + 'h': 1253, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'fr' + } + }, + 'bidder': {} + } + }; + + function cb() {} + + beforeEach(function() { + _internal.getFeatures().reset(); + }); + + it('exists', function () { + expect(adagioRtdSubmodule.getBidRequestData).to.be.a('function'); + }); + + it('update the ortb2Fragments object with adg_rtd signals', function() { + const bidRequest = utils.deepClone(bidReqConfig); + + sandbox.stub(window.top.document, 'getElementById').returns(getElementByIdMock()); + sandbox.stub(window.top, 'getComputedStyle').returns({ display: 'block' }); + sandbox.stub(utils, 'inIframe').returns(false); + + adagioRtdSubmodule.getBidRequestData(bidRequest, cb); + const signals = bidRequest.ortb2Fragments.global.site.ext.data.adg_rtd; + expect(signals).to.have.property('features'); + expect(signals).to.have.property('session'); + expect(signals).to.have.property('uid'); + expect(signals.features.viewport_dimensions).to.match(/\d+x\d+/); + expect(signals.features.page_dimensions).to.match(/\d+x\d+/); + + expect(bidRequest.adUnits[0]).to.have.property('ortb2Imp'); + expect(bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd.adunit_position).to.match(/\d+x\d+/); + }); + + describe('update the ortb2Fragments object a SafeFrame context', function() { + it('update', function() { + sandbox.stub(utils, 'isSafeFrameWindow').returns(true); + sandbox.stub(utils, 'canAccessWindowTop').returns(false); + + window.$sf = { + ext: { + geom() { + return { + win: {t: 23, r: 1920, b: 1200, l: 0, w: 1920, h: 1177}, + self: {t: 210, r: 1159, b: 460, l: 859, w: 300, h: 250}, + } + } + } + }; + + const bidRequest = utils.deepClone(bidReqConfig); + adagioRtdSubmodule.getBidRequestData(bidRequest, cb); + + const fragmentExt = bidRequest.ortb2Fragments.global.site.ext.data.adg_rtd; + expect(fragmentExt.features.viewport_dimensions).equal('1920x1177'); + expect(fragmentExt.features.page_dimensions).equal(''); + + const ortb2ImpExt = bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd; + expect(ortb2ImpExt.adunit_position).equal('210x859'); + + window.$sf = undefined; + }); + + it('handle missformated $sf object and update', function() { + sandbox.stub(utils, 'isSafeFrameWindow').returns(true); + sandbox.stub(utils, 'canAccessWindowTop').returns(false); + + window.$sf = { + ext: { + geom: '' + } + }; + + const bidRequest = utils.deepClone(bidReqConfig); + adagioRtdSubmodule.getBidRequestData(bidRequest, cb); + + const fragmentExt = bidRequest.ortb2Fragments.global.site.ext.data.adg_rtd; + expect(fragmentExt.features.viewport_dimensions).equal(''); + expect(fragmentExt.features.page_dimensions).equal(''); + + const ortb2ImpExt = bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd; + expect(ortb2ImpExt.adunit_position).equal(''); + + window.$sf = undefined; + }); + }); + + describe('update the ortb2Fragments object in a "inIframe" context', function() { + it('update when window.top is accessible', function() { + sandbox.stub(utils, 'canAccessWindowTop').returns(true); + sandbox.stub(utils, 'isSafeFrameWindow').returns(false); + sandbox.stub(utils, 'inIframe').returns(true); + + const bidRequest = utils.deepClone(bidReqConfig); + adagioRtdSubmodule.getBidRequestData(bidRequest, cb); + + const ortb2ImpExt = bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd; + expect(ortb2ImpExt.adunit_position).equal(''); + }); + + it('catch error when window.top is accessible', function() { + sandbox.stub(utils, 'canAccessWindowTop').returns(true); + sandbox.stub(utils, 'isSafeFrameWindow').returns(false); + sandbox.stub(window.document, 'getElementById').throws(); + + const bidRequest = utils.deepClone(bidReqConfig); + adagioRtdSubmodule.getBidRequestData(bidRequest, cb); + + const ortb2ImpExt = bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd; + expect(ortb2ImpExt.adunit_position).equal(''); + }); + }); + + it('update the ortb2Fragments object when window.top is not accessible', function() { + sandbox.stub(utils, 'canAccessWindowTop').returns(false); + sandbox.stub(utils, 'isSafeFrameWindow').returns(false); + + const bidRequest = utils.deepClone(bidReqConfig); + adagioRtdSubmodule.getBidRequestData(bidRequest, cb); + + const fragmentExt = bidRequest.ortb2Fragments.global.site.ext.data.adg_rtd; + expect(fragmentExt.features.viewport_dimensions).equal(''); + expect(fragmentExt.features.page_dimensions).equal(''); + + const ortb2ImpExt = bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd; + expect(ortb2ImpExt.adunit_position).equal(''); + }); + }); + + describe('submodule `onBidRequestEvent`', function() { + const bidderRequest = { + 'bidderCode': 'adagio', + 'auctionId': '3de10dc0-fe75-480f-95cc-f15f2c4929fe', + 'bidderRequestId': '4ecd1f17cf829b', + 'bids': [ + { + 'bidder': 'adagio', + 'params': { + 'organizationId': '1000', + 'site': 'example', + 'adUnitElementId': 'div-gpt-ad-1460505748561-0', + 'pagetype': 'article', + 'environment': 'desktop', + 'placement': 'div-gpt-ad-1460505748561-0', + 'adagioAuctionId': '4c259968-0158-443d-af93-551bac594b6c', + 'pageviewId': 'dfb9b067-e5c4-4212-97bb-c67d6313ecaf' + }, + 'ortb2Imp': { + 'ext': { + 'tid': '235c991e-fcc4-416b-95d3-f60e53575bee', + 'data': { + 'adserver': { + 'name': 'gam', + 'adslot': '/19968336/header-bid-tag-0' + }, + 'pbadslot': '/19968336/header-bid-tag-0', + 'adunit_position': '8x95' + }, + 'gpid': '/19968336/header-bid-tag-0' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '235c991e-fcc4-416b-95d3-f60e53575bee', + 'adUnitId': '79ab5904-0b21-4235-965a-f4905af072b7', + 'bidId': '534aa529a44e0e', + 'bidderRequestId': '4ecd1f17cf829b', + 'auctionId': '3de10dc0-fe75-480f-95cc-f15f2c4929fe', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'ortb2': { + 'site': { + 'ext': { + 'data': { + 'adg_rtd': { + 'uid': 'dfb9b067-e5c4-4212-97bb-c67d6313ecaf', + 'features': { + 'page_dimensions': '1359x1353', + 'viewport_dimensions': '1359x1253', + 'user_timestamp': '1715621032', + 'dom_loading': '28' + }, + 'session': { + 'new': true, + 'rnd': 0.020644826280300954, + 'vwSmplg': 0.1, + 'vwSmplgNxt': 0.1 + } + } + } + } + } + }, + }, + ], + 'auctionStart': 1715613832791, + 'timeout': 700, + 'ortb2': { + 'site': { + 'ext': { + 'data': { + 'adg_rtd': { + 'features': { + 'page_dimensions': '1359x1353', + 'viewport_dimensions': '1359x1253', + 'user_timestamp': '1715621032', + 'dom_loading': '28' + }, + 'session': { + 'new': true, + 'rnd': 0.020644826280300954, + 'vwSmplg': 0.1, + 'vwSmplgNxt': 0.1 + } + } + } + } + } + }, + 'start': 1715613832796 + } + + it('store a copy of computed property', function() { + const spy = sandbox.spy(_internal.getAdagioNs().queue, 'push') + sandbox.stub(Date, 'now').returns(12345); + + _internal.getGuard().clear(); + + const config = { + params: { + organizationId: '1000', + site: 'example' + } + }; + const bidderRequestCopy = utils.deepClone(bidderRequest); + adagioRtdSubmodule.onBidRequestEvent(bidderRequestCopy, config); + + clock.tick(1); + + const { + bidder, + adUnitCode, + mediaTypes, + params, + auctionId, + bidderRequestsCount } = bidderRequestCopy.bids[0]; + + const expected = { + bidder, + adUnitCode, + mediaTypes, + ortb2: bidderRequestCopy.bids[0].ortb2.site.ext.data, + ortb2Imp: bidderRequestCopy.bids[0].ortb2Imp.ext.data, + params, + auctionId, + bidderRequestsCount, + organizationId: config.params.organizationId, + site: config.params.site, + localPbjs: 'pbjs', + localPbjsRef: getGlobal() + } + + expect(spy.withArgs({ + action: 'store', + ts: Date.now(), + data: expected, + }).calledOnce).to.be.true; + }); + }); +}); From 8681fb9da3eee495da8bcfb012e84d000a6b9267 Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 29 May 2024 21:55:34 +0200 Subject: [PATCH 42/46] AdagioBidAdapter: update preparation for Rtd module and Prebid.js 9 (#11580) --- modules/adagioBidAdapter.js | 15 +++++----- test/spec/modules/adagioBidAdapter_spec.js | 33 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index e3b25773061..b6ffc9b8d0d 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -667,13 +667,14 @@ function autoFillParams(bid) { bid.params.site = adgGlobalConf.siteId.split(':')[1]; } - // Edge case. Useful when Prebid Manager cannot handle properly params setting… - if (adgGlobalConf.useAdUnitCodeAsPlacement === true || bid.params.useAdUnitCodeAsPlacement === true) { + // `useAdUnitCodeAsPlacement` is an edge case. Useful when a Prebid Manager cannot handle properly params setting. + // In Prebid.js 9, `placement` should be defined in ortb2Imp and the `useAdUnitCodeAsPlacement` param should be removed + bid.params.placement = deepAccess(bid, 'ortb2Imp.ext.data.placement', bid.params.placement); + if (!bid.params.placement && (adgGlobalConf.useAdUnitCodeAsPlacement === true || bid.params.useAdUnitCodeAsPlacement === true)) { bid.params.placement = bid.adUnitCode; } - bid.params.adUnitElementId = deepAccess(bid, 'ortb2Imp.ext.data.elementId', null) || bid.params.adUnitElementId; - + bid.params.adUnitElementId = deepAccess(bid, 'ortb2Imp.ext.data.divId', bid.params.adUnitElementId); if (!bid.params.adUnitElementId) { if (adgGlobalConf.useAdUnitCodeAsAdUnitElementId === true || bid.params.useAdUnitCodeAsAdUnitElementId === true) { bid.params.adUnitElementId = bid.adUnitCode; @@ -959,14 +960,14 @@ const OUTSTREAM_RENDERER = { * @returns */ const _getFeatures = (bidRequest) => { - const f = { ...deepAccess(bidRequest, 'ortb2.ext.features', GlobalExchange.getOrSetGlobalFeatures()) } || {}; + const f = { ...deepAccess(bidRequest, 'ortb2.site.ext.data.adg_rtd.features', GlobalExchange.getOrSetGlobalFeatures()) } || {}; f.print_number = deepAccess(bidRequest, 'bidderRequestsCount', 1).toString(); if (f.type === 'bidAdapter') { f.adunit_position = getSlotPosition(bidRequest.params.adUnitElementId) } else { - f.adunit_position = deepAccess(bidRequest, 'ortb2Imp.ext.data.adunit_position'); + f.adunit_position = deepAccess(bidRequest, 'ortb2Imp.ext.data.adg_rtd.adunit_position'); } Object.keys(f).forEach((prop) => { @@ -1019,7 +1020,7 @@ export const spec = { // We don't validate the dsa object in adapter and let our server do it. const dsa = deepAccess(bidderRequest, 'ortb2.regs.ext.dsa'); - let rtdSamplingSession = deepAccess(bidderRequest, 'ortb2.ext.session'); + let rtdSamplingSession = deepAccess(bidderRequest, 'ortb2.site.ext.data.adg_rtd.session'); const dataExchange = (rtdSamplingSession) ? { session: rtdSamplingSession } : GlobalExchange.getExchangeData(); const aucId = generateUUID() diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index 1371c97dddf..ec8486f62ad 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -349,6 +349,39 @@ describe('Adagio bid adapter', () => { adagioMock.verify(); }); + describe('with Adagio Rtd Provider', function() { + it('it dont enqueue features from the bidder adapter', function() { + sandbox.stub(adagio, 'hasRtd').returns(true); + const bid01 = new BidRequestBuilder().withParams().build(); + const bidderRequest = new BidderRequestBuilder().build(); + spec.buildRequests([bid01], bidderRequest); + adagioMock.expects('enqueue').withArgs(sinon.match({ action: 'features' })).never(); + adagioMock.verify(); + }); + + it('get feature from ortb2', function() { + sandbox.stub(adagio, 'hasRtd').returns(true); + const bid01 = new BidRequestBuilder().withParams().build(); + bid01.ortb2Imp = { + ext: { data: {adg_rtd: {adunit_position: '1x1'}} } + }; + bid01.ortb2 = { + site: { + ext: + { + data: { + adg_rtd: { features: {} } + } + } + } + }; + const bidderRequest = new BidderRequestBuilder().build(); + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests[0].data.adUnits[0].features).to.exist; + expect(requests[0].data.adUnits[0].features.adunit_position).to.equal('1x1'); + }); + }); + it('should filter some props in case refererDetection.reachedTop is false', function() { const bid01 = new BidRequestBuilder().withParams().build(); const bidderRequest = new BidderRequestBuilder({ From b15af76cdcb443bc59de4a2770aff34ccf038d5a Mon Sep 17 00:00:00 2001 From: sebastienrufiange <131205907+sebastienrufiange@users.noreply.github.com> Date: Wed, 29 May 2024 15:58:03 -0400 Subject: [PATCH 43/46] Contxtful RTD Provider : add ORTB2 support (#11497) * contxtfulRtdProvider: update the Contxtful Rtd module * contxtfulRtdProvider: update the GPT example * contxtfulRtdProvider: update the tests * contxtfulRtdProvider: refresh the documentation * contxtfulRtdProvider: fix getTargetingData unit test * contxtfulRtdProvider: revert out-of-scope changes * contxtfulRtdProvider: revert out-of-scope changes * contxtfulRtdProvider: sync docs with content on prebid.github.io * contxtfulRtdProvider: improve header style * contxtfulRtdProvider: improve sentences * contxtfulRtdProvider: remove spurious text * contxtfulRtdProvider: trigger build * contxtfulRtdProvider: fix multi-line row in .md file --------- Co-authored-by: Sebastien Boisvert --- .../gpt/contxtfulRtdProvider_example.html | 261 +++++++--- modules/contxtfulRtdProvider.js | 221 +++++++-- modules/contxtfulRtdProvider.md | 73 ++- .../spec/modules/contxtfulRtdProvider_spec.js | 453 ++++++++++++++++-- 4 files changed, 825 insertions(+), 183 deletions(-) diff --git a/integrationExamples/gpt/contxtfulRtdProvider_example.html b/integrationExamples/gpt/contxtfulRtdProvider_example.html index 29284de81a2..e47bd4142f9 100644 --- a/integrationExamples/gpt/contxtfulRtdProvider_example.html +++ b/integrationExamples/gpt/contxtfulRtdProvider_example.html @@ -1,91 +1,212 @@ + - - - - + +Contxtful Rtd Provider Example + - +googletag.cmd.push(function () { + googletag + .defineSlot('/19968336/header-bid-tag-1', + div2Sizes, 'div-2') + .addService(googletag.pubads()); + googletag.pubads().enableSingleRequest(); + googletag.enableServices(); +}); + + -

Contxtful RTD Provider

-
- - +

Contxtful Rtd Provider Example

+ +

+ +
Div-1
+
+ +
+ +
+ +
Div-2
+
+ +
+ + diff --git a/modules/contxtfulRtdProvider.js b/modules/contxtfulRtdProvider.js index 6d4b2a2ce29..e2bb1f3b909 100644 --- a/modules/contxtfulRtdProvider.js +++ b/modules/contxtfulRtdProvider.js @@ -1,8 +1,8 @@ /** - * Contxtful Technologies Inc - * This RTD module provides receptivity feature that can be accessed using the - * getReceptivity() function. The value returned by this function enriches the ad-units - * that are passed within the `getTargetingData` functions and GAM. + * Contxtful Technologies Inc. + * This RTD module provides receptivity that can be accessed using the + * getTargetingData and getBidRequestData functions. The receptivity enriches ad units + * and bid requests. */ import { submodule } from '../src/hook.js'; @@ -10,8 +10,11 @@ import { logInfo, logError, isStr, + mergeDeep, isEmptyStr, + isEmpty, buildUrl, + isArray, } from '../src/utils.js'; import { loadExternalScript } from '../src/adloader.js'; @@ -20,8 +23,55 @@ const MODULE = `${MODULE_NAME}RtdProvider`; const CONTXTFUL_RECEPTIVITY_DOMAIN = 'api.receptivity.io'; -let initialReceptivity = null; -let contxtfulModule = null; +let rxApi = null; +let isFirstBidRequestCall = true; + +/** + * Return current receptivity value for the requester. + * @param { String } requester + * @return { { Object } } + */ +function getRxEngineReceptivity(requester) { + return rxApi?.receptivity(requester); +} + +function loadSessionReceptivity(requester) { + let sessionStorageValue = sessionStorage.getItem(requester); + if (!sessionStorageValue) { + return null; + } + + try { + // Check expiration of the cached value + let sessionStorageReceptivity = JSON.parse(sessionStorageValue); + let expiration = parseInt(sessionStorageReceptivity?.exp); + if (expiration < new Date().getTime()) { + return null; + } + + let rx = sessionStorageReceptivity?.rx; + return rx; + } catch { + return null; + } +}; + +/** + * Prepare a receptivity batch + * @param {Array.} requesters + * @param {Function} method + * @returns A batch + */ +function prepareBatch(requesters, method) { + return requesters.reduce((acc, requester) => { + const receptivity = method(requester); + if (!isEmpty(receptivity)) { + return { ...acc, [requester]: receptivity }; + } else { + return acc; + } + }, {}); +} /** * Init function used to start sub module @@ -30,11 +80,10 @@ let contxtfulModule = null; */ function init(config) { logInfo(MODULE, 'init', config); - initialReceptivity = null; - contxtfulModule = null; + rxApi = null; try { - const {version, customer, hostname} = extractParameters(config); + const { version, customer, hostname } = extractParameters(config); initCustomer(version, customer, hostname); return true; } catch (error) { @@ -51,7 +100,7 @@ function init(config) { * @return { { version: String, customer: String, hostname: String } } * @throws params.{name} should be a non-empty string */ -function extractParameters(config) { +export function extractParameters(config) { const version = config?.params?.version; if (!isStr(version) || isEmptyStr(version)) { throw Error(`${MODULE}: params.version should be a non-empty string`); @@ -64,7 +113,7 @@ function extractParameters(config) { const hostname = config?.params?.hostname || CONTXTFUL_RECEPTIVITY_DOMAIN; - return {version, customer, hostname}; + return { version, customer, hostname }; } /** @@ -78,73 +127,145 @@ function initCustomer(version, customer, hostname) { const CONNECTOR_URL = buildUrl({ protocol: 'https', host: hostname, - pathname: `/${version}/prebid/${customer}/connector/p.js`, + pathname: `/${version}/prebid/${customer}/connector/rxConnector.js`, }); const externalScript = loadExternalScript(CONNECTOR_URL, MODULE_NAME); - addExternalScriptEventListener(externalScript); + addExternalScriptEventListener(externalScript, customer); } /** * Add event listener to the script tag for the expected events from the external script. * @param { HTMLScriptElement } script */ -function addExternalScriptEventListener(script) { - if (!script) { - return; - } - - script.addEventListener('initialReceptivity', ({ detail }) => { - let receptivityState = detail?.ReceptivityState; - if (isStr(receptivityState) && !isEmptyStr(receptivityState)) { - initialReceptivity = receptivityState; +function addExternalScriptEventListener(script, tagId) { + script.addEventListener( + 'rxConnectorIsReady', + async ({ detail: rxConnector }) => { + // Fetch the customer configuration + const { rxApiBuilder, fetchConfig } = rxConnector; + let config = await fetchConfig(tagId); + if (!config) { + return; + } + rxApi = await rxApiBuilder(config); } - }); - - script.addEventListener('rxEngineIsReady', ({ detail: api }) => { - contxtfulModule = api; - }); -} - -/** - * Return current receptivity. - * @return { { ReceptivityState: String } } - */ -function getReceptivity() { - return { - ReceptivityState: contxtfulModule?.GetReceptivity()?.ReceptivityState || initialReceptivity - }; + ); } /** * Set targeting data for ad server * @param { [String] } adUnits - * @param {*} _config + * @param {*} config * @param {*} _userConsent - * @return {{ code: { ReceptivityState: String } }} + * @return {{ code: { ReceptivityState: String } }} */ -function getTargetingData(adUnits, _config, _userConsent) { - logInfo(MODULE, 'getTargetingData'); - if (!adUnits) { +function getTargetingData(adUnits, config, _userConsent) { + try { + if (String(config?.params?.adServerTargeting) === 'false') { + return {}; + } + logInfo(MODULE, 'getTargetingData'); + + const requester = config?.params?.customer; + const rx = getRxEngineReceptivity(requester) || + loadSessionReceptivity(requester) || {}; + if (isEmpty(rx)) { + return {}; + } + + return adUnits.reduce((targets, code) => { + targets[code] = rx; + return targets; + }, {}); + } catch (error) { + logError(MODULE, error); return {}; } +} + +/** + * @param {Object} reqBidsConfigObj Bid request configuration object + * @param {Function} onDone Called on completion + * @param {Object} config Configuration for Contxtful RTD module + * @param {Object} userConsent + */ +function getBidRequestData(reqBidsConfigObj, onDone, config, userConsent) { + function onReturn() { + if (isFirstBidRequestCall) { + isFirstBidRequestCall = false; + }; + onDone(); + } + logInfo(MODULE, 'getBidRequestData'); + const bidders = config?.params?.bidders || []; + if (isEmpty(bidders) || !isArray(bidders)) { + onReturn(); + return; + } - const receptivity = getReceptivity(); - if (!receptivity?.ReceptivityState) { + let fromApiBatched = () => rxApi?.receptivityBatched?.(bidders); + let fromApiSingle = () => prepareBatch(bidders, getRxEngineReceptivity); + let fromStorage = () => prepareBatch(bidders, loadSessionReceptivity); + + function tryMethods(methods) { + for (let method of methods) { + try { + let batch = method(); + if (!isEmpty(batch)) { + return batch; + } + } catch (error) { } + } return {}; } + let rxBatch = {}; + try { + if (isFirstBidRequestCall) { + rxBatch = tryMethods([fromStorage, fromApiBatched, fromApiSingle]); + } else { + rxBatch = tryMethods([fromApiBatched, fromApiSingle, fromStorage]) + } + } catch (error) { } - return adUnits.reduce((targets, code) => { - targets[code] = receptivity; - return targets; - }, {}); -} + if (isEmpty(rxBatch)) { + onReturn(); + return; + } + + bidders + .map((bidderCode) => ({ bidderCode, rx: rxBatch[bidderCode] })) + .filter(({ rx }) => !isEmpty(rx)) + .forEach(({ bidderCode, rx }) => { + const ortb2 = { + user: { + data: [ + { + name: MODULE_NAME, + ext: { + rx, + params: { + ev: config.params?.version, + ci: config.params?.customer, + }, + }, + }, + ], + }, + }; + mergeDeep(reqBidsConfigObj.ortb2Fragments?.bidder, { + [bidderCode]: ortb2, + }); + }); + + onReturn(); +}; export const contxtfulSubmodule = { name: MODULE_NAME, init, - extractParameters, getTargetingData, + getBidRequestData, }; submodule('realTimeData', contxtfulSubmodule); diff --git a/modules/contxtfulRtdProvider.md b/modules/contxtfulRtdProvider.md index dfefca2067a..71a641db4ad 100644 --- a/modules/contxtfulRtdProvider.md +++ b/modules/contxtfulRtdProvider.md @@ -2,25 +2,42 @@ **Module Name:** Contxtful RTD Provider **Module Type:** RTD Provider -**Maintainer:** [prebid@contxtful.com](mailto:prebid@contxtful.com) +**Maintainer:** [contact@contxtful.com](mailto:contact@contxtful.com) # Description The Contxtful RTD module offers a unique feature—Receptivity. Receptivity is an efficiency metric, enabling the qualification of any instant in a session in real time based on attention. The core idea is straightforward: the likelihood of an ad’s success increases when it grabs attention and is presented in the right context at the right time. -To utilize this module, you need to register for an account with [Contxtful](https://contxtful.com). For inquiries, please contact [prebid@contxtful.com](mailto:prebid@contxtful.com). - -# Configuration +To utilize this module, you need to register for an account with [Contxtful](https://contxtful.com). For inquiries, please contact [contact@contxtful.com](mailto:contact@contxtful.com). ## Build Instructions To incorporate this module into your `prebid.js`, compile the module using the following command: ```sh -gulp build --modules=contxtfulRtdProvider, +gulp build --modules=rtdModule,contxtfulRtdProvider, +``` + +## Testing + +To run the test server locally: +```sh +gulp serve --modules=rtdModule,contxtfulRtdProvider, --fix --nolint --notest +chrome http://localhost:9999/integrationExamples/gpt/contxtfulRtdProvider_example.html ``` -## Module Configuration +To run the unit tests: + +```bash +gulp test +``` + +To run the unit tests for a particular file: +```bash +gulp test --file "test/spec/modules/contxtfulRtdProvider_spec.js" --nolint +``` + +## Configuration Configure the `contxtfulRtdProvider` by passing the required settings through the `setConfig` function in `prebid.js`. @@ -35,31 +52,53 @@ pbjs.setConfig({ "name": "contxtful", "waitForIt": true, "params": { - "version": "", - "customer": "" + "version": "Contact contact@contxtful.com for the API version", + "customer": "Contact contact@contxtful.com for the customer ID", + "hostname": "api.receptivity.io", // Optional, default: "api.receptivity.io" + "bidders": ["bidderCode1", "bidderCode", "..."], // list of bidders + "adServerTargeting": true, // Optional, default: true } } ] } }); ``` +## Parameters -### Configuration Parameters - -| Name | Type | Scope | Description | -|------------|----------|----------|-------------------------------------------| -| `version` | `string` | Required | Specifies the API version of Contxtful. | -| `customer` | `string` | Required | Your unique customer identifier. | +| Name | Type | Scope | Description | +|---------------------|----------|----------|--------------------------------------------| +| `version` | `String` | Required | Specifies the version of the Contxtful Receptivity API. | +| `customer` | `String` | Required | Your unique customer identifier. | +| `hostname` | `String` | Optional | Target URL for CONTXTFUL external JavaScript file. Default is "api.receptivity.io". Changing default behaviour is not recommended. Please reach out to contact@contxtful.com if you experience issues. | +| `adServerTargeting` | `Boolean`| Optional | Enables the `getTargetingData` to inject targeting value in ad units. Setting to true enables the feature, false disables the feature. Default is true | +| `bidders` | `Array` | Optional | Setting this array enables Receptivity in the `ortb2` object through `getBidRequestData` for all the listed `bidders`. Default is `[]` (an empty array). RECOMMENDED : Add all the bidders active like this `["bidderCode1", "bidderCode", "..."]` | -# Usage +## Usage: Injection in Ad Servers The `contxtfulRtdProvider` module loads an external JavaScript file and authenticates with Contxtful APIs. The `getTargetingData` function then adds a `ReceptivityState` to each ad slot, which can have one of two values: `Receptive` or `NonReceptive`. ```json { "adUnitCode1": { "ReceptivityState": "Receptive" }, - "adUnitCode2": { "ReceptivityState": "NonReceptive" } + "adUnitCode2": { "ReceptivityState": "Receptive" } } ``` -This module also integrates seamlessly with Google Ad Manager, ensuring that the `ReceptivityState` is available as early as possible in the ad serving process. \ No newline at end of file +This module also integrates seamlessly with Google Ad Manager, ensuring that the `ReceptivityState` is available as early as possible in the ad serving process. + +## Usage: Injection in ortb2 for bidders + +Setting the `bidders` field in the configuration parameters enables Receptivity in the `ortb2` object through `getBidRequestData` for all the listed bidders. +On a Bid Request Event, all bidders in the configuration will inherit the Receptivity data through `ortb2` +Default is `[]` (an empty array) + +RECOMMENDED : Add all the bidders active like this `["bidderCode1", "bidderCode", "..."]` + +## Links + +- [Basic Prebid.js Example](https://docs.prebid.org/dev-docs/examples/basic-example.html) +- [How Bid Adapters Should Read First Party Data](https://docs.prebid.org/features/firstPartyData.html#how-bid-adapters-should-read-first-party-data) +- [getBidRequestData](https://docs.prebid.org/dev-docs/add-rtd-submodule.html#getbidrequestdata) +- [getTargetingData](https://docs.prebid.org/dev-docs/add-rtd-submodule.html#gettargetingdata) +- [Contxtful Documentation](https://documentation.contxtful.com/) + diff --git a/test/spec/modules/contxtfulRtdProvider_spec.js b/test/spec/modules/contxtfulRtdProvider_spec.js index 541c0e6e6dd..01e7a242d19 100644 --- a/test/spec/modules/contxtfulRtdProvider_spec.js +++ b/test/spec/modules/contxtfulRtdProvider_spec.js @@ -1,25 +1,40 @@ -import { contxtfulSubmodule } from '../../../modules/contxtfulRtdProvider.js'; +import { contxtfulSubmodule, extractParameters } from '../../../modules/contxtfulRtdProvider.js'; import { expect } from 'chai'; import { loadExternalScriptStub } from 'test/mocks/adloaderStub.js'; - import * as events from '../../../src/events'; -const _ = null; const VERSION = 'v1'; const CUSTOMER = 'CUSTOMER'; -const CONTXTFUL_CONNECTOR_ENDPOINT = `https://api.receptivity.io/${VERSION}/prebid/${CUSTOMER}/connector/p.js`; -const INITIAL_RECEPTIVITY = { ReceptivityState: 'INITIAL_RECEPTIVITY' }; -const INITIAL_RECEPTIVITY_EVENT = new CustomEvent('initialReceptivity', { detail: INITIAL_RECEPTIVITY }); +const CONTXTFUL_CONNECTOR_ENDPOINT = `https://api.receptivity.io/${VERSION}/prebid/${CUSTOMER}/connector/rxConnector.js`; + +const RX_FROM_SESSION_STORAGE = { ReceptivityState: 'Receptive', test_info: 'rx_from_session_storage' }; +const RX_FROM_API = { ReceptivityState: 'Receptive', test_info: 'rx_from_engine' }; + +const RX_API_MOCK = { receptivity: sinon.stub(), }; +const RX_CONNECTOR_MOCK = { + fetchConfig: sinon.stub(), + rxApiBuilder: sinon.stub(), +}; -const CONTXTFUL_API = { GetReceptivity: sinon.stub() } -const RX_ENGINE_IS_READY_EVENT = new CustomEvent('rxEngineIsReady', {detail: CONTXTFUL_API}); +const TIMEOUT = 10; +const RX_CONNECTOR_IS_READY_EVENT = new CustomEvent('rxConnectorIsReady', { detail: RX_CONNECTOR_MOCK }); + +function writeToStorage(requester, timeDiff) { + let rx = RX_FROM_SESSION_STORAGE; + let exp = new Date().getTime() + timeDiff; + let item = { rx, exp, }; + sessionStorage.setItem(requester, JSON.stringify(item),); +} function buildInitConfig(version, customer) { return { name: 'contxtful', params: { - version, - customer, + version: version, + customer: customer, + hostname: 'api.receptivity.io', + bidders: ['mock-bidder-code'], + adServerTargeting: true, }, }; } @@ -33,9 +48,19 @@ describe('contxtfulRtdProvider', function () { loadExternalScriptTag = document.createElement('script'); loadExternalScriptStub.callsFake((_url, _moduleName) => loadExternalScriptTag); - CONTXTFUL_API.GetReceptivity.reset(); + RX_API_MOCK.receptivity.reset(); + RX_API_MOCK.receptivity.callsFake((tagId) => RX_FROM_API); + + RX_CONNECTOR_MOCK.fetchConfig.reset(); + RX_CONNECTOR_MOCK.fetchConfig.callsFake((tagId) => new Promise((resolve, reject) => resolve({ tag_id: tagId }))); + + RX_CONNECTOR_MOCK.rxApiBuilder.reset(); + RX_CONNECTOR_MOCK.rxApiBuilder.callsFake((_config) => new Promise((resolve, reject) => resolve(RX_API_MOCK))); eventsEmitSpy = sandbox.spy(events, ['emit']); + + let tagId = CUSTOMER; + sessionStorage.clear(); }); afterEach(function () { @@ -43,7 +68,7 @@ describe('contxtfulRtdProvider', function () { sandbox.restore(); }); - describe('extractParameters with invalid configuration', () => { + describe('extractParameters', () => { const { params: { customer, version }, } = buildInitConfig(VERSION, CUSTOMER); @@ -87,27 +112,27 @@ describe('contxtfulRtdProvider', function () { theories.forEach(([params, expectedErrorMessage, _description]) => { const config = { name: 'contxtful', params }; - it('throws the expected error', () => { - expect(() => contxtfulSubmodule.extractParameters(config)).to.throw( + it('detects invalid configuration and throws the expected error', () => { + expect(() => extractParameters(config)).to.throw( expectedErrorMessage ); }); }); }); - describe('initialization with invalid config', function () { - it('returns false', () => { + describe('extractParameters', function () { + it('detects invalid configuration and returns false', () => { expect(contxtfulSubmodule.init({})).to.be.false; }); }); - describe('initialization with valid config', function () { - it('returns true when initializing', () => { + describe('init', function () { + it('uses a valid configuration and returns true when initializing', () => { const config = buildInitConfig(VERSION, CUSTOMER); expect(contxtfulSubmodule.init(config)).to.be.true; }); - it('loads contxtful module script asynchronously', (done) => { + it('loads a RX connector script asynchronously', (done) => { contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); setTimeout(() => { @@ -115,54 +140,84 @@ describe('contxtfulRtdProvider', function () { expect(loadExternalScriptStub.args[0][0]).to.equal( CONTXTFUL_CONNECTOR_ENDPOINT ); + done(); - }, 10); + }, TIMEOUT); }); }); - describe('load external script return falsy', function () { + describe('init', function () { it('returns true when initializing', () => { - loadExternalScriptStub.callsFake(() => {}); + loadExternalScriptStub.callsFake((url, moduleCode, callback, doc, attributes) => { + return { addEventListener: (type, listener) => { } }; + }); const config = buildInitConfig(VERSION, CUSTOMER); expect(contxtfulSubmodule.init(config)).to.be.true; }); }); - describe('rxEngine from external script', function () { - it('use rxEngine api to get receptivity', () => { - contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); - loadExternalScriptTag.dispatchEvent(RX_ENGINE_IS_READY_EVENT); + describe('init', function () { + it('gets the RX API returned by an external script', (done) => { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); - contxtfulSubmodule.getTargetingData(['ad-slot']); + setTimeout(() => { + contxtfulSubmodule.getTargetingData(['ad-slot'], config); + expect(RX_CONNECTOR_MOCK.fetchConfig.callCount, 'fetchConfig').to.be.equal(1); + expect(RX_CONNECTOR_MOCK.rxApiBuilder.callCount, 'rxApiBuilder').to.be.equal(1); + done(); + }, TIMEOUT); + }); + }); + + describe('init', function () { + it('uses the RX API to get receptivity', (done) => { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); - expect(CONTXTFUL_API.GetReceptivity.calledOnce).to.be.true; + setTimeout(() => { + contxtfulSubmodule.getTargetingData(['ad-slot'], config); + expect(RX_API_MOCK.receptivity.callCount, 'receptivity 42').to.be.equal(1); + expect(RX_API_MOCK.receptivity.firstCall.returnValue, 'receptivity').to.be.equal(RX_FROM_API); + done(); + }, TIMEOUT); }); }); - describe('initial receptivity is not dispatched', function () { - it('does not initialize receptivity value', () => { - contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + describe('init', function () { + it('detect that initial receptivity is not dispatched and it does not initialize receptivity value', (done) => { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); - let targetingData = contxtfulSubmodule.getTargetingData(['ad-slot']); - expect(targetingData).to.deep.equal({}); + setTimeout(() => { + let targetingData = contxtfulSubmodule.getTargetingData(['ad-slot'], config); + expect(targetingData).to.deep.equal({}); + done(); + }, TIMEOUT); }); }); - describe('initial receptivity is invalid', function () { + describe('init', function () { const theories = [ [new Event('initialReceptivity'), 'event without details'], - [new CustomEvent('initialReceptivity', { }), 'custom event without details'], + [new CustomEvent('initialReceptivity', {}), 'custom event without details'], [new CustomEvent('initialReceptivity', { detail: {} }), 'custom event with invalid details'], [new CustomEvent('initialReceptivity', { detail: { ReceptivityState: '' } }), 'custom event with details without ReceptivityState'], ]; theories.forEach(([initialReceptivityEvent, _description]) => { - it('does not initialize receptivity value', () => { - contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + it('figures out that initial receptivity is invalid and it does not initialize receptivity value', (done) => { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); loadExternalScriptTag.dispatchEvent(initialReceptivityEvent); - let targetingData = contxtfulSubmodule.getTargetingData(['ad-slot']); - expect(targetingData).to.deep.equal({}); + setTimeout(() => { + let targetingData = contxtfulSubmodule.getTargetingData(['ad-slot'], config); + expect(targetingData).to.deep.equal({}); + done(); + }, TIMEOUT); }); }) }); @@ -173,28 +228,334 @@ describe('contxtfulRtdProvider', function () { [[], {}, 'empty ad-slots'], [ ['ad-slot'], - { 'ad-slot': { ReceptivityState: 'INITIAL_RECEPTIVITY' } }, + { 'ad-slot': RX_FROM_API }, 'single ad-slot', ], [ ['ad-slot-1', 'ad-slot-2'], { - 'ad-slot-1': { ReceptivityState: 'INITIAL_RECEPTIVITY' }, - 'ad-slot-2': { ReceptivityState: 'INITIAL_RECEPTIVITY' }, + 'ad-slot-1': RX_FROM_API, + 'ad-slot-2': RX_FROM_API, + }, + 'many ad-slots', + ], + ]; + + theories.forEach(([adUnits, expected, description]) => { + it('adds receptivity to the ad units using the RX API', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + setTimeout(() => { + let targetingData = contxtfulSubmodule.getTargetingData(adUnits, config); + expect(targetingData, description).to.deep.equal(expected); + done(); + }, TIMEOUT); + }); + }); + }); + + describe('getTargetingData', function () { + const theories = [ + [undefined, {}, 'undefined ad-slots'], + [[], {}, 'empty ad-slots'], + [ + ['ad-slot'], + {}, + 'single ad-slot', + ], + [ + ['ad-slot-1', 'ad-slot-2'], + { + }, + 'many ad-slots', + ], + ]; + + theories.forEach(([adUnits, expected, description]) => { + it('honours "adServerTargeting" and the RX API is not called', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + config.params.adServerTargeting = false; + contxtfulSubmodule.init(config); + loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + setTimeout(() => { + let _ = contxtfulSubmodule.getTargetingData(adUnits, config); + expect(RX_API_MOCK.receptivity.callCount).to.be.equal(0); + done(); + }, TIMEOUT); + }); + + it('honours adServerTargeting and it does not add receptivity to the ad units', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + config.params.adServerTargeting = false; + contxtfulSubmodule.init(config); + loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + setTimeout(() => { + let targetingData = contxtfulSubmodule.getTargetingData(adUnits, config); + expect(targetingData, description).to.deep.equal(expected); + done(); + }, TIMEOUT); + }); + }); + }); + + describe('getTargetingData', function () { + const theories = [ + [undefined, {}, 'undefined ad-slots'], + [[], {}, 'empty ad-slots'], + [ + ['ad-slot'], + { 'ad-slot': RX_FROM_SESSION_STORAGE }, + 'single ad-slot', + ], + [ + ['ad-slot-1', 'ad-slot-2'], + { + 'ad-slot-1': RX_FROM_SESSION_STORAGE, + 'ad-slot-2': RX_FROM_SESSION_STORAGE, }, 'many ad-slots', ], ]; theories.forEach(([adUnits, expected, _description]) => { - it('adds "ReceptivityState" to the adUnits', function () { - contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); - loadExternalScriptTag.dispatchEvent(INITIAL_RECEPTIVITY_EVENT); + it('uses non-expired info from session storage and adds receptivity to the ad units using session storage', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + // Simulate that there was a write to sessionStorage in the past. + writeToStorage(config.params.customer, +100); + contxtfulSubmodule.init(config); - expect(contxtfulSubmodule.getTargetingData(adUnits)).to.deep.equal( + setTimeout(() => { + expect(contxtfulSubmodule.getTargetingData(adUnits, config)).to.deep.equal( + expected + ); + done(); + }, TIMEOUT); + }); + }); + }); + + describe('getTargetingData', function () { + const theories = [ + [undefined, {}, 'undefined ad-slots'], + [[], {}, 'empty ad-slots'], + [ + ['ad-slot'], + {}, + 'single ad-slot', + ], + [ + ['ad-slot-1', 'ad-slot-2'], + { + }, + 'many ad-slots', + ], + ]; + + theories.forEach(([adUnits, expected, _description]) => { + it('ignores expired info from session storage and does not forward the info to ad units', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + // Simulate that there was a write to sessionStorage in the past. + writeToStorage(config.params.customer, -100); + contxtfulSubmodule.init(config); + expect(contxtfulSubmodule.getTargetingData(adUnits, config)).to.deep.equal( expected ); + done(); }); }); }); + + describe('getBidRequestData', function () { + it('calls once the onDone callback', function (done) { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, buildInitConfig(VERSION, CUSTOMER)); + expect(onDoneSpy.calledOnce).to.be.true; + done(); + }, TIMEOUT); + }); + }); + + describe('getBidRequestData', function () { + it('does not write receptivity to the global OpenRTB 2 fragment', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + setTimeout(() => { + const onDone = () => 42; + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDone, config); + expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.equal({}); + done(); + }, TIMEOUT); + }); + }); + + describe('getBidRequestData', function () { + it('writes receptivity to the configured bidder OpenRTB 2 fragments', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + let expectedOrtb2 = { + user: { + data: [ + { + name: 'contxtful', + ext: { + rx: RX_FROM_API, + params: { + ev: config.params?.version, + ci: config.params?.customer, + }, + }, + }, + ], + }, + }; + + setTimeout(() => { + const onDone = () => undefined; + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDone, config); + let actualOrtb2 = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]]; + expect(actualOrtb2).to.deep.equal(expectedOrtb2); + done(); + }, TIMEOUT); + }); + }); + + describe('getBidRequestData', function () { + it('uses non-expired info from session storage and adds receptivity to the reqBidsConfigObj', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + // Simulate that there was a write to sessionStorage in the past. + writeToStorage(config.params.bidders[0], +100); + + contxtfulSubmodule.init(config); + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + let expectedOrtb2 = { + user: { + data: [ + { + name: 'contxtful', + ext: { + rx: RX_FROM_SESSION_STORAGE, + params: { + ev: config.params?.version, + ci: config.params?.customer, + }, + }, + }, + ], + }, + }; + + // Since the RX_CONNECTOR_IS_READY_EVENT event was not dispatched, the RX engine is not loaded. + setTimeout(() => { + const noOp = () => undefined; + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, noOp, buildInitConfig(VERSION, CUSTOMER)); + let actualOrtb2 = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]]; + expect(actualOrtb2).to.deep.equal(expectedOrtb2); + done(); + }, TIMEOUT); + }); + }); + + describe('getBidRequestData', function () { + it('uses the RX API', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + setTimeout(() => { + expect(RX_CONNECTOR_MOCK.fetchConfig.callCount).to.equal(1); + expect(RX_CONNECTOR_MOCK.rxApiBuilder.callCount).to.equal(1); + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + expect(onDoneSpy.callCount).to.equal(1); + expect(RX_API_MOCK.receptivity.callCount).to.equal(1); + done(); + }, TIMEOUT); + }); + }); + + describe('getBidRequestData', function () { + it('adds receptivity to the reqBidsConfigObj', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + let ortb2 = { + user: { + data: [ + { + name: 'contxtful', + ext: { + rx: RX_FROM_API, + params: { + ev: config.params?.version, + ci: config.params?.customer, + }, + }, + }, + ], + }, + }; + + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + expect(reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]]).to.deep.equal(ortb2); + done(); + }, TIMEOUT); + }); + }); }); From 38ca7c230e5b58174d3583ceab541b7b6078b11b Mon Sep 17 00:00:00 2001 From: Saar Amrani Date: Wed, 29 May 2024 23:04:34 +0300 Subject: [PATCH 44/46] Add vidazoo bidder to topicsFpdModule. (#11283) --- modules/topicsFpdModule.js | 3 +++ modules/topicsFpdModule.md | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/modules/topicsFpdModule.js b/modules/topicsFpdModule.js index 72f4068af7f..523c0db326a 100644 --- a/modules/topicsFpdModule.js +++ b/modules/topicsFpdModule.js @@ -47,6 +47,9 @@ const bidderIframeList = { }, { bidder: 'undertone', iframeURL: 'https://creative-p.undertone.com/spk-public/topics_frame.html' + }, { + bidder: 'vidazoo', + iframeURL: 'https://static.vidazoo.com/topics_api/topics_frame.html' }] } diff --git a/modules/topicsFpdModule.md b/modules/topicsFpdModule.md index d187645f520..1f299a9eabe 100644 --- a/modules/topicsFpdModule.md +++ b/modules/topicsFpdModule.md @@ -68,6 +68,10 @@ pbjs.setConfig({ bidder: 'undertone', iframeURL: 'https://creative-p.undertone.com/spk-public/topics_frame.html', expiry: 7 // Configurable expiry days + },{ + bidder: 'vidazoo', + iframeURL: 'https://static.vidazoo.com/topics_api/topics_frame.html', + expiry: 7 // Configurable expiry days }] } .... From 84e4359543eb582b8821e0163078f8db701af84e Mon Sep 17 00:00:00 2001 From: ahmadlob <109217988+ahmadlob@users.noreply.github.com> Date: Wed, 29 May 2024 23:26:33 +0300 Subject: [PATCH 45/46] Taboola Bid Adapter: fix ortb2 user override issue (#11516) * pass-user-fields * pass-user-fields * tests added --- modules/taboolaBidAdapter.js | 49 +++++++++--------- test/spec/modules/taboolaBidAdapter_spec.js | 57 ++++++++++++++++++--- 2 files changed, 76 insertions(+), 30 deletions(-) diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index 74a614bc6b0..ab5d5fef139 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -64,6 +64,7 @@ export const userData = { return tblaId; } } + return undefined; }, getCookieDataByKey(cookieData, key) { if (!cookieData) { @@ -78,6 +79,7 @@ export const userData = { if (hasLocalStorage() && localStorageIsEnabled()) { return getDataFromLocalStorage(STORAGE_KEY); } + return undefined; }, getFromTRC() { return window.TRC ? window.TRC.user_id : 0; @@ -274,35 +276,38 @@ function getSiteProperties({publisherId}, refererInfo, ortb2) { function fillTaboolaReqData(bidderRequest, bidRequest, data) { const {refererInfo, gdprConsent = {}, uspConsent} = bidderRequest; const site = getSiteProperties(bidRequest.params, refererInfo, bidderRequest.ortb2); - const device = {ua: navigator.userAgent}; - let user = { - buyeruid: userData.getUserId(gdprConsent, uspConsent), - ext: {} - }; - if (bidderRequest && bidderRequest.ortb2 && bidderRequest.ortb2.user) { - user.data = bidderRequest.ortb2.user.data; + deepSetValue(data, 'device.ua', navigator.userAgent); + const extractedUserId = userData.getUserId(gdprConsent, uspConsent); + if (data.user == undefined) { + data.user = { + buyeruid: 0, + ext: {} + } } - const regs = { - coppa: 0, - ext: {} - }; - + if (extractedUserId && extractedUserId !== 0) { + deepSetValue(data, 'user.buyeruid', extractedUserId); + } + if (data.regs?.ext == undefined) { + data.regs = { + ext: {} + } + } + deepSetValue(data, 'regs.coppa', 0); if (gdprConsent.gdprApplies) { - user.ext.consent = bidderRequest.gdprConsent.consentString; - regs.ext.gdpr = 1; + deepSetValue(data, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(data, 'regs.ext.gdpr', 1); } - if (uspConsent) { - regs.ext.us_privacy = uspConsent; + deepSetValue(data, 'regs.ext.us_privacy', uspConsent); } if (bidderRequest.ortb2?.regs?.gpp) { - regs.ext.gpp = bidderRequest.ortb2.regs.gpp; - regs.ext.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + deepSetValue(data, 'regs.ext.gpp', bidderRequest.ortb2.regs.gpp); + deepSetValue(data, 'regs.ext.gpp_sid', bidderRequest.ortb2.regs.gpp_sid); } if (config.getConfig('coppa')) { - regs.coppa = 1; + deepSetValue(data, 'regs.coppa', 1); } const ortb2 = bidderRequest.ortb2 || { @@ -311,16 +316,14 @@ function fillTaboolaReqData(bidderRequest, bidRequest, data) { wlang: [] }; + deepSetValue(data, 'source.fd', 1); + data.id = bidderRequest.bidderRequestId; data.site = site; - data.device = device; - data.source = {fd: 1}; data.tmax = (bidderRequest.timeout == undefined) ? undefined : parseInt(bidderRequest.timeout); data.bcat = ortb2.bcat || bidRequest.params.bcat || []; data.badv = ortb2.badv || bidRequest.params.badv || []; data.wlang = ortb2.wlang || bidRequest.params.wlang || []; - data.user = user; - data.regs = regs; deepSetValue(data, 'ext.pageType', ortb2?.ext?.data?.pageType || ortb2?.ext?.data?.section || bidRequest.params.pageType); deepSetValue(data, 'ext.prebid.version', '$prebid.version$'); } diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index 2ea8c325989..55d0731ec21 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -199,6 +199,13 @@ describe('Taboola Adapter', function () { }], id: 'mock-uuid', 'test': 0, + 'device': {'ua': navigator.userAgent}, + 'user': { + 'buyeruid': 0, + 'ext': {}, + }, + 'regs': {'ext': {}, 'coppa': 0}, + 'source': {'fd': 1}, 'site': { 'id': commonBidRequest.params.publisherId, 'name': commonBidRequest.params.publisherId, @@ -208,16 +215,9 @@ describe('Taboola Adapter', function () { 'publisher': {'id': commonBidRequest.params.publisherId}, 'content': {'language': navigator.language} }, - 'device': {'ua': navigator.userAgent}, - 'source': {'fd': 1}, 'bcat': [], 'badv': [], 'wlang': [], - 'user': { - 'buyeruid': 0, - 'ext': {}, - }, - 'regs': {'coppa': 0, 'ext': {}}, 'ext': { 'prebid': { 'version': '$prebid.version$' @@ -363,12 +363,33 @@ describe('Taboola Adapter', function () { bcat: ['EX1', 'EX2', 'EX3'], badv: ['site.com'], wlang: ['de'], + user: { + id: 'externalUserIdPassed' + } } } const res = spec.buildRequests([defaultBidRequest], bidderRequest); expect(res.data.bcat).to.deep.equal(bidderRequest.ortb2.bcat) expect(res.data.badv).to.deep.equal(bidderRequest.ortb2.badv) expect(res.data.wlang).to.deep.equal(bidderRequest.ortb2.wlang) + expect(res.data.user.id).to.deep.equal(bidderRequest.ortb2.user.id) + }); + + it('should pass user entities', function () { + const bidderRequest = { + ...commonBidderRequest, + ortb2: { + user: { + id: 'userid', + buyeruid: 'buyeruid', + yob: 1990 + } + } + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.user.id).to.deep.equal(bidderRequest.ortb2.user.id) + expect(res.data.user.buyeruid).to.deep.equal(bidderRequest.ortb2.user.buyeruid) + expect(res.data.user.yob).to.deep.equal(bidderRequest.ortb2.user.yob) }); it('should pass pageType if exists in ortb2', function () { @@ -503,6 +524,28 @@ describe('Taboola Adapter', function () { expect(res.data.user.buyeruid).to.equal('12121212'); }); + it('should get buyeruid from cookie as priority and external user id from ortb2 object', function () { + getDataFromLocalStorage.returns(51525152); + hasLocalStorage.returns(false); + localStorageIsEnabled.returns(false); + cookiesAreEnabled.returns(true); + getCookie.returns('taboola%20global%3Auser-id=12121212'); + + const bidderRequest = { + ...commonBidderRequest, + ortb2: { + user: { + id: 'userid', + buyeruid: 'buyeruid', + yob: 1990 + } + } + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.user.id).to.deep.equal('userid') + expect(res.data.user.buyeruid).to.equal('12121212'); + }); + it('should get user id from cookie if local storage isn`t defined, only TGID_COOKIE_KEY exists', function () { getDataFromLocalStorage.returns(51525152); hasLocalStorage.returns(false); From 5a62b3863a22dcdac2bebe4c8d0d4fb6f129699f Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 29 May 2024 17:22:35 -0700 Subject: [PATCH 46/46] Currency module: fix bug where changing currency configs causes currency logic to run multiple times (#11613) --- modules/currency.js | 53 ++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/modules/currency.js b/modules/currency.js index c26e64a9400..e2da8b519fa 100644 --- a/modules/currency.js +++ b/modules/currency.js @@ -155,36 +155,35 @@ function loadRates() { function initCurrency() { conversionCache = {}; - currencySupportEnabled = true; - - logInfo('Installing addBidResponse decorator for currency module', arguments); - - // Adding conversion function to prebid global for external module and on page use - getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency); - getHook('addBidResponse').before(addBidResponseHook, 100); - getHook('responsesReady').before(responsesReadyHook); - onEvent(EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); - onEvent(EVENTS.AUCTION_INIT, loadRates); - loadRates(); + if (!currencySupportEnabled) { + currencySupportEnabled = true; + // Adding conversion function to prebid global for external module and on page use + getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency); + getHook('addBidResponse').before(addBidResponseHook, 100); + getHook('responsesReady').before(responsesReadyHook); + onEvent(EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); + onEvent(EVENTS.AUCTION_INIT, loadRates); + loadRates(); + } } function resetCurrency() { - logInfo('Uninstalling addBidResponse decorator for currency module', arguments); - - getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); - getHook('responsesReady').getHooks({hook: responsesReadyHook}).remove(); - offEvent(EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); - offEvent(EVENTS.AUCTION_INIT, loadRates); - delete getGlobal().convertCurrency; - - adServerCurrency = 'USD'; - conversionCache = {}; - currencySupportEnabled = false; - currencyRatesLoaded = false; - needToCallForCurrencyFile = true; - currencyRates = {}; - bidderCurrencyDefault = {}; - responseReady = defer(); + if (currencySupportEnabled) { + getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); + getHook('responsesReady').getHooks({hook: responsesReadyHook}).remove(); + offEvent(EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); + offEvent(EVENTS.AUCTION_INIT, loadRates); + delete getGlobal().convertCurrency; + + adServerCurrency = 'USD'; + conversionCache = {}; + currencySupportEnabled = false; + currencyRatesLoaded = false; + needToCallForCurrencyFile = true; + currencyRates = {}; + bidderCurrencyDefault = {}; + responseReady = defer(); + } } function responsesReadyHook(next, ready) {