From 0862952985f5fbf24d7508f036419ade8115cdc0 Mon Sep 17 00:00:00 2001 From: Rich Audience Date: Tue, 13 Feb 2024 18:03:39 +0100 Subject: [PATCH 01/21] Richaudience Bid Adapter : add compatibility to GPP (#11022) * RichaudienceBidAdapter add function onTimeout * Add unit test * revert: Revert changes in integrationExamples/creative.html * fix: Remove useless package in richaudiences test module * Change referer with host * Fix(RichaudienceBidAdapter): Change url tracking * deploy * change test * remove change others adapters * feat(RichaudienceBidAdapter): Add compatibility to GPP * fix(RichaudienceBidAdapter): Add test to GPP * fix(RichaudienceBidAdapter): Add test to GPP * fix(RichaudienceBidAdapter): Change tmax/timeout hardcoded #9787 --------- Co-authored-by: Sergi Gimenez --- modules/richaudienceBidAdapter.js | 31 +++++++++++-- .../modules/richaudienceBidAdapter_spec.js | 43 ++++++++++++++++++- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index 36513aeda47..b63e31266fb 100755 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -49,7 +49,7 @@ export const spec = { referer: (typeof bidderRequest.refererInfo.page != 'undefined' ? encodeURIComponent(bidderRequest.refererInfo.page) : null), numIframes: (typeof bidderRequest.refererInfo.numIframes != 'undefined' ? bidderRequest.refererInfo.numIframes : null), transactionId: bid.ortb2Imp?.ext?.tid, - timeout: config.getConfig('bidderTimeout'), + timeout: bidderRequest.timeout || 600, user: raiSetEids(bid), demand: raiGetDemandType(bid), videoData: raiGetVideoInfo(bid), @@ -75,6 +75,18 @@ export const spec = { } } + if (bidderRequest?.gppConsent) { + payload.privacy = { + gpp: bidderRequest.gppConsent.gppString, + gpp_sid: bidderRequest.gppConsent.applicableSections + } + } else if (bidderRequest?.ortb2?.regs?.gpp) { + payload.privacy = { + gpp: bidderRequest.ortb2.regs.gpp, + gpp_sid: bidderRequest.ortb2.regs.gpp_sid + } + } + var payloadString = JSON.stringify(payload); var endpoint = 'https://shb.richaudience.com/hb/'; @@ -145,12 +157,13 @@ export const spec = { * @param {gdprConsent} GPDR consent object * @returns {Array} */ - getUserSyncs: function (syncOptions, serverResponses, gdprConsent) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { const syncs = []; var rand = Math.floor(Math.random() * 9999999999); var syncUrl = ''; var consent = ''; + var consentGPP = ''; var raiSync = {}; @@ -160,11 +173,20 @@ export const spec = { consent = `consentString=${gdprConsent.consentString}` } + // GPP Consent + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + consentGPP = 'gpp=' + encodeURIComponent(gppConsent.gppString); + consentGPP += '&gpp_sid=' + encodeURIComponent(gppConsent?.applicableSections?.join(',')); + } + if (syncOptions.iframeEnabled && raiSync.raiIframe != 'exclude') { syncUrl = 'https://sync.richaudience.com/dcf3528a0b8aa83634892d50e91c306e/?ord=' + rand if (consent != '') { syncUrl += `&${consent}` } + if (consentGPP != '') { + syncUrl += `&${consentGPP}` + } syncs.push({ type: 'iframe', url: syncUrl @@ -176,6 +198,9 @@ export const spec = { if (consent != '') { syncUrl += `&${consent}` } + if (consentGPP != '') { + syncUrl += `&${consentGPP}` + } syncs.push({ type: 'image', url: syncUrl @@ -346,7 +371,7 @@ function raiGetTimeoutURL(data) { url = url.replace('[timeout_publisher]', timeout) url = url.replace('[placement_hash]', params[0].pid) - if (REFERER != null) { + if (document.location.host != null) { url = url.replace('[domain]', document.location.host) } return url diff --git a/test/spec/modules/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index 20c60ca328a..d2b173f53df 100644 --- a/test/spec/modules/richaudienceBidAdapter_spec.js +++ b/test/spec/modules/richaudienceBidAdapter_spec.js @@ -293,7 +293,7 @@ describe('Richaudience adapter tests', function () { expect(requestContent.sizes[3]).to.have.property('w').and.to.equal(970); expect(requestContent.sizes[3]).to.have.property('h').and.to.equal(250); expect(requestContent).to.have.property('transactionId').and.to.equal('29df2112-348b-4961-8863-1b33684d95e6'); - expect(requestContent).to.have.property('timeout').and.to.equal(3000); + expect(requestContent).to.have.property('timeout').and.to.equal(600); expect(requestContent).to.have.property('numIframes').and.to.equal(0); expect(typeof requestContent.scr_rsl === 'string') expect(typeof requestContent.cpuc === 'number') @@ -916,7 +916,7 @@ describe('Richaudience adapter tests', function () { it('onTimeout exist as a function', () => { expect(spec.onTimeout).to.exist.and.to.be.a('function'); }); - it('should send timeout', function () { + it('should send timeouts', function () { spec.onTimeout(DEFAULT_PARAMS_VIDEO_TIMEOUT); expect(utils.triggerPixel.called).to.equal(true); expect(utils.triggerPixel.firstCall.args[0]).to.equal('https://s.richaudience.com/err/?ec=6&ev=3000&pla=ADb1f40rmi&int=PREBID&pltfm=&node=&dm=localhost:9876'); @@ -924,6 +924,13 @@ describe('Richaudience adapter tests', function () { }); describe('userSync', function () { + let sandbox; + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + afterEach(function() { + sandbox.restore(); + }); it('Verifies user syncs iframe include', function () { config.setConfig({ 'userSync': {filterSettings: {iframe: {bidders: '*', filter: 'include'}}} @@ -1261,5 +1268,37 @@ describe('Richaudience adapter tests', function () { }, [], {consentString: '', gdprApplies: true}); expect(syncs).to.have.lengthOf(0); }); + + it('Verifies user syncs iframe/image include with GPP', function () { + config.setConfig({ + 'userSync': {filterSettings: {iframe: {bidders: '*', filter: 'include'}}} + }) + + var syncs = spec.getUserSyncs({iframeEnabled: true}, [BID_RESPONSE], { + gppString: 'DBABL~BVVqAAEABgA.QA', + applicableSections: [7]}, + ); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + + config.setConfig({ + 'userSync': {filterSettings: {image: {bidders: '*', filter: 'include'}}} + }) + + var syncs = spec.getUserSyncs({pixelEnabled: true}, [BID_RESPONSE], { + gppString: 'DBABL~BVVqAAEABgA.QA', + applicableSections: [7, 5]}, + ); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + }); + + it('Verifies user syncs URL image include with GPP', function () { + const gppConsent = { gppString: 'DBACMYA~CP5P4cAP5P4cAPoABAESAlEAAAAAAAAAAAAAA2QAQA2ADZABADYAAAAA.QA2QAQA2AAAA.IA2QAQA2AAAA~BP5P4cAP5P4cAPoABABGBACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA', applicableSections: [0] }; + const result = spec.getUserSyncs({pixelEnabled: true}, undefined, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'image', url: `https://sync.richaudience.com/bf7c142f4339da0278e83698a02b0854/?referrer=http%3A%2F%2Fdomain.com&gpp=DBACMYA~CP5P4cAP5P4cAPoABAESAlEAAAAAAAAAAAAAA2QAQA2ADZABADYAAAAA.QA2QAQA2AAAA.IA2QAQA2AAAA~BP5P4cAP5P4cAPoABABGBACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA&gpp_sid=0` + }]); + }); }) }); From 5e7b34b7c68542fe79852e40b127eec4d30b2b67 Mon Sep 17 00:00:00 2001 From: Brian Schmidt Date: Tue, 13 Feb 2024 13:54:42 -0800 Subject: [PATCH 02/21] add OpenX topics iframe (#11039) --- modules/topicsFpdModule.js | 5 ++++- modules/topicsFpdModule.md | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/modules/topicsFpdModule.js b/modules/topicsFpdModule.js index dd240a929a7..f07ab0c7c14 100644 --- a/modules/topicsFpdModule.js +++ b/modules/topicsFpdModule.js @@ -22,13 +22,16 @@ export function reset() { } const bidderIframeList = { - maxTopicCaller: 2, + maxTopicCaller: 4, bidders: [{ bidder: 'pubmatic', iframeURL: 'https://ads.pubmatic.com/AdServer/js/topics/topics_frame.html' }, { bidder: 'rtbhouse', iframeURL: 'https://topics.authorizedvault.com/topicsapi.html' + }, { + bidder: 'openx', + iframeURL: 'https://pa.openx.net/topics_frame.html' }, { bidder: 'improvedigital', iframeURL: 'https://hb.360yield.com/privacy-sandbox/topics.html' diff --git a/modules/topicsFpdModule.md b/modules/topicsFpdModule.md index 6ef0bf241dd..a6bb14dca67 100644 --- a/modules/topicsFpdModule.md +++ b/modules/topicsFpdModule.md @@ -40,6 +40,10 @@ pbjs.setConfig({ bidder: 'rtbhouse', iframeURL: 'https://topics.authorizedvault.com/topicsapi.html', expiry: 7 // Configurable expiry days + },{ + bidder: 'openx', + iframeURL: 'https://pa.openx.net/topics_frame.html', + expiry: 7 // Configurable expiry days },{ bidder: 'rubicon', iframeURL: 'https://rubicon.com:8080/topics/fpd/topic.html', // dummy URL From 6eeb99eb289db02443d9e8e208bcd32341f89a33 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 14 Feb 2024 14:02:21 +0100 Subject: [PATCH 03/21] =?UTF-8?q?=E2=9C=A8=20add=20sellerCurrency=20to=20f?= =?UTF-8?q?ledge=20auction=20config=20for=20criteo=20bid=20adapter=20(#110?= =?UTF-8?q?84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: v.raybaud --- modules/criteoBidAdapter.js | 4 +--- test/spec/modules/criteoBidAdapter_spec.js | 8 +++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index fcf8d2ad953..40d39dc5618 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -298,9 +298,6 @@ export const spec = { if (!sellerSignals.floor && bidRequest.params.bidFloor) { sellerSignals.floor = bidRequest.params.bidFloor; } - if (!sellerSignals.sellerCurrency && bidRequest.params.bidFloorCur) { - sellerSignals.sellerCurrency = bidRequest.params.bidFloorCur; - } if (body?.ext?.sellerSignalsPerImp !== undefined) { const sellerSignalsPerImp = body.ext.sellerSignalsPerImp[bidId]; if (sellerSignalsPerImp !== undefined) { @@ -317,6 +314,7 @@ export const spec = { auctionSignals: {}, decisionLogicUrl: FLEDGE_DECISION_LOGIC_URL, interestGroupBuyers: Object.keys(perBuyerSignals), + sellerCurrency: sellerSignals.currency || '???', }, }); }); diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index ce07c6e49bc..4c599550afb 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -2576,7 +2576,8 @@ describe('The Criteo bidding adapter', function () { sellerSignalsPerImp: { 'test-bidId': { foo2: 'bar2', - } + currency: 'USD' + }, }, }, }, @@ -2646,8 +2647,9 @@ describe('The Criteo bidding adapter', function () { foo: 'bar', foo2: 'bar2', floor: 1, - sellerCurrency: 'EUR', + currency: 'USD', }, + sellerCurrency: 'USD' }, }); expect(interpretedResponse.fledgeAuctionConfigs[1]).to.deep.equal({ @@ -2669,8 +2671,8 @@ describe('The Criteo bidding adapter', function () { sellerSignals: { foo: 'bar', floor: 1, - sellerCurrency: 'EUR', }, + sellerCurrency: '???' }, }); }); From 7fbe4e9b98144982e330c9c108031693fdc0242d Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Date: Wed, 14 Feb 2024 14:25:23 +0100 Subject: [PATCH 04/21] greenbids Analytics Adapter: fix double sampling bug (#11090) * greenbidsAnalyticsAdapter: fix double sampling bug * greenbidsAnalyticsAdapter bump version --- modules/greenbidsAnalyticsAdapter.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/greenbidsAnalyticsAdapter.js b/modules/greenbidsAnalyticsAdapter.js index 5d1f35f24ff..9b23dba01b9 100644 --- a/modules/greenbidsAnalyticsAdapter.js +++ b/modules/greenbidsAnalyticsAdapter.js @@ -6,7 +6,7 @@ import {deepClone, generateUUID, logError, logInfo, logWarn} from '../src/utils. const analyticsType = 'endpoint'; -export const ANALYTICS_VERSION = '2.0.0'; +export const ANALYTICS_VERSION = '2.1.0'; const ANALYTICS_SERVER = 'https://a.greenbids.ai'; @@ -33,7 +33,6 @@ export const isSampled = function(greenbidsId, samplingRate) { return true; } const hashInt = parseInt(greenbidsId.slice(-4), 16); - return hashInt < samplingRate * (0xFFFF + 1); } @@ -59,8 +58,6 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER if (typeof analyticsOptions.options.sampling === 'number') { logWarn('"options.sampling" is deprecated, please use "greenbidsSampling" instead.'); analyticsOptions.options.greenbidsSampling = analyticsOptions.options.sampling; - // Set sampling to null to prevent prebid analytics integrated sampling to happen - analyticsOptions.options.sampling = null; } /** @@ -228,6 +225,10 @@ greenbidsAnalyticsAdapter.originEnableAnalytics = greenbidsAnalyticsAdapter.enab greenbidsAnalyticsAdapter.enableAnalytics = function(config) { this.initConfig(config); + if (typeof config.options.sampling === 'number') { + // Set sampling to 1 to prevent prebid analytics integrated sampling to happen + config.options.sampling = 1; + } logInfo('loading greenbids analytics'); greenbidsAnalyticsAdapter.originEnableAnalytics(config); }; From 8df7a80feefc335207e41480444ac373a6a42376 Mon Sep 17 00:00:00 2001 From: matthieularere-msq <63732822+matthieularere-msq@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:25:39 +0100 Subject: [PATCH 05/21] mediasquare Bid Adapter: minor change with floors (#11100) --- modules/mediasquareBidAdapter.js | 2 ++ test/spec/modules/mediasquareBidAdapter_spec.js | 1 + 2 files changed, 3 insertions(+) diff --git a/modules/mediasquareBidAdapter.js b/modules/mediasquareBidAdapter.js index 1ec05d17eef..a84c19b786b 100644 --- a/modules/mediasquareBidAdapter.js +++ b/modules/mediasquareBidAdapter.js @@ -62,6 +62,8 @@ export const spec = { if (tmpFloor != {}) { floor[value.join('x')] = tmpFloor; } }); } + let tmpFloor = adunitValue.getFloor({currency: 'USD', mediaType: '*', size: '*'}); + if (tmpFloor != {}) { floor['*'] = tmpFloor; } } codes.push({ owner: adunitValue.params.owner, diff --git a/test/spec/modules/mediasquareBidAdapter_spec.js b/test/spec/modules/mediasquareBidAdapter_spec.js index 6082ef65055..cdeae38aa19 100644 --- a/test/spec/modules/mediasquareBidAdapter_spec.js +++ b/test/spec/modules/mediasquareBidAdapter_spec.js @@ -173,6 +173,7 @@ describe('MediaSquare bid adapter tests', function () { const responsefloor = JSON.parse(requestfloor.data); expect(responsefloor.codes[0]).to.have.property('floor').exist; expect(responsefloor.codes[0].floor).to.have.property('300x250').and.to.have.property('floor').and.to.equal(1); + expect(responsefloor.codes[0].floor).to.have.property('*'); }); it('Verify parse response', function () { From b6276873d507a532835623da84e376046a569aba Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 14 Feb 2024 10:01:11 -0800 Subject: [PATCH 06/21] PAAPI/fledgeForGpt: make auction configs available independently from GPT (#10930) * paapi module * fledgeForGpt/paapi split and config aliases * Add reuse = false option and GPT slot reset * simpler log messages * fix reuse * refactory reset logic * remove reuse option, treat auction configs as single-use * do not do global reset if called with auction filter * at auction end autoconfig, reset all slots involved in the auction * includeBlanks * use includeBlanks from fledgeForGpt --- integrationExamples/gpt/fledge_example.html | 15 +- modules/.submodules.json | 3 + modules/fledgeForGpt.js | 243 +++----- modules/paapi.js | 241 ++++++++ test/helpers/indexStub.js | 4 +- test/spec/modules/fledgeForGpt_spec.js | 506 ++++------------ test/spec/modules/paapi_spec.js | 628 ++++++++++++++++++++ 7 files changed, 1101 insertions(+), 539 deletions(-) create mode 100644 modules/paapi.js create mode 100644 test/spec/modules/paapi_spec.js diff --git a/integrationExamples/gpt/fledge_example.html b/integrationExamples/gpt/fledge_example.html index 5059e03daef..5a6ab7a5fef 100644 --- a/integrationExamples/gpt/fledge_example.html +++ b/integrationExamples/gpt/fledge_example.html @@ -44,15 +44,11 @@ pbjs.que.push(function() { pbjs.setConfig({ - fledgeForGpt: { - enabled: true - } - }); - - pbjs.setBidderConfig({ - bidders: ['openx'], - config: { - fledgeEnabled: true + paapi: { + enabled: true, + gpt: { + autoconfig: false + } } }); @@ -69,6 +65,7 @@ googletag.cmd.push(function() { pbjs.que.push(function() { pbjs.setTargetingForGPTAsync(); + pbjs.setPAAPIConfigForGPT(); googletag.pubads().refresh(); }); }); diff --git a/modules/.submodules.json b/modules/.submodules.json index d2a13a57330..61d8c843d47 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -102,6 +102,9 @@ "videoModule": [ "jwplayerVideoProvider", "videojsVideoProvider" + ], + "paapi": [ + "fledgeForGpt" ] } } diff --git a/modules/fledgeForGpt.js b/modules/fledgeForGpt.js index 7162f4e9230..7e841234e24 100644 --- a/modules/fledgeForGpt.js +++ b/modules/fledgeForGpt.js @@ -1,169 +1,104 @@ /** - * Fledge modules is responsible for registering fledged auction configs into the GPT slot; - * GPT is resposible to run the fledge auction. + * GPT-specific slot configuration logic for PAAPI. */ -import { config } from '../src/config.js'; -import { getHook } from '../src/hook.js'; -import {deepSetValue, logInfo, logWarn, mergeDeep} from '../src/utils.js'; -import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js'; -import * as events from '../src/events.js' -import CONSTANTS from '../src/constants.json'; -import {currencyCompare} from '../libraries/currencyUtils/currency.js'; -import {maximum, minimum} from '../src/utils/reducers.js'; +import {submodule} from '../src/hook.js'; +import {deepAccess, logInfo, logWarn} from '../src/utils.js'; import {getGptSlotForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; - -const MODULE = 'fledgeForGpt' -const PENDING = {}; - -export let isEnabled = false; - -config.getConfig('fledgeForGpt', config => init(config.fledgeForGpt)); - -/** - * Module init. - */ -export function init(cfg) { - if (cfg && cfg.enabled === true) { - if (!isEnabled) { - getHook('addComponentAuction').before(addComponentAuctionHook); - getHook('makeBidRequests').after(markForFledge); - events.on(CONSTANTS.EVENTS.AUCTION_INIT, onAuctionInit); - events.on(CONSTANTS.EVENTS.AUCTION_END, onAuctionEnd); - isEnabled = true; - } - logInfo(`${MODULE} enabled (browser ${isFledgeSupported() ? 'supports' : 'does NOT support'} fledge)`, cfg); - } else { - if (isEnabled) { - getHook('addComponentAuction').getHooks({hook: addComponentAuctionHook}).remove(); - getHook('makeBidRequests').getHooks({hook: markForFledge}).remove() - events.off(CONSTANTS.EVENTS.AUCTION_INIT, onAuctionInit); - events.off(CONSTANTS.EVENTS.AUCTION_END, onAuctionEnd); - isEnabled = false; +import {config} from '../src/config.js'; +import {getGlobal} from '../src/prebidGlobal.js'; + +const MODULE = 'fledgeForGpt'; + +let getPAAPIConfig; + +// for backwards compat, we attempt to automatically set GPT configuration as soon as we +// have the auction configs available. Disabling this allows one to call pbjs.setPAAPIConfigForGPT at their +// own pace. +let autoconfig = true; + +Object.entries({ + [MODULE]: MODULE, + 'paapi': 'paapi.gpt' +}).forEach(([topic, ns]) => { + const configKey = `${ns}.autoconfig`; + config.getConfig(topic, (cfg) => { + autoconfig = deepAccess(cfg, configKey, true); + }); +}); + +export function slotConfigurator() { + const PREVIOUSLY_SET = {}; + return function setComponentAuction(adUnitCode, auctionConfigs, reset = true) { + const gptSlot = getGptSlotForAdUnitCode(adUnitCode); + if (gptSlot && gptSlot.setConfig) { + let previous = PREVIOUSLY_SET[adUnitCode] ?? {}; + let configsBySeller = Object.fromEntries(auctionConfigs.map(cfg => [cfg.seller, cfg])); + const sellers = Object.keys(configsBySeller); + if (reset) { + configsBySeller = Object.assign(previous, configsBySeller); + previous = Object.fromEntries(sellers.map(seller => [seller, null])); + } else { + sellers.forEach(seller => { + previous[seller] = null; + }); + } + Object.keys(previous).length ? PREVIOUSLY_SET[adUnitCode] = previous : delete PREVIOUSLY_SET[adUnitCode]; + const componentAuction = Object.entries(configsBySeller) + .map(([configKey, auctionConfig]) => ({configKey, auctionConfig})); + if (componentAuction.length > 0) { + gptSlot.setConfig({componentAuction}); + logInfo(MODULE, `register component auction configs for: ${adUnitCode}: ${gptSlot.getAdUnitPath()}`, auctionConfigs); + } + } else if (auctionConfigs.length > 0) { + logWarn(MODULE, `unable to register component auction config for ${adUnitCode}`, auctionConfigs); } - logInfo(`${MODULE} disabled`, cfg); - } -} - -function setComponentAuction(adUnitCode, auctionConfigs) { - const gptSlot = getGptSlotForAdUnitCode(adUnitCode); - if (gptSlot && gptSlot.setConfig) { - gptSlot.setConfig({ - componentAuction: auctionConfigs.map(cfg => ({ - configKey: cfg.seller, - auctionConfig: cfg - })) - }); - logInfo(MODULE, `register component auction configs for: ${adUnitCode}: ${gptSlot.getAdUnitPath()}`, auctionConfigs); - } else { - logWarn(MODULE, `unable to register component auction config for ${adUnitCode}`, auctionConfigs); - } + }; } -function onAuctionInit({auctionId}) { - PENDING[auctionId] = {}; -} +const setComponentAuction = slotConfigurator(); -function getSlotSignals(bidsReceived = [], bidRequests = []) { - let bidfloor, bidfloorcur; - if (bidsReceived.length > 0) { - const bestBid = bidsReceived.reduce(maximum(currencyCompare(bid => [bid.cpm, bid.currency]))); - bidfloor = bestBid.cpm; - bidfloorcur = bestBid.currency; - } else { - const floors = bidRequests.map(bid => typeof bid.getFloor === 'function' && bid.getFloor()).filter(f => f); - const minFloor = floors.length && floors.reduce(minimum(currencyCompare(floor => [floor.floor, floor.currency]))) - bidfloor = minFloor?.floor; - bidfloorcur = minFloor?.currency; - } - const cfg = {}; - if (bidfloor) { - deepSetValue(cfg, 'auctionSignals.prebid.bidfloor', bidfloor); - bidfloorcur && deepSetValue(cfg, 'auctionSignals.prebid.bidfloorcur', bidfloorcur); +export function onAuctionConfigFactory(setGptConfig = setComponentAuction) { + return function onAuctionConfig(auctionId, configsByAdUnit, markAsUsed) { + if (autoconfig) { + Object.entries(configsByAdUnit).forEach(([adUnitCode, cfg]) => { + setGptConfig(adUnitCode, cfg?.componentAuctions ?? []); + markAsUsed(adUnitCode); + }); + } } - return cfg; } -function onAuctionEnd({auctionId, bidsReceived, bidderRequests}) { - try { - const allReqs = bidderRequests?.flatMap(br => br.bids); - Object.entries(PENDING[auctionId]).forEach(([adUnitCode, auctionConfigs]) => { - const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode; - const slotSignals = getSlotSignals(bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit)); - setComponentAuction(adUnitCode, auctionConfigs.map(cfg => mergeDeep({}, slotSignals, cfg))) +export function setPAAPIConfigFactory( + getConfig = (filters) => getPAAPIConfig(filters, true), + setGptConfig = setComponentAuction) { + /** + * Configure GPT slots with PAAPI auction configs. + * `filters` are the same filters accepted by `pbjs.getPAAPIConfig`; + */ + return function(filters = {}) { + let some = false; + Object.entries( + getConfig(filters) || {} + ).forEach(([au, config]) => { + if (config != null) { + some = true; + } + setGptConfig(au, config?.componentAuctions || [], true); }) - } finally { - delete PENDING[auctionId]; - } -} - -function setFPDSignals(auctionConfig, fpd) { - auctionConfig.auctionSignals = mergeDeep({}, {prebid: fpd}, auctionConfig.auctionSignals); -} - -export function addComponentAuctionHook(next, request, componentAuctionConfig) { - const {adUnitCode, auctionId, ortb2, ortb2Imp} = request; - if (PENDING.hasOwnProperty(auctionId)) { - setFPDSignals(componentAuctionConfig, {ortb2, ortb2Imp}); - !PENDING[auctionId].hasOwnProperty(adUnitCode) && (PENDING[auctionId][adUnitCode] = []); - PENDING[auctionId][adUnitCode].push(componentAuctionConfig); - } else { - logWarn(MODULE, `Received component auction config for auction that has closed (auction '${auctionId}', adUnit '${adUnitCode}')`, componentAuctionConfig) - } - next(request, componentAuctionConfig); -} - -function isFledgeSupported() { - return 'runAdAuction' in navigator && 'joinAdInterestGroup' in navigator -} - -export function markForFledge(next, bidderRequests) { - if (isFledgeSupported()) { - const globalFledgeConfig = config.getConfig('fledgeForGpt'); - const bidders = globalFledgeConfig?.bidders ?? []; - bidderRequests.forEach((bidderReq) => { - const useGlobalConfig = globalFledgeConfig?.enabled && (bidders.length === 0 || bidders.includes(bidderReq.bidderCode)); - config.runWithBidder(bidderReq.bidderCode, () => { - const fledgeEnabled = config.getConfig('fledgeEnabled') ?? (useGlobalConfig ? globalFledgeConfig.enabled : undefined); - const defaultForSlots = config.getConfig('defaultForSlots') ?? (useGlobalConfig ? globalFledgeConfig?.defaultForSlots : undefined); - Object.assign(bidderReq, {fledgeEnabled}); - bidderReq.bids.forEach(bidReq => { deepSetValue(bidReq, 'ortb2Imp.ext.ae', bidReq.ortb2Imp?.ext?.ae ?? defaultForSlots) }) - }) - }); - } - next(bidderRequests); -} - -export function setImpExtAe(imp, bidRequest, context) { - if (imp.ext?.ae && !context.bidderRequest.fledgeEnabled) { - delete imp.ext?.ae; - } -} -registerOrtbProcessor({type: IMP, name: 'impExtAe', fn: setImpExtAe}); - -// to make it easier to share code between the PBS adapter and adapters whose backend is PBS, break up -// fledge response processing in two steps: first aggregate all the auction configs by their imp... - -export function parseExtPrebidFledge(response, ortbResponse, context) { - (ortbResponse.ext?.prebid?.fledge?.auctionconfigs || []).forEach((cfg) => { - const impCtx = context.impContext[cfg.impid]; - if (!impCtx?.imp?.ext?.ae) { - logWarn('Received fledge auction configuration for an impression that was not in the request or did not ask for it', cfg, impCtx?.imp); - } else { - impCtx.fledgeConfigs = impCtx.fledgeConfigs || []; - impCtx.fledgeConfigs.push(cfg); + if (!some) { + logInfo(`${MODULE}: No component auctions available to set`); } - }) + } } -registerOrtbProcessor({type: RESPONSE, name: 'extPrebidFledge', fn: parseExtPrebidFledge, dialects: [PBS]}); - -// ...then, make them available in the adapter's response. This is the client side version, for which the -// interpretResponse api is {fledgeAuctionConfigs: [{bidId, config}]} +/** + * Configure GPT slots with PAAPI component auctions. Accepts the same filter arguments as `pbjs.getPAAPIConfig`. + */ +getGlobal().setPAAPIConfigForGPT = setPAAPIConfigFactory(); -export function setResponseFledgeConfigs(response, ortbResponse, context) { - const configs = Object.values(context.impContext) - .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => ({bidId: impCtx.bidRequest.bidId, config: cfg.config}))); - if (configs.length > 0) { - response.fledgeAuctionConfigs = configs; +submodule('paapi', { + name: 'gpt', + onAuctionConfig: onAuctionConfigFactory(), + init(params) { + getPAAPIConfig = params.getPAAPIConfig; } -} -registerOrtbProcessor({type: RESPONSE, name: 'fledgeAuctionConfigs', priority: -1, fn: setResponseFledgeConfigs, dialects: [PBS]}) +}); diff --git a/modules/paapi.js b/modules/paapi.js new file mode 100644 index 00000000000..720935bd3f5 --- /dev/null +++ b/modules/paapi.js @@ -0,0 +1,241 @@ +/** + * Collect PAAPI component auction configs from bid adapters and make them available through `pbjs.getPAAPIConfig()` + */ +import {config} from '../src/config.js'; +import {getHook, module} from '../src/hook.js'; +import {deepSetValue, logInfo, logWarn, mergeDeep} from '../src/utils.js'; +import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; +import {currencyCompare} from '../libraries/currencyUtils/currency.js'; +import {maximum, minimum} from '../src/utils/reducers.js'; +import {auctionManager} from '../src/auctionManager.js'; +import {getGlobal} from '../src/prebidGlobal.js'; + +const MODULE = 'PAAPI'; + +const submodules = []; +const USED = new WeakSet(); + +export function registerSubmodule(submod) { + submodules.push(submod); + submod.init && submod.init({getPAAPIConfig}); +} + +module('paapi', registerSubmodule); + +function auctionConfigs() { + const store = new WeakMap(); + return function (auctionId, init = {}) { + const auction = auctionManager.index.getAuction({auctionId}); + if (auction == null) return; + if (!store.has(auction)) { + store.set(auction, init); + } + return store.get(auction); + }; +} + +const pendingForAuction = auctionConfigs(); +const configsForAuction = auctionConfigs(); +let latestAuctionForAdUnit = {}; +let moduleConfig = {}; + +['paapi', 'fledgeForGpt'].forEach(ns => { + config.getConfig(ns, config => { + init(config[ns], ns); + }); +}); + +export function reset() { + submodules.splice(0, submodules.length); + latestAuctionForAdUnit = {}; +} + +export function init(cfg, configNamespace) { + if (configNamespace !== 'paapi') { + logWarn(`'${configNamespace}' configuration options will be renamed to 'paapi'; consider using setConfig({paapi: [...]}) instead`); + } + if (cfg && cfg.enabled === true) { + moduleConfig = cfg; + logInfo(`${MODULE} enabled (browser ${isFledgeSupported() ? 'supports' : 'does NOT support'} runAdAuction)`, cfg); + } else { + moduleConfig = {}; + logInfo(`${MODULE} disabled`, cfg); + } +} + +getHook('addComponentAuction').before(addComponentAuctionHook); +getHook('makeBidRequests').after(markForFledge); +events.on(CONSTANTS.EVENTS.AUCTION_END, onAuctionEnd); + +function getSlotSignals(bidsReceived = [], bidRequests = []) { + let bidfloor, bidfloorcur; + if (bidsReceived.length > 0) { + const bestBid = bidsReceived.reduce(maximum(currencyCompare(bid => [bid.cpm, bid.currency]))); + bidfloor = bestBid.cpm; + bidfloorcur = bestBid.currency; + } else { + const floors = bidRequests.map(bid => typeof bid.getFloor === 'function' && bid.getFloor()).filter(f => f); + const minFloor = floors.length && floors.reduce(minimum(currencyCompare(floor => [floor.floor, floor.currency]))); + bidfloor = minFloor?.floor; + bidfloorcur = minFloor?.currency; + } + const cfg = {}; + if (bidfloor) { + deepSetValue(cfg, 'auctionSignals.prebid.bidfloor', bidfloor); + bidfloorcur && deepSetValue(cfg, 'auctionSignals.prebid.bidfloorcur', bidfloorcur); + } + return cfg; +} + +function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes}) { + const allReqs = bidderRequests?.flatMap(br => br.bids); + const paapiConfigs = {}; + (adUnitCodes || []).forEach(au => { + paapiConfigs[au] = null; + !latestAuctionForAdUnit.hasOwnProperty(au) && (latestAuctionForAdUnit[au] = null); + }) + Object.entries(pendingForAuction(auctionId) || {}).forEach(([adUnitCode, auctionConfigs]) => { + const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode; + const slotSignals = getSlotSignals(bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit)); + paapiConfigs[adUnitCode] = { + componentAuctions: auctionConfigs.map(cfg => mergeDeep({}, slotSignals, cfg)) + }; + latestAuctionForAdUnit[adUnitCode] = auctionId; + }); + configsForAuction(auctionId, paapiConfigs); + submodules.forEach(submod => submod.onAuctionConfig?.( + auctionId, + paapiConfigs, + (adUnitCode) => paapiConfigs[adUnitCode] != null && USED.add(paapiConfigs[adUnitCode])) + ); +} + +function setFPDSignals(auctionConfig, fpd) { + auctionConfig.auctionSignals = mergeDeep({}, {prebid: fpd}, auctionConfig.auctionSignals); +} + +export function addComponentAuctionHook(next, request, componentAuctionConfig) { + if (getFledgeConfig().enabled) { + const {adUnitCode, auctionId, ortb2, ortb2Imp} = request; + const configs = pendingForAuction(auctionId); + if (configs != null) { + setFPDSignals(componentAuctionConfig, {ortb2, ortb2Imp}); + !configs.hasOwnProperty(adUnitCode) && (configs[adUnitCode] = []); + configs[adUnitCode].push(componentAuctionConfig); + } else { + logWarn(MODULE, `Received component auction config for auction that has closed (auction '${auctionId}', adUnit '${adUnitCode}')`, componentAuctionConfig); + } + } + next(request, componentAuctionConfig); +} + +/** + * Get PAAPI auction configuration. + * + * @param auctionId? optional auction filter; if omitted, the latest auction for each ad unit is used + * @param adUnitCode? optional ad unit filter + * @param includeBlanks if true, include null entries for ad units that match the given filters but do not have any available auction configs. + * @returns {{}} a map from ad unit code to auction config for the ad unit. + */ +export function getPAAPIConfig({auctionId, adUnitCode} = {}, includeBlanks = false) { + const output = {}; + const targetedAuctionConfigs = auctionId && configsForAuction(auctionId); + Object.keys((auctionId != null ? targetedAuctionConfigs : latestAuctionForAdUnit) ?? []).forEach(au => { + const latestAuctionId = latestAuctionForAdUnit[au]; + const auctionConfigs = targetedAuctionConfigs ?? (latestAuctionId && configsForAuction(latestAuctionId)); + if ((adUnitCode ?? au) === au) { + let candidate; + if (targetedAuctionConfigs?.hasOwnProperty(au)) { + candidate = targetedAuctionConfigs[au]; + } else if (auctionId == null && auctionConfigs?.hasOwnProperty(au)) { + candidate = auctionConfigs[au]; + } + if (candidate && !USED.has(candidate)) { + output[au] = candidate; + USED.add(candidate); + } else if (includeBlanks) { + output[au] = null; + } + } + }) + return output; +} + +getGlobal().getPAAPIConfig = (filters) => getPAAPIConfig(filters); + +function isFledgeSupported() { + return 'runAdAuction' in navigator && 'joinAdInterestGroup' in navigator; +} + +function getFledgeConfig() { + const bidder = config.getCurrentBidder(); + const useGlobalConfig = moduleConfig.enabled && (bidder == null || !moduleConfig.bidders?.length || moduleConfig.bidders?.includes(bidder)); + return { + enabled: config.getConfig('fledgeEnabled') ?? useGlobalConfig, + ae: config.getConfig('defaultForSlots') ?? (useGlobalConfig ? moduleConfig.defaultForSlots : undefined) + }; +} + +export function markForFledge(next, bidderRequests) { + if (isFledgeSupported()) { + bidderRequests.forEach((bidderReq) => { + config.runWithBidder(bidderReq.bidderCode, () => { + const {enabled, ae} = getFledgeConfig(); + Object.assign(bidderReq, {fledgeEnabled: enabled}); + bidderReq.bids.forEach(bidReq => { + deepSetValue(bidReq, 'ortb2Imp.ext.ae', bidReq.ortb2Imp?.ext?.ae ?? ae); + }); + }); + }); + } + next(bidderRequests); +} + +export function setImpExtAe(imp, bidRequest, context) { + if (imp.ext?.ae && !context.bidderRequest.fledgeEnabled) { + delete imp.ext?.ae; + } +} + +registerOrtbProcessor({type: IMP, name: 'impExtAe', fn: setImpExtAe}); + +// to make it easier to share code between the PBS adapter and adapters whose backend is PBS, break up +// fledge response processing in two steps: first aggregate all the auction configs by their imp... + +export function parseExtPrebidFledge(response, ortbResponse, context) { + (ortbResponse.ext?.prebid?.fledge?.auctionconfigs || []).forEach((cfg) => { + const impCtx = context.impContext[cfg.impid]; + if (!impCtx?.imp?.ext?.ae) { + logWarn('Received fledge auction configuration for an impression that was not in the request or did not ask for it', cfg, impCtx?.imp); + } else { + impCtx.fledgeConfigs = impCtx.fledgeConfigs || []; + impCtx.fledgeConfigs.push(cfg); + } + }); +} + +registerOrtbProcessor({type: RESPONSE, name: 'extPrebidFledge', fn: parseExtPrebidFledge, dialects: [PBS]}); + +// ...then, make them available in the adapter's response. This is the client side version, for which the +// interpretResponse api is {fledgeAuctionConfigs: [{bidId, config}]} + +export function setResponseFledgeConfigs(response, ortbResponse, context) { + const configs = Object.values(context.impContext) + .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => ({ + bidId: impCtx.bidRequest.bidId, + config: cfg.config + }))); + if (configs.length > 0) { + response.fledgeAuctionConfigs = configs; + } +} + +registerOrtbProcessor({ + type: RESPONSE, + name: 'fledgeAuctionConfigs', + priority: -1, + fn: setResponseFledgeConfigs, + dialects: [PBS] +}); diff --git a/test/helpers/indexStub.js b/test/helpers/indexStub.js index 2916b60ae32..5202106c9cf 100644 --- a/test/helpers/indexStub.js +++ b/test/helpers/indexStub.js @@ -1,6 +1,6 @@ import {AuctionIndex} from '../../src/auctionIndex.js'; -export function stubAuctionIndex({bidRequests, bidderRequests, adUnits}) { +export function stubAuctionIndex({bidRequests, bidderRequests, adUnits, auctionId = 'mock-auction'}) { if (adUnits == null) { adUnits = [] } @@ -15,7 +15,7 @@ export function stubAuctionIndex({bidRequests, bidderRequests, adUnits}) { } const auction = { getAuctionId() { - return 'mock-auction' + return auctionId; }, getBidRequests() { return bidderRequests; diff --git a/test/spec/modules/fledgeForGpt_spec.js b/test/spec/modules/fledgeForGpt_spec.js index 60a8e196ae0..8ab11171121 100644 --- a/test/spec/modules/fledgeForGpt_spec.js +++ b/test/spec/modules/fledgeForGpt_spec.js @@ -1,419 +1,177 @@ -import { - expect -} from 'chai'; -import * as fledge from 'modules/fledgeForGpt.js'; -import {config} from '../../../src/config.js'; -import adapterManager from '../../../src/adapterManager.js'; -import * as utils from '../../../src/utils.js'; +import {onAuctionConfigFactory, setPAAPIConfigFactory, slotConfigurator} from 'modules/fledgeForGpt.js'; import * as gptUtils from '../../../libraries/gptUtils/gptUtils.js'; -import {hook} from '../../../src/hook.js'; import 'modules/appnexusBidAdapter.js'; import 'modules/rubiconBidAdapter.js'; -import {parseExtPrebidFledge, setImpExtAe, setResponseFledgeConfigs} from 'modules/fledgeForGpt.js'; -import * as events from 'src/events.js'; -import CONSTANTS from 'src/constants.json'; -import {getGlobal} from '../../../src/prebidGlobal.js'; +import {deepSetValue} from '../../../src/utils.js'; +import {config} from 'src/config.js'; describe('fledgeForGpt module', () => { - let sandbox; + let sandbox, fledgeAuctionConfig; beforeEach(() => { sandbox = sinon.sandbox.create(); + fledgeAuctionConfig = { + seller: 'bidder', + mock: 'config' + }; }); afterEach(() => { sandbox.restore(); }); - describe('addComponentAuction', function () { - before(() => { - fledge.init({enabled: true}); - }); - const fledgeAuctionConfig = { - seller: 'bidder', - mock: 'config' - }; - - describe('addComponentAuctionHook', function () { - let nextFnSpy, mockGptSlot; - beforeEach(function () { - nextFnSpy = sinon.spy(); - mockGptSlot = { - setConfig: sinon.stub(), - getAdUnitPath: () => 'mock/gpt/au' - }; - sandbox.stub(gptUtils, 'getGptSlotForAdUnitCode').callsFake(() => mockGptSlot); - }); - - it('should call next()', function () { - const request = {auctionId: 'aid', adUnitCode: 'auc'}; - fledge.addComponentAuctionHook(nextFnSpy, request, fledgeAuctionConfig); - sinon.assert.calledWith(nextFnSpy, request, fledgeAuctionConfig); + describe('slotConfigurator', () => { + let mockGptSlot, setGptConfig; + beforeEach(() => { + mockGptSlot = { + setConfig: sinon.stub(), + getAdUnitPath: () => 'mock/gpt/au' + }; + sandbox.stub(gptUtils, 'getGptSlotForAdUnitCode').callsFake(() => mockGptSlot); + setGptConfig = slotConfigurator(); + }); + it('should set GPT slot config', () => { + setGptConfig('au', [fledgeAuctionConfig]); + sinon.assert.calledWith(gptUtils.getGptSlotForAdUnitCode, 'au'); + sinon.assert.calledWith(mockGptSlot.setConfig, { + componentAuction: [{ + configKey: 'bidder', + auctionConfig: fledgeAuctionConfig, + }] }); + }); - it('should collect auction configs and route them to GPT at end of auction', () => { - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); - const cf1 = {...fledgeAuctionConfig, id: 1, seller: 'b1'}; - const cf2 = {...fledgeAuctionConfig, id: 2, seller: 'b2'}; - fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au1'}, cf1); - fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au2'}, cf2); - events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); - sinon.assert.calledWith(gptUtils.getGptSlotForAdUnitCode, 'au1'); - sinon.assert.calledWith(gptUtils.getGptSlotForAdUnitCode, 'au2'); + describe('when reset = true', () => { + it('should reset GPT slot config', () => { + setGptConfig('au', [fledgeAuctionConfig]); + mockGptSlot.setConfig.resetHistory(); + gptUtils.getGptSlotForAdUnitCode.resetHistory(); + setGptConfig('au', [], true); + sinon.assert.calledWith(gptUtils.getGptSlotForAdUnitCode, 'au'); sinon.assert.calledWith(mockGptSlot.setConfig, { componentAuction: [{ - configKey: 'b1', - auctionConfig: cf1, + configKey: 'bidder', + auctionConfig: null }] }); + }); + + it('should reset only sellers with no fresh config', () => { + setGptConfig('au', [{seller: 's1'}, {seller: 's2'}]); + mockGptSlot.setConfig.resetHistory(); + setGptConfig('au', [{seller: 's1'}], true); sinon.assert.calledWith(mockGptSlot.setConfig, { componentAuction: [{ - configKey: 'b2', - auctionConfig: cf2, + configKey: 's1', + auctionConfig: {seller: 's1'} + }, { + configKey: 's2', + auctionConfig: null }] - }); + }) }); - it('should drop auction configs after end of auction', () => { - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); - events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); - fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au'}, fledgeAuctionConfig); - events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); + it('should not reset sellers that were already reset', () => { + setGptConfig('au', [{seller: 's1'}]); + setGptConfig('au', [], true); + mockGptSlot.setConfig.resetHistory(); + setGptConfig('au', [], true); sinon.assert.notCalled(mockGptSlot.setConfig); - }); + }) - it('should augment auctionSignals with FPD', () => { - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); - fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au1', ortb2: {fpd: 1}, ortb2Imp: {fpd: 2}}, fledgeAuctionConfig); - events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); + it('should keep track of configuration history by slot', () => { + setGptConfig('au1', [{seller: 's1'}]); + setGptConfig('au1', [{seller: 's2'}], false); + setGptConfig('au2', [{seller: 's3'}]); + mockGptSlot.setConfig.resetHistory(); + setGptConfig('au1', [], true); sinon.assert.calledWith(mockGptSlot.setConfig, { componentAuction: [{ - configKey: 'bidder', - auctionConfig: { - ...fledgeAuctionConfig, - auctionSignals: { - prebid: { - ortb2: {fpd: 1}, - ortb2Imp: {fpd: 2} - } - } - }, + configKey: 's1', + auctionConfig: null + }, { + configKey: 's2', + auctionConfig: null }] - }) - }) - - describe('floor signal', () => { - before(() => { - if (!getGlobal().convertCurrency) { - getGlobal().convertCurrency = () => null; - getGlobal().convertCurrency.mock = true; - } - }); - after(() => { - if (getGlobal().convertCurrency.mock) { - delete getGlobal().convertCurrency; - } - }); - - beforeEach(() => { - sandbox.stub(getGlobal(), 'convertCurrency').callsFake((amount, from, to) => { - if (from === to) return amount; - if (from === 'USD' && to === 'JPY') return amount * 100; - if (from === 'JPY' && to === 'USD') return amount / 100; - throw new Error('unexpected currency conversion'); - }); }); - + }) + }); + }); + describe('onAuctionConfig', () => { + [ + 'fledgeForGpt', + 'paapi.gpt' + ].forEach(namespace => { + describe(`using ${namespace} config`, () => { Object.entries({ - 'bids': (payload, values) => { - payload.bidsReceived = values - .map((val) => ({adUnitCode: 'au', cpm: val.amount, currency: val.cur})) - .concat([{adUnitCode: 'other', cpm: 10000, currency: 'EUR'}]) - }, - 'no bids': (payload, values) => { - payload.bidderRequests = values - .map((val) => ({bids: [{adUnitCode: 'au', getFloor: () => ({floor: val.amount, currency: val.cur})}]})) - .concat([{bids: {adUnitCode: 'other', getFloor: () => ({floor: -10000, currency: 'EUR'})}}]) - } - }).forEach(([tcase, setup]) => { - describe(`when auction has ${tcase}`, () => { - Object.entries({ - 'no currencies': { - values: [{amount: 1}, {amount: 100}, {amount: 10}, {amount: 100}], - 'bids': { - bidfloor: 100, - bidfloorcur: undefined - }, - 'no bids': { - bidfloor: 1, - bidfloorcur: undefined, - } - }, - 'only zero values': { - values: [{amount: 0, cur: 'USD'}, {amount: 0, cur: 'JPY'}], - 'bids': { - bidfloor: undefined, - bidfloorcur: undefined, - }, - 'no bids': { - bidfloor: undefined, - bidfloorcur: undefined, - } - }, - 'matching currencies': { - values: [{amount: 10, cur: 'JPY'}, {amount: 100, cur: 'JPY'}], - 'bids': { - bidfloor: 100, - bidfloorcur: 'JPY', - }, - 'no bids': { - bidfloor: 10, - bidfloorcur: 'JPY', - } - }, - 'mixed currencies': { - values: [{amount: 10, cur: 'USD'}, {amount: 10, cur: 'JPY'}], - 'bids': { - bidfloor: 10, - bidfloorcur: 'USD' - }, - 'no bids': { - bidfloor: 10, - bidfloorcur: 'JPY', - } - } - }).forEach(([t, testConfig]) => { - const values = testConfig.values; - const {bidfloor, bidfloorcur} = testConfig[tcase]; - - describe(`with ${t}`, () => { - let payload; - beforeEach(() => { - payload = {auctionId: 'aid'}; - setup(payload, values); - }); + 'omitted': [undefined, true], + 'enabled': [true, true], + 'disabled': [false, false] + }).forEach(([t, [autoconfig, shouldSetConfig]]) => { + describe(`when autoconfig is ${t}`, () => { + beforeEach(() => { + const cfg = {}; + deepSetValue(cfg, `${namespace}.autoconfig`, autoconfig); + config.setConfig(cfg); + }); + afterEach(() => { + config.resetConfig(); + }); - it('should populate bidfloor/bidfloorcur', () => { - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); - fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au'}, fledgeAuctionConfig); - events.emit(CONSTANTS.EVENTS.AUCTION_END, payload); - sinon.assert.calledWith(mockGptSlot.setConfig, sinon.match(arg => { - return arg.componentAuction.some(au => au.auctionConfig.auctionSignals?.prebid?.bidfloor === bidfloor && au.auctionConfig.auctionSignals?.prebid?.bidfloorcur === bidfloorcur) - })) - }) - }); + it(`should ${shouldSetConfig ? '' : 'NOT'} set GPT slot configuration`, () => { + const auctionConfig = {componentAuctions: [{seller: 'mock1'}, {seller: 'mock2'}]}; + const setGptConfig = sinon.stub(); + const markAsUsed = sinon.stub(); + onAuctionConfigFactory(setGptConfig)('aid', {au1: auctionConfig, au2: null}, markAsUsed); + if (shouldSetConfig) { + sinon.assert.calledWith(setGptConfig, 'au1', auctionConfig.componentAuctions); + sinon.assert.calledWith(setGptConfig, 'au2', []); + sinon.assert.calledWith(markAsUsed, 'au1'); + } else { + sinon.assert.notCalled(setGptConfig); + sinon.assert.notCalled(markAsUsed); + } }); }) }) - }); - }); + }) + }) }); - - describe('fledgeEnabled', function () { - const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]])); - - before(function () { - // navigator.runAdAuction & co may not exist, so we can't stub it normally with - // sinon.stub(navigator, 'runAdAuction') or something - Object.keys(navProps).forEach(p => { - navigator[p] = sinon.stub(); - }); - hook.ready(); + describe('setPAAPIConfigForGpt', () => { + let getPAAPIConfig, setGptConfig, setPAAPIConfigForGPT; + beforeEach(() => { + getPAAPIConfig = sinon.stub(); + setGptConfig = sinon.stub(); + setPAAPIConfigForGPT = setPAAPIConfigFactory(getPAAPIConfig, setGptConfig); }); - after(function () { - Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig); - }); - - afterEach(function () { - config.resetConfig(); + Object.entries({ + missing: null, + empty: {} + }).forEach(([t, configs]) => { + it(`does not set GPT slot config when config is ${t}`, () => { + getPAAPIConfig.returns(configs); + setPAAPIConfigForGPT('mock-filters'); + sinon.assert.calledWith(getPAAPIConfig, 'mock-filters'); + sinon.assert.notCalled(setGptConfig); + }) }); - const adUnits = [{ - 'code': '/19968336/header-bid-tag1', - 'mediaTypes': { - 'banner': { - 'sizes': [[728, 90]] + it('sets GPT slot config for each ad unit that has PAAPI config, and resets the rest', () => { + const cfg = { + au1: { + componentAuctions: [{seller: 's1'}, {seller: 's2'}] }, - }, - 'bids': [ - { - 'bidder': 'appnexus', + au2: { + componentAuctions: [{seller: 's3'}] }, - { - 'bidder': 'rubicon', - }, - ] - }]; - function expectFledgeFlags(...enableFlags) { - const bidRequests = adapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() { - }, - [] - ); - - expect(bidRequests[0].bids[0].bidder).equals('appnexus'); - expect(bidRequests[0].fledgeEnabled).to.eql(enableFlags[0].enabled) - bidRequests[0].bids.forEach(bid => expect(bid.ortb2Imp.ext.ae).to.eql(enableFlags[0].ae)) - - expect(bidRequests[1].bids[0].bidder).equals('rubicon'); - expect(bidRequests[1].fledgeEnabled).to.eql(enableFlags[1].enabled) - bidRequests[1].bids.forEach(bid => expect(bid.ortb2Imp?.ext?.ae).to.eql(enableFlags[1].ae)); - } - - describe('with setBidderConfig()', () => { - it('should set fledgeEnabled correctly per bidder', function () { - config.setConfig({bidderSequence: 'fixed'}); - config.setBidderConfig({ - bidders: ['appnexus'], - config: { - fledgeEnabled: true, - defaultForSlots: 1, - } - }); - expectFledgeFlags({enabled: true, ae: 1}, {enabled: void 0, ae: void 0}); - }); - }); - - describe('with setConfig()', () => { - it('should set fledgeEnabled correctly per bidder', function () { - config.setConfig({ - bidderSequence: 'fixed', - fledgeForGpt: { - enabled: true, - bidders: ['appnexus'], - defaultForSlots: 1, - } - }); - expectFledgeFlags({enabled: true, ae: 1}, {enabled: void 0, ae: void 0}); - }); - - it('should set fledgeEnabled correctly for all bidders', function () { - config.setConfig({ - bidderSequence: 'fixed', - fledgeForGpt: { - enabled: true, - defaultForSlots: 1, - } - }); - expectFledgeFlags({enabled: true, ae: 1}, {enabled: true, ae: 1}); - }); - - it('should not override pub-defined ext.ae', () => { - config.setConfig({ - bidderSequence: 'fixed', - fledgeForGpt: { - enabled: true, - defaultForSlots: 1, - } - }); - Object.assign(adUnits[0], {ortb2Imp: {ext: {ae: 0}}}); - expectFledgeFlags({enabled: true, ae: 0}, {enabled: true, ae: 0}); - }) - }); - }); - - describe('ortb processors for fledge', () => { - it('imp.ext.ae should be removed if fledge is not enabled', () => { - const imp = {ext: {ae: 1}}; - setImpExtAe(imp, {}, {bidderRequest: {}}); - expect(imp.ext.ae).to.not.exist; - }); - it('imp.ext.ae should be left intact if fledge is enabled', () => { - const imp = {ext: {ae: 2}}; - setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true}}); - expect(imp.ext.ae).to.equal(2); - }); - describe('parseExtPrebidFledge', () => { - function packageConfigs(configs) { - return { - ext: { - prebid: { - fledge: { - auctionconfigs: configs - } - } - } - }; - } - - function generateImpCtx(fledgeFlags) { - return Object.fromEntries(Object.entries(fledgeFlags).map(([impid, fledgeEnabled]) => [impid, {imp: {ext: {ae: fledgeEnabled}}}])); - } - - function generateCfg(impid, ...ids) { - return ids.map((id) => ({impid, config: {id}})); - } - - function extractResult(ctx) { - return Object.fromEntries( - Object.entries(ctx) - .map(([impid, ctx]) => [impid, ctx.fledgeConfigs?.map(cfg => cfg.config.id)]) - .filter(([_, val]) => val != null) - ); + au3: null } - - it('should collect fledge configs by imp', () => { - const ctx = { - impContext: generateImpCtx({e1: 1, e2: 1, d1: 0}) - }; - const resp = packageConfigs( - generateCfg('e1', 1, 2, 3) - .concat(generateCfg('e2', 4) - .concat(generateCfg('d1', 5, 6))) - ); - parseExtPrebidFledge({}, resp, ctx); - expect(extractResult(ctx.impContext)).to.eql({ - e1: [1, 2, 3], - e2: [4], - }); - }); - it('should not choke if fledge config references unknown imp', () => { - const ctx = {impContext: generateImpCtx({i: 1})}; - const resp = packageConfigs(generateCfg('unknown', 1)); - parseExtPrebidFledge({}, resp, ctx); - expect(extractResult(ctx.impContext)).to.eql({}); - }); - }); - describe('setResponseFledgeConfigs', () => { - it('should set fledgeAuctionConfigs paired with their corresponding bid id', () => { - const ctx = { - impContext: { - 1: { - bidRequest: {bidId: 'bid1'}, - fledgeConfigs: [{config: {id: 1}}, {config: {id: 2}}] - }, - 2: { - bidRequest: {bidId: 'bid2'}, - fledgeConfigs: [{config: {id: 3}}] - }, - 3: { - bidRequest: {bidId: 'bid3'} - } - } - }; - const resp = {}; - setResponseFledgeConfigs(resp, {}, ctx); - expect(resp.fledgeAuctionConfigs).to.eql([ - {bidId: 'bid1', config: {id: 1}}, - {bidId: 'bid1', config: {id: 2}}, - {bidId: 'bid2', config: {id: 3}}, - ]); - }); - it('should not set fledgeAuctionConfigs if none exist', () => { - const resp = {}; - setResponseFledgeConfigs(resp, {}, { - impContext: { - 1: { - fledgeConfigs: [] - }, - 2: {} - } - }); - expect(resp).to.eql({}); - }); + getPAAPIConfig.returns(cfg); + setPAAPIConfigForGPT('mock-filters'); + sinon.assert.calledWith(getPAAPIConfig, 'mock-filters'); + Object.entries(cfg).forEach(([au, config]) => { + sinon.assert.calledWith(setGptConfig, au, config?.componentAuctions ?? [], true); + }) }); - }); + }) }); diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js new file mode 100644 index 00000000000..3d264e87e51 --- /dev/null +++ b/test/spec/modules/paapi_spec.js @@ -0,0 +1,628 @@ +import {expect} from 'chai'; +import {config} from '../../../src/config.js'; +import adapterManager from '../../../src/adapterManager.js'; +import * as utils from '../../../src/utils.js'; +import {hook} from '../../../src/hook.js'; +import 'modules/appnexusBidAdapter.js'; +import 'modules/rubiconBidAdapter.js'; +import { + addComponentAuctionHook, + getPAAPIConfig, + parseExtPrebidFledge, + registerSubmodule, + setImpExtAe, + setResponseFledgeConfigs, + reset +} from 'modules/paapi.js'; +import * as events from 'src/events.js'; +import CONSTANTS from 'src/constants.json'; +import {getGlobal} from '../../../src/prebidGlobal.js'; +import {auctionManager} from '../../../src/auctionManager.js'; +import {stubAuctionIndex} from '../../helpers/indexStub.js'; +import {AuctionIndex} from '../../../src/auctionIndex.js'; + +describe('paapi module', () => { + let sandbox; + before(reset); + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + afterEach(() => { + sandbox.restore(); + reset(); + }); + + [ + 'fledgeForGpt', + 'paapi' + ].forEach(configNS => { + describe(`using ${configNS} for configuration`, () => { + describe('getPAAPIConfig', function () { + let nextFnSpy, fledgeAuctionConfig; + before(() => { + config.setConfig({[configNS]: {enabled: true}}); + }); + beforeEach(() => { + fledgeAuctionConfig = { + seller: 'bidder', + mock: 'config' + }; + nextFnSpy = sinon.spy(); + }); + + describe('on a single auction', function () { + const auctionId = 'aid'; + beforeEach(function () { + sandbox.stub(auctionManager, 'index').value(stubAuctionIndex({auctionId})); + }); + + it('should call next()', function () { + const request = {auctionId, adUnitCode: 'auc'}; + addComponentAuctionHook(nextFnSpy, request, fledgeAuctionConfig); + sinon.assert.calledWith(nextFnSpy, request, fledgeAuctionConfig); + }); + + describe('should collect auction configs', () => { + let cf1, cf2; + beforeEach(() => { + cf1 = {...fledgeAuctionConfig, id: 1, seller: 'b1'}; + cf2 = {...fledgeAuctionConfig, id: 2, seller: 'b2'}; + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, cf1); + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au2'}, cf2); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au1', 'au2', 'au3']}); + }); + + it('and make them available at end of auction', () => { + sinon.assert.match(getPAAPIConfig({auctionId}), { + au1: { + componentAuctions: [cf1] + }, + au2: { + componentAuctions: [cf2] + } + }); + }); + + it('and filter them by ad unit', () => { + const cfg = getPAAPIConfig({auctionId, adUnitCode: 'au1'}); + expect(Object.keys(cfg)).to.have.members(['au1']); + sinon.assert.match(cfg.au1, { + componentAuctions: [cf1] + }); + }); + + it('and not return them again', () => { + getPAAPIConfig(); + const cfg = getPAAPIConfig(); + expect(cfg).to.eql({}); + }); + + describe('includeBlanks = true', () => { + it('includes all ad units', () => { + const cfg = getPAAPIConfig({}, true); + expect(Object.keys(cfg)).to.have.members(['au1', 'au2', 'au3']); + expect(cfg.au3).to.eql(null); + }) + it('includes the targeted adUnit', () => { + expect(getPAAPIConfig({adUnitCode: 'au3'}, true)).to.eql({ + au3: null + }) + }); + it('includes the targeted auction', () => { + const cfg = getPAAPIConfig({auctionId}, true); + expect(Object.keys(cfg)).to.have.members(['au1', 'au2', 'au3']); + expect(cfg.au3).to.eql(null); + }); + it('does not include non-existing ad units', () => { + expect(getPAAPIConfig({adUnitCode: 'other'})).to.eql({}); + }); + it('does not include non-existing auctions', () => { + expect(getPAAPIConfig({auctionId: 'other'})).to.eql({}); + }) + }); + }); + + it('should drop auction configs after end of auction', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId}); + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au'}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId}); + expect(getPAAPIConfig({auctionId})).to.eql({}); + }); + + it('should augment auctionSignals with FPD', () => { + addComponentAuctionHook(nextFnSpy, { + auctionId, + adUnitCode: 'au1', + ortb2: {fpd: 1}, + ortb2Imp: {fpd: 2} + }, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId}); + sinon.assert.match(getPAAPIConfig({auctionId}), { + au1: { + componentAuctions: [{ + ...fledgeAuctionConfig, + auctionSignals: { + prebid: { + ortb2: {fpd: 1}, + ortb2Imp: {fpd: 2} + } + } + }] + } + }); + }); + + describe('submodules', () => { + let submods; + beforeEach(() => { + submods = [1, 2].map(i => ({ + name: `test${i}`, + onAuctionConfig: sinon.stub() + })); + submods.forEach(registerSubmodule); + }); + + describe('onAuctionConfig', () => { + const auctionId = 'aid'; + it('is invoked with null configs when there\'s no config', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au']}); + submods.forEach(submod => sinon.assert.calledWith(submod.onAuctionConfig, auctionId, {au: null})); + }); + it('is invoked with relevant configs', () => { + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, fledgeAuctionConfig); + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au2'}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au1', 'au2', 'au3']}); + submods.forEach(submod => { + sinon.assert.calledWith(submod.onAuctionConfig, auctionId, { + au1: {componentAuctions: [fledgeAuctionConfig]}, + au2: {componentAuctions: [fledgeAuctionConfig]}, + au3: null + }) + }); + }); + it('removes configs from getPAAPIConfig if the module calls markAsUsed', () => { + submods[0].onAuctionConfig.callsFake((auctionId, configs, markAsUsed) => { + markAsUsed('au1'); + }); + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au1']}); + expect(getPAAPIConfig()).to.eql({}); + }); + it('keeps them available if they do not', () => { + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au1']}); + expect(getPAAPIConfig()).to.not.be.empty; + }) + }); + }); + + describe('floor signal', () => { + before(() => { + if (!getGlobal().convertCurrency) { + getGlobal().convertCurrency = () => null; + getGlobal().convertCurrency.mock = true; + } + }); + after(() => { + if (getGlobal().convertCurrency.mock) { + delete getGlobal().convertCurrency; + } + }); + + beforeEach(() => { + sandbox.stub(getGlobal(), 'convertCurrency').callsFake((amount, from, to) => { + if (from === to) return amount; + if (from === 'USD' && to === 'JPY') return amount * 100; + if (from === 'JPY' && to === 'USD') return amount / 100; + throw new Error('unexpected currency conversion'); + }); + }); + + Object.entries({ + 'bids': (payload, values) => { + payload.bidsReceived = values + .map((val) => ({adUnitCode: 'au', cpm: val.amount, currency: val.cur})) + .concat([{adUnitCode: 'other', cpm: 10000, currency: 'EUR'}]); + }, + 'no bids': (payload, values) => { + payload.bidderRequests = values + .map((val) => ({ + bids: [{ + adUnitCode: 'au', + getFloor: () => ({floor: val.amount, currency: val.cur}) + }] + })) + .concat([{bids: {adUnitCode: 'other', getFloor: () => ({floor: -10000, currency: 'EUR'})}}]); + } + }).forEach(([tcase, setup]) => { + describe(`when auction has ${tcase}`, () => { + Object.entries({ + 'no currencies': { + values: [{amount: 1}, {amount: 100}, {amount: 10}, {amount: 100}], + 'bids': { + bidfloor: 100, + bidfloorcur: undefined + }, + 'no bids': { + bidfloor: 1, + bidfloorcur: undefined, + } + }, + 'only zero values': { + values: [{amount: 0, cur: 'USD'}, {amount: 0, cur: 'JPY'}], + 'bids': { + bidfloor: undefined, + bidfloorcur: undefined, + }, + 'no bids': { + bidfloor: undefined, + bidfloorcur: undefined, + } + }, + 'matching currencies': { + values: [{amount: 10, cur: 'JPY'}, {amount: 100, cur: 'JPY'}], + 'bids': { + bidfloor: 100, + bidfloorcur: 'JPY', + }, + 'no bids': { + bidfloor: 10, + bidfloorcur: 'JPY', + } + }, + 'mixed currencies': { + values: [{amount: 10, cur: 'USD'}, {amount: 10, cur: 'JPY'}], + 'bids': { + bidfloor: 10, + bidfloorcur: 'USD' + }, + 'no bids': { + bidfloor: 10, + bidfloorcur: 'JPY', + } + } + }).forEach(([t, testConfig]) => { + const values = testConfig.values; + const {bidfloor, bidfloorcur} = testConfig[tcase]; + + describe(`with ${t}`, () => { + let payload; + beforeEach(() => { + payload = {auctionId}; + setup(payload, values); + }); + + it('should populate bidfloor/bidfloorcur', () => { + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au'}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, payload); + const signals = getPAAPIConfig({auctionId}).au.componentAuctions[0].auctionSignals; + expect(signals.prebid?.bidfloor).to.eql(bidfloor); + expect(signals.prebid?.bidfloorcur).to.eql(bidfloorcur); + }); + }); + }); + }); + }); + }); + }); + + describe('with multiple auctions', () => { + const AUCTION1 = 'auction1'; + const AUCTION2 = 'auction2'; + + function mockAuction(auctionId) { + return { + getAuctionId() { + return auctionId; + } + }; + } + + function expectAdUnitsFromAuctions(actualConfig, auToAuctionMap) { + expect(Object.keys(actualConfig)).to.have.members(Object.keys(auToAuctionMap)); + Object.entries(actualConfig).forEach(([au, cfg]) => { + cfg.componentAuctions.forEach(cmp => expect(cmp.auctionId).to.eql(auToAuctionMap[au])); + }); + } + + let configs; + beforeEach(() => { + const mockAuctions = [mockAuction(AUCTION1), mockAuction(AUCTION2)]; + sandbox.stub(auctionManager, 'index').value(new AuctionIndex(() => mockAuctions)); + configs = {[AUCTION1]: {}, [AUCTION2]: {}}; + Object.entries({ + [AUCTION1]: [['au1', 'au2'], ['missing-1']], + [AUCTION2]: [['au2', 'au3'], []], + }).forEach(([auctionId, [adUnitCodes, noConfigAdUnitCodes]]) => { + adUnitCodes.forEach(adUnitCode => { + const cfg = {...fledgeAuctionConfig, auctionId, adUnitCode}; + configs[auctionId][adUnitCode] = cfg; + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode}, cfg); + }); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId, adUnitCodes: adUnitCodes.concat(noConfigAdUnitCodes)}); + }); + }); + + it('should filter by auction', () => { + expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION1}), {au1: AUCTION1, au2: AUCTION1}); + expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION2}), {au2: AUCTION2, au3: AUCTION2}); + }); + + it('should filter by auction and ad unit', () => { + expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION1, adUnitCode: 'au2'}), {au2: AUCTION1}); + expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION2, adUnitCode: 'au2'}), {au2: AUCTION2}); + }); + + it('should use last auction for each ad unit', () => { + expectAdUnitsFromAuctions(getPAAPIConfig(), {au1: AUCTION1, au2: AUCTION2, au3: AUCTION2}); + }); + + it('should filter by ad unit and use latest auction', () => { + expectAdUnitsFromAuctions(getPAAPIConfig({adUnitCode: 'au2'}), {au2: AUCTION2}); + }); + + it('should keep track of which configs were returned', () => { + expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION1}), {au1: AUCTION1, au2: AUCTION1}); + expect(getPAAPIConfig({auctionId: AUCTION1})).to.eql({}); + expectAdUnitsFromAuctions(getPAAPIConfig(), {au2: AUCTION2, au3: AUCTION2}); + }); + + describe('includeBlanks = true', () => { + Object.entries({ + 'auction with blanks': { + filters: {auctionId: AUCTION1}, + expected: {au1: true, au2: true, 'missing-1': false} + }, + 'blank adUnit in an auction': { + filters: {auctionId: AUCTION1, adUnitCode: 'missing-1'}, + expected: {'missing-1': false} + }, + 'non-existing auction': { + filters: {auctionId: 'other'}, + expected: {} + }, + 'non-existing adUnit in an auction': { + filters: {auctionId: AUCTION2, adUnitCode: 'other'}, + expected: {} + }, + 'non-existing ad unit': { + filters: {adUnitCode: 'other'}, + expected: {}, + }, + 'non existing ad unit in a non-existing auction': { + filters: {adUnitCode: 'other', auctionId: 'other'}, + expected: {} + }, + 'all ad units': { + filters: {}, + expected: {'au1': true, 'au2': true, 'missing-1': false, 'au3': true} + } + }).forEach(([t, {filters, expected}]) => { + it(t, () => { + const cfg = getPAAPIConfig(filters, true); + expect(Object.keys(cfg)).to.have.members(Object.keys(expected)); + Object.entries(expected).forEach(([au, shouldBeFilled]) => { + if (shouldBeFilled) { + expect(cfg[au]).to.not.be.null; + } else { + expect(cfg[au]).to.be.null; + } + }) + }) + }) + }); + }); + }); + + describe('markForFledge', function () { + const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]])); + + before(function () { + // navigator.runAdAuction & co may not exist, so we can't stub it normally with + // sinon.stub(navigator, 'runAdAuction') or something + Object.keys(navProps).forEach(p => { + navigator[p] = sinon.stub(); + }); + hook.ready(); + config.resetConfig(); + }); + + after(function () { + Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig); + }); + + afterEach(function () { + config.resetConfig(); + }); + + const adUnits = [{ + 'code': '/19968336/header-bid-tag1', + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + }, + }, + 'bids': [ + { + 'bidder': 'appnexus', + }, + { + 'bidder': 'rubicon', + }, + ] + }]; + + function expectFledgeFlags(...enableFlags) { + const bidRequests = Object.fromEntries( + adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() { + }, + [] + ).map(b => [b.bidderCode, b]) + ); + + expect(bidRequests.appnexus.fledgeEnabled).to.eql(enableFlags[0].enabled); + bidRequests.appnexus.bids.forEach(bid => expect(bid.ortb2Imp.ext.ae).to.eql(enableFlags[0].ae)); + + expect(bidRequests.rubicon.fledgeEnabled).to.eql(enableFlags[1].enabled); + bidRequests.rubicon.bids.forEach(bid => expect(bid.ortb2Imp?.ext?.ae).to.eql(enableFlags[1].ae)); + } + + describe('with setBidderConfig()', () => { + it('should set fledgeEnabled correctly per bidder', function () { + config.setBidderConfig({ + bidders: ['appnexus'], + config: { + defaultForSlots: 1, + fledgeEnabled: true + } + }); + expectFledgeFlags({enabled: true, ae: 1}, {enabled: void 0, ae: void 0}); + }); + }); + + describe('with setConfig()', () => { + it('should set fledgeEnabled correctly per bidder', function () { + config.setConfig({ + bidderSequence: 'fixed', + [configNS]: { + enabled: true, + bidders: ['appnexus'], + defaultForSlots: 1, + } + }); + expectFledgeFlags({enabled: true, ae: 1}, {enabled: false, ae: undefined}); + }); + + it('should set fledgeEnabled correctly for all bidders', function () { + config.setConfig({ + bidderSequence: 'fixed', + [configNS]: { + enabled: true, + defaultForSlots: 1, + } + }); + expectFledgeFlags({enabled: true, ae: 1}, {enabled: true, ae: 1}); + }); + + it('should not override pub-defined ext.ae', () => { + config.setConfig({ + bidderSequence: 'fixed', + [configNS]: { + enabled: true, + defaultForSlots: 1, + } + }); + Object.assign(adUnits[0], {ortb2Imp: {ext: {ae: 0}}}); + expectFledgeFlags({enabled: true, ae: 0}, {enabled: true, ae: 0}); + }); + }); + }); + }); + }); + + describe('ortb processors for fledge', () => { + it('imp.ext.ae should be removed if fledge is not enabled', () => { + const imp = {ext: {ae: 1}}; + setImpExtAe(imp, {}, {bidderRequest: {}}); + expect(imp.ext.ae).to.not.exist; + }); + it('imp.ext.ae should be left intact if fledge is enabled', () => { + const imp = {ext: {ae: 2}}; + setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true}}); + expect(imp.ext.ae).to.equal(2); + }); + describe('parseExtPrebidFledge', () => { + function packageConfigs(configs) { + return { + ext: { + prebid: { + fledge: { + auctionconfigs: configs + } + } + } + }; + } + + function generateImpCtx(fledgeFlags) { + return Object.fromEntries(Object.entries(fledgeFlags).map(([impid, fledgeEnabled]) => [impid, {imp: {ext: {ae: fledgeEnabled}}}])); + } + + function generateCfg(impid, ...ids) { + return ids.map((id) => ({impid, config: {id}})); + } + + function extractResult(ctx) { + return Object.fromEntries( + Object.entries(ctx) + .map(([impid, ctx]) => [impid, ctx.fledgeConfigs?.map(cfg => cfg.config.id)]) + .filter(([_, val]) => val != null) + ); + } + + it('should collect fledge configs by imp', () => { + const ctx = { + impContext: generateImpCtx({e1: 1, e2: 1, d1: 0}) + }; + const resp = packageConfigs( + generateCfg('e1', 1, 2, 3) + .concat(generateCfg('e2', 4) + .concat(generateCfg('d1', 5, 6))) + ); + parseExtPrebidFledge({}, resp, ctx); + expect(extractResult(ctx.impContext)).to.eql({ + e1: [1, 2, 3], + e2: [4], + }); + }); + it('should not choke if fledge config references unknown imp', () => { + const ctx = {impContext: generateImpCtx({i: 1})}; + const resp = packageConfigs(generateCfg('unknown', 1)); + parseExtPrebidFledge({}, resp, ctx); + expect(extractResult(ctx.impContext)).to.eql({}); + }); + }); + describe('setResponseFledgeConfigs', () => { + it('should set fledgeAuctionConfigs paired with their corresponding bid id', () => { + const ctx = { + impContext: { + 1: { + bidRequest: {bidId: 'bid1'}, + fledgeConfigs: [{config: {id: 1}}, {config: {id: 2}}] + }, + 2: { + bidRequest: {bidId: 'bid2'}, + fledgeConfigs: [{config: {id: 3}}] + }, + 3: { + bidRequest: {bidId: 'bid3'} + } + } + }; + const resp = {}; + setResponseFledgeConfigs(resp, {}, ctx); + expect(resp.fledgeAuctionConfigs).to.eql([ + {bidId: 'bid1', config: {id: 1}}, + {bidId: 'bid1', config: {id: 2}}, + {bidId: 'bid2', config: {id: 3}}, + ]); + }); + it('should not set fledgeAuctionConfigs if none exist', () => { + const resp = {}; + setResponseFledgeConfigs(resp, {}, { + impContext: { + 1: { + fledgeConfigs: [] + }, + 2: {} + } + }); + expect(resp).to.eql({}); + }); + }); + }); +}); From 24b6d7191770e2f438da046f339196414fc737b8 Mon Sep 17 00:00:00 2001 From: onetag-dev <38786435+onetag-dev@users.noreply.github.com> Date: Wed, 14 Feb 2024 19:02:30 +0100 Subject: [PATCH 07/21] Add Onetag topics iframe (#11091) Co-authored-by: onetag-dev --- modules/topicsFpdModule.js | 3 +++ modules/topicsFpdModule.md | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/modules/topicsFpdModule.js b/modules/topicsFpdModule.js index f07ab0c7c14..b5a90dacfb4 100644 --- a/modules/topicsFpdModule.js +++ b/modules/topicsFpdModule.js @@ -35,6 +35,9 @@ const bidderIframeList = { }, { bidder: 'improvedigital', iframeURL: 'https://hb.360yield.com/privacy-sandbox/topics.html' + }, { + bidder: 'onetag', + iframeURL: 'https://onetag-sys.com/static/topicsapi.html' }] } diff --git a/modules/topicsFpdModule.md b/modules/topicsFpdModule.md index a6bb14dca67..89eb03fb6df 100644 --- a/modules/topicsFpdModule.md +++ b/modules/topicsFpdModule.md @@ -52,6 +52,10 @@ pbjs.setConfig({ bidder: 'appnexus', iframeURL: 'https://appnexus.com:8080/topics/fpd/topic.html', // dummy URL expiry: 7 // Configurable expiry days + }, { + bidder: 'onetag', + iframeURL: 'https://onetag-sys.com/static/topicsapi.html', + expiry: 7 // Configurable expiry days }] } .... From 0b629557c16151e7953dbe40eae218055f3ba3af Mon Sep 17 00:00:00 2001 From: Viktor Dreiling <34981284+3link@users.noreply.github.com> Date: Wed, 14 Feb 2024 19:02:42 +0100 Subject: [PATCH 08/21] Use built-in sampling (#11041) --- modules/liveIntentAnalyticsAdapter.js | 11 ++----- .../liveIntentAnalyticsAdapter_spec.js | 32 ++++++++++++++++--- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/modules/liveIntentAnalyticsAdapter.js b/modules/liveIntentAnalyticsAdapter.js index ffe4f8f58b0..54402bcafc6 100644 --- a/modules/liveIntentAnalyticsAdapter.js +++ b/modules/liveIntentAnalyticsAdapter.js @@ -10,11 +10,8 @@ const ANALYTICS_TYPE = 'endpoint'; const URL = 'https://wba.liadm.com/analytic-events'; const GVL_ID = 148; const ADAPTER_CODE = 'liveintent'; -const DEFAULT_SAMPLING = 0.1; const DEFAULT_BID_WON_TIMEOUT = 2000; const { EVENTS: { AUCTION_END } } = CONSTANTS; -let initOptions = {}; -let isSampled; let bidWonTimeout; function handleAuctionEnd(args) { @@ -123,19 +120,15 @@ function ignoreUndefined(data) { let liAnalytics = Object.assign(adapter({URL, ANALYTICS_TYPE}), { track({ eventType, args }) { - if (eventType == AUCTION_END && args && isSampled) { handleAuctionEnd(args); } + if (eventType == AUCTION_END && args) { handleAuctionEnd(args); } } }); // save the base class function liAnalytics.originEnableAnalytics = liAnalytics.enableAnalytics; - // override enableAnalytics so we can get access to the config passed in from the page liAnalytics.enableAnalytics = function (config) { - initOptions = config.options; - const sampling = (initOptions && initOptions.sampling) ?? DEFAULT_SAMPLING; - isSampled = Math.random() < parseFloat(sampling); - bidWonTimeout = (initOptions && initOptions.bidWonTimeout) ?? DEFAULT_BID_WON_TIMEOUT; + bidWonTimeout = config?.options?.bidWonTimeout ?? DEFAULT_BID_WON_TIMEOUT; liAnalytics.originEnableAnalytics(config); // call the base class function }; diff --git a/test/spec/modules/liveIntentAnalyticsAdapter_spec.js b/test/spec/modules/liveIntentAnalyticsAdapter_spec.js index fa4c5cd8cad..d00bfbc7bb5 100644 --- a/test/spec/modules/liveIntentAnalyticsAdapter_spec.js +++ b/test/spec/modules/liveIntentAnalyticsAdapter_spec.js @@ -16,7 +16,7 @@ let events = require('src/events'); let constants = require('src/constants.json'); let auctionId = '99abbc81-c1f1-41cd-8f25-f7149244c897' -const config = { +const configWithSamplingAll = { provider: 'liveintent', options: { bidWonTimeout: 2000, @@ -24,6 +24,14 @@ const config = { } } +const configWithSamplingNone = { + provider: 'liveintent', + options: { + bidWonTimeout: 2000, + sampling: 0 + } +} + let args = { auctionId: auctionId, timestamp: 1660915379703, @@ -273,8 +281,8 @@ describe('LiveIntent Analytics Adapter ', () => { clock.restore(); }); - it('request is computed and sent correctly', () => { - liAnalytics.enableAnalytics(config); + it('request is computed and sent correctly when sampling is 1', () => { + liAnalytics.enableAnalytics(configWithSamplingAll); sandbox.stub(utils, 'generateUUID').returns(instanceId); sandbox.stub(refererDetection, 'getRefererInfo').returns({page: url}); sandbox.stub(auctionManager.index, 'getAuction').withArgs(auctionId).returns({ getWinningBids: () => winningBids }); @@ -288,7 +296,23 @@ describe('LiveIntent Analytics Adapter ', () => { it('track is called', () => { sandbox.stub(liAnalytics, 'track'); - liAnalytics.enableAnalytics(config); + liAnalytics.enableAnalytics(configWithSamplingAll); expectEvents().to.beTrackedBy(liAnalytics.track); }) + + it('no request is computed when sampling is 0', () => { + liAnalytics.enableAnalytics(configWithSamplingNone); + sandbox.stub(utils, 'generateUUID').returns(instanceId); + sandbox.stub(refererDetection, 'getRefererInfo').returns({page: url}); + sandbox.stub(auctionManager.index, 'getAuction').withArgs(auctionId).returns({ getWinningBids: () => winningBids }); + events.emit(constants.EVENTS.AUCTION_END, args); + clock.tick(2000); + expect(server.requests.length).to.equal(0); + }); + + it('track is not called', () => { + sandbox.stub(liAnalytics, 'track'); + liAnalytics.enableAnalytics(configWithSamplingNone); + sinon.assert.callCount(liAnalytics.track, 0); + }) }); From 5b4bfd9632f3d412c397f9ad0112e6335f1ccdbc Mon Sep 17 00:00:00 2001 From: Andrii Pukh <152202940+apukh-magnite@users.noreply.github.com> Date: Wed, 14 Feb 2024 20:29:35 +0200 Subject: [PATCH 09/21] Rubicon Bid Adapter: pass DSA fields (#10974) * Pass DSA fields through fastlane.json * adjusting field names to reflect IAB changes * adjust to new field names * Add DSA meta field for biiders * Add an unit test to handle DSA in response * Update the comments --------- Co-authored-by: bretg --- modules/rubiconBidAdapter.js | 25 ++++ test/spec/modules/rubiconBidAdapter_spec.js | 132 ++++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index daaf9a14b9f..813db65717e 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -697,6 +697,10 @@ export const spec = { bid.mediaType = ad.creative_type; } + if (ad.dsa && Object.keys(ad.dsa).length) { + bid.meta.dsa = ad.dsa; + } + if (ad.adomain) { bid.meta.advertiserDomains = Array.isArray(ad.adomain) ? ad.adomain : [ad.adomain]; } @@ -906,6 +910,7 @@ function applyFPD(bidRequest, mediaType, data) { let impExtData = deepAccess(bidRequest.ortb2Imp, 'ext.data') || {}; const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid'); + const dsa = deepAccess(fpd, 'regs.ext.dsa'); const SEGTAX = {user: [4], site: [1, 2, 5, 6]}; const MAP = {user: 'tg_v.', site: 'tg_i.', adserver: 'tg_i.dfp_ad_unit_code', pbadslot: 'tg_i.pbadslot', keywords: 'kw'}; const validate = function(prop, key, parentName) { @@ -961,6 +966,26 @@ function applyFPD(bidRequest, mediaType, data) { data['p_gpid'] = gpid; } + // add dsa signals + if (dsa && Object.keys(dsa).length) { + pick(dsa, [ + 'dsainfo', (dsainfo) => data['dsainfo'] = dsainfo, + 'dsarequired', (required) => data['dsarequired'] = required, + 'pubrender', (pubrender) => data['dsapubrender'] = pubrender, + 'datatopub', (datatopub) => data['dsadatatopubs'] = datatopub, + 'transparency', (transparency) => { + if (Array.isArray(transparency) && transparency.length) { + data['dsatransparency'] = transparency.reduce((param, transp) => { + if (param) { + param += '~~' + } + return param += `${transp.domain}~${transp.dsaparams.join('_')}` + }, '') + } + } + ]) + } + // only send one of pbadslot or dfp adunit code (prefer pbadslot) if (data['tg_i.pbadslot']) { delete data['tg_i.dfp_ad_unit_code']; diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index f0e33ce940e..62e494ea295 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -21,6 +21,7 @@ import 'modules/priceFloors.js'; import 'modules/multibid/index.js'; import adapterManager from 'src/adapterManager.js'; import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import { deepClone } from '../../../src/utils.js'; const INTEGRATION = `pbjs_lite_v$prebid.version$`; // $prebid.version$ will be substituted in by gulp in built prebid const PBS_INTEGRATION = 'pbjs'; @@ -1711,6 +1712,57 @@ describe('the rubicon adapter', function () { expect(data['p_gpid']).to.equal('/1233/sports&div1'); }); + describe('Pass DSA signals', function() { + const ortb2 = { + regs: { + ext: { + dsa: { + dsarequired: 3, + pubrender: 0, + datatopub: 2, + transparency: [ + { + domain: 'testdomain.com', + dsaparams: [1], + }, + { + domain: 'testdomain2.com', + dsaparams: [1, 2] + } + ] + } + } + } + } + it('should send dsa signals if \"ortb2.regs.ext.dsa\"', function() { + const expectedTransparency = 'testdomain.com~1~~testdomain2.com~1_2' + const [request] = spec.buildRequests(bidderRequest.bids.map((b) => ({...b, ortb2})), bidderRequest) + const data = parseQuery(request.data); + + expect(data).to.be.an('Object'); + expect(data).to.have.property('dsarequired'); + expect(data).to.have.property('dsapubrender'); + expect(data).to.have.property('dsadatatopubs'); + expect(data).to.have.property('dsatransparency'); + + expect(data['dsarequired']).to.equal(ortb2.regs.ext.dsa.dsarequired.toString()); + expect(data['dsapubrender']).to.equal(ortb2.regs.ext.dsa.pubrender.toString()); + expect(data['dsadatatopubs']).to.equal(ortb2.regs.ext.dsa.datatopub.toString()); + expect(data['dsatransparency']).to.equal(expectedTransparency) + }) + it('should return one transparency param', function() { + const expectedTransparency = 'testdomain.com~1'; + const ortb2Clone = deepClone(ortb2); + ortb2Clone.regs.ext.dsa.transparency.pop() + const [request] = spec.buildRequests(bidderRequest.bids.map((b) => ({...b, ortb2: ortb2Clone})), bidderRequest) + const data = parseQuery(request.data); + + expect(data).to.be.an('Object'); + expect(data).to.have.property('dsatransparency'); + expect(data['dsatransparency']).to.equal(expectedTransparency); + }) + }) + it('should send gpid and pbadslot since it is prefered over dfp code', function () { bidderRequest.bids[0].ortb2Imp = { ext: { @@ -3276,6 +3328,86 @@ describe('the rubicon adapter', function () { expect(bids[0].cpm).to.be.equal(0); }); + it('should handle DSA object from response', function() { + let response = { + 'status': 'ok', + 'account_id': 14062, + 'site_id': 70608, + 'zone_id': 530022, + 'size_id': 15, + 'alt_size_ids': [ + 43 + ], + 'tracking': '', + 'inventory': {}, + 'ads': [ + { + 'status': 'ok', + 'impression_id': '153dc240-8229-4604-b8f5-256933b9374c', + 'size_id': '15', + 'ad_id': '6', + 'adomain': ['test.com'], + 'advertiser': 7, + 'network': 8, + 'creative_id': 'crid-9', + 'type': 'script', + 'script': 'alert(\'foo\')', + 'campaign_id': 10, + 'cpm': 0.811, + 'targeting': [ + { + 'key': 'rpfl_14062', + 'values': [ + '15_tier_all_test' + ] + } + ], + 'dsa': { + 'behalf': 'Advertiser', + 'paid': 'Advertiser', + 'transparency': [{ + 'domain': 'dsp1domain.com', + 'dsaparams': [1, 2] + }], + 'adrender': 1 + } + }, + { + 'status': 'ok', + 'impression_id': '153dc240-8229-4604-b8f5-256933b9374d', + 'size_id': '43', + 'ad_id': '7', + 'adomain': ['test.com'], + 'advertiser': 7, + 'network': 8, + 'creative_id': 'crid-9', + 'type': 'script', + 'script': 'alert(\'foo\')', + 'campaign_id': 10, + 'cpm': 0.911, + 'targeting': [ + { + 'key': 'rpfl_14062', + 'values': [ + '43_tier_all_test' + ] + } + ], + 'dsa': {} + } + ] + }; + let bids = spec.interpretResponse({body: response}, { + bidRequest: bidderRequest.bids[0] + }); + expect(bids).to.be.lengthOf(2); + expect(bids[1].meta.dsa).to.have.property('behalf'); + expect(bids[1].meta.dsa).to.have.property('paid'); + + // if we dont have dsa field in response or the dsa object is empty + expect(bids[0].meta).to.not.have.property('dsa'); + }) + it('should create bids with matching requestIds if imp id matches', function () { let bidRequests = [{ 'bidder': 'rubicon', From f9584c68a1741d2dbfc21aacfb3d501af68a082b Mon Sep 17 00:00:00 2001 From: Denis <7009699+someden@users.noreply.github.com> Date: Wed, 14 Feb 2024 21:44:18 +0300 Subject: [PATCH 10/21] Fix build (#11098) --- gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 125dec93402..d035da4b0fc 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -88,7 +88,7 @@ function lint(done) { '!plugins/**/node_modules/**', './*.js' ], { base: './' }) - .pipe(eslint({ fix: !argv.nolintfix, quiet: !(argv.lintWarnings ?? true) })) + .pipe(eslint({ fix: !argv.nolintfix, quiet: !(typeof argv.lintWarnings === 'boolean' ? argv.lintWarnings : true) })) .pipe(eslint.format('stylish')) .pipe(eslint.failAfterError()) .pipe(gulpif(isFixed, gulp.dest('./'))); From 8e5a2b94d7a29e5ba212ac0108c4d2f335a5b3ea Mon Sep 17 00:00:00 2001 From: Aymeric Le Corre Date: Wed, 14 Feb 2024 20:40:22 +0100 Subject: [PATCH 11/21] Lucead Bid Adapter: Add new adapter (#11068) * Add Whitebox adapter * Add Lucead Bid Adapter * update maintainer * update endpoint url --- modules/luceadBidAdapter.js | 147 ++++++++++++++++++ modules/luceadBidAdapter.md | 27 ++++ src/adloader.js | 3 +- test/spec/modules/luceadBidAdapter_spec.js | 165 +++++++++++++++++++++ 4 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 modules/luceadBidAdapter.js create mode 100644 modules/luceadBidAdapter.md create mode 100644 test/spec/modules/luceadBidAdapter_spec.js diff --git a/modules/luceadBidAdapter.js b/modules/luceadBidAdapter.js new file mode 100644 index 00000000000..8958e8f3786 --- /dev/null +++ b/modules/luceadBidAdapter.js @@ -0,0 +1,147 @@ +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {loadExternalScript} from '../src/adloader.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {getUniqueIdentifierStr, logInfo} from '../src/utils.js'; +import {fetch} from '../src/ajax.js'; + +const bidderCode = 'lucead'; +let baseUrl = 'https://ayads.io'; +let staticUrl = 'https://s.ayads.io'; +let companionUrl = 'https://cdn.jsdelivr.net/gh/lucead/prebid-js-external-js-lucead@master/dist/prod.min.js'; +let endpointUrl = 'https://prebid.ayads.io/go'; +const defaultCurrency = 'EUR'; +const defaultTtl = 500; +const isDevEnv = location.hostname.endsWith('.ngrok-free.app'); + +function isBidRequestValid(bidRequest) { + return !!bidRequest?.params?.placementId; +} + +export function log(msg, obj) { + logInfo('Lucead - ' + msg, obj); +} + +function buildRequests(validBidRequests, bidderRequest) { + if (isDevEnv) { + baseUrl = `https://${location.hostname}`; + staticUrl = baseUrl; + companionUrl = `${staticUrl}/dist/prebid-companion.js`; + endpointUrl = `${baseUrl}/go`; + } + + log('buildRequests', { + validBidRequests, + bidderRequest, + }); + + const companionData = { + base_url: baseUrl, + static_url: staticUrl, + endpoint_url: endpointUrl, + request_id: bidderRequest.bidderRequestId, + validBidRequests, + bidderRequest, + getUniqueIdentifierStr, + ortbConverter, + }; + + loadExternalScript(companionUrl, bidderCode, () => window.ayads_prebid && window.ayads_prebid(companionData)); + + return validBidRequests.map(bidRequest => ({ + method: 'POST', + url: `${endpointUrl}/prebid/sub`, + 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, + }), + 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?.ad_id || '0', + netRevenue: response?.netRevenue || true, + ad: response?.ad || '', + meta: { + advertiserDomains: response?.advertiserDomains || [], + }, + }] : 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}]; + + return {bids, fledgeAuctionConfigs}; +} + +function report(type = 'impression', data = {}) { + // noinspection JSCheckFunctionSignatures + return fetch(`${endpointUrl}/report/${type}`, { + body: JSON.stringify(data), + method: 'POST', + contentType: 'text/plain' + }); +} + +function onBidWon(bid) { + log('Bid won', bid); + + return report(`impression`, { + bid_id: bid?.bidId, + ad_id: bid?.creativeId, + placement_id: bid?.params ? bid?.params[0]?.placementId : 0, + spent: bid?.cpm, + currency: bid?.currency, + }); +} + +function onTimeout(timeoutData) { + log('Timeout from adapter', timeoutData); +} + +export const spec = { + code: bidderCode, + // gvlid: BIDDER_GVLID, + aliases: [], + isBidRequestValid, + buildRequests, + interpretResponse, + onBidWon, + onTimeout, +}; + +// noinspection JSCheckFunctionSignatures +registerBidder(spec); diff --git a/modules/luceadBidAdapter.md b/modules/luceadBidAdapter.md new file mode 100644 index 00000000000..45fd3ec5301 --- /dev/null +++ b/modules/luceadBidAdapter.md @@ -0,0 +1,27 @@ +# Overview + +Module Name: Lucead Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@lucead.com + +# Description + +Module that connects to Lucead demand source to fetch bids. + +# Test Parameters +``` +const adUnits = [ + { + code: 'test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: "lucead", + params: { + placementId: '1', + } + } + ] + } + ]; +``` diff --git a/src/adloader.js b/src/adloader.js index f60955736bd..5309f3a3d42 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -31,7 +31,8 @@ const _approvedLoadExternalJSList = [ 'qortex', 'dynamicAdBoost', 'contxtful', - 'id5' + 'id5', + 'lucead', ] /** diff --git a/test/spec/modules/luceadBidAdapter_spec.js b/test/spec/modules/luceadBidAdapter_spec.js new file mode 100644 index 00000000000..fa8d76cc30b --- /dev/null +++ b/test/spec/modules/luceadBidAdapter_spec.js @@ -0,0 +1,165 @@ +/* eslint-disable prebid/validate-imports,no-undef */ +import { expect } from 'chai'; +import { spec } from 'modules/luceadBidAdapter.js'; +import sinon from 'sinon'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import {deepClone} from 'src/utils.js'; +import * as ajax from 'src/ajax.js'; + +describe('Lucead Adapter', () => { + describe('inherited functions', function () { + it('exists and is a function', function () { + // noinspection JSCheckFunctionSignatures + const adapter = newBidder(spec); + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid; + beforeEach(function () { + bid = { + bidder: 'lucead', + params: { + placementId: '1', + }, + }; + }); + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('onBidWon', function () { + let sandbox; + const bid = { foo: 'bar' }; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + it('should trigger impression pixel', function () { + sandbox.spy(ajax, 'fetch'); + spec.onBidWon(bid); + expect(ajax.fetch.args[0][0]).to.match(/report\/impression$/); + }); + + afterEach(function () { + sandbox.restore(); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + bidder: 'lucead', + adUnitCode: 'lucead_code', + bidId: 'abc1234', + sizes: [[1800, 1000], [640, 300]], + requestId: 'xyz654', + params: { + placementId: '123', + } + } + ]; + + const bidderRequest = { + bidderRequestId: '13aaa3df18bfe4', + bids: {} + }; + + it('should have a post method', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request[0].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); + }); + + it('should have an url that contains sub keyword', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request[0].url).to.match(/sub/); + }); + }); + + 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 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'}, + })}; + + it('should get correct bid response', function () { + const result = spec.interpretResponse(serverResponse, bidRequest); + + expect(Object.keys(result.bids[0])).to.have.members([ + 'requestId', + 'cpm', + 'width', + 'height', + 'currency', + 'ttl', + 'creativeId', + 'netRevenue', + 'ad', + 'meta', + ]); + }); + + it('should return bid empty response', function () { + const serverResponse = {body: {cpm: 0}}; + const bidRequest = {data: '{}'}; + const result = spec.interpretResponse(serverResponse, bidRequest); + expect(result.bids[0].ad).to.be.equal(''); + expect(result.bids[0].cpm).to.be.equal(0); + }); + + it('should add advertiserDomains', function () { + const bidRequest = {data: JSON.stringify({ + bidder: 'lucead', + params: { + placementId: '1', + } + })}; + + const result = spec.interpretResponse(serverResponse, bidRequest); + 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; + }); + }); +}); From 40e3f40c5613062a981616e39a80514b6b8c8460 Mon Sep 17 00:00:00 2001 From: Mikael Lundin Date: Wed, 14 Feb 2024 23:00:30 +0100 Subject: [PATCH 12/21] Adnuntius Bid Adapter: Allow user ID to be passed as parameter (#11029) * Removed linting issues * Fixed merge issues. * Bugfix on storageTool. * Change to pass user ID as a parameter to the adserver. * fetch user id from paraters comment. --------- Co-authored-by: Antonios Sarhanis --- modules/adnuntiusBidAdapter.js | 11 +++++++--- test/spec/modules/adnuntiusBidAdapter_spec.js | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js index a498d056513..02dd7453be8 100644 --- a/modules/adnuntiusBidAdapter.js +++ b/modules/adnuntiusBidAdapter.js @@ -103,11 +103,16 @@ const storageTool = (function () { storage.setDataInLocalStorage(META_DATA_KEY, JSON.stringify(metaDataForSaving)); }; - const getUsi = function (meta, ortb2) { - let usi = (meta && meta.usi) ? meta.usi : false; + const getUsi = function (meta, ortb2, bidderRequest) { + // Fetch user id from parameters. + const paramUsi = (bidderRequest.bids) ? bidderRequest.bids.find(bid => { + if (bid.params && bid.params.userId) return true + }).params.userId : false + let usi = (meta && meta.usi) ? meta.usi : false if (ortb2 && ortb2.user && ortb2.user.id) { usi = ortb2.user.id } + if (paramUsi) usi = paramUsi return usi; } @@ -131,7 +136,7 @@ const storageTool = (function () { refreshStorage: function (bidderRequest) { const ortb2 = bidderRequest.ortb2 || {}; metaInternal = getMetaInternal().reduce((a, entry) => ({ ...a, [entry.key]: entry.value }), {}); - metaInternal.usi = getUsi(metaInternal, ortb2); + metaInternal.usi = getUsi(metaInternal, ortb2, bidderRequest); if (!metaInternal.usi) { delete metaInternal.usi; } diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js index e109ca1829c..20795b59e8c 100644 --- a/test/spec/modules/adnuntiusBidAdapter_spec.js +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -660,6 +660,27 @@ describe('adnuntiusBidAdapter', function() { expect(request[0]).to.have.property('url') expect(request[0].url).to.equal(ENDPOINT_URL); }); + + it('should user in user', function () { + config.setBidderConfig({ + bidders: ['adnuntius'], + }); + const req = [ + { + bidId: 'adn-000000000008b6bc', + bidder: 'adnuntius', + params: { + auId: '000000000008b6bc', + network: 'adnuntius', + userId: 'different_user_id' + } + } + ] + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(req, { bids: req })); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('url') + expect(request[0].url).to.equal(`${ENDPOINT_URL_BASE}&userId=different_user_id`); + }); }); describe('user privacy', function() { From 13cbc7c53c52e856fdcd5d9522283d9609c2d103 Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 15 Feb 2024 14:01:31 +0100 Subject: [PATCH 13/21] Adagio Bid Adapter: add DSA support (#11096) --- modules/adagioBidAdapter.js | 7 ++- test/spec/modules/adagioBidAdapter_spec.js | 57 +++++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index efa6777a0dd..acd89ef72df 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -990,6 +990,9 @@ export const spec = { const syncEnabled = deepAccess(config.getConfig('userSync'), 'syncEnabled') const usIfr = syncEnabled && userSync.canBidderRegisterSync('iframe', 'adagio') + // We don't validate the dsa object in adapter and let our server do it. + const dsa = deepAccess(bidderRequest, 'ortb2.regs.ext.dsa'); + const aucId = generateUUID() const adUnits = validBidRequests.map(rawBidRequest => { @@ -1179,7 +1182,8 @@ export const spec = { coppa: coppa, ccpa: uspConsent, gpp: gppConsent.gpp, - gppSid: gppConsent.gppSid + gppSid: gppConsent.gppSid, + dsa: dsa // populated if exists }, schain: schain, user: { @@ -1216,6 +1220,7 @@ export const spec = { const bidReq = (find(bidRequest.data.adUnits, bid => bid.bidId === bidObj.requestId)); if (bidReq) { + // bidObj.meta is the `bidResponse.meta` object according to https://docs.prebid.org/dev-docs/bidder-adaptor.html#interpreting-the-response bidObj.meta = deepAccess(bidObj, 'meta', {}); bidObj.meta.mediaType = bidObj.mediaType; bidObj.meta.advertiserDomains = (Array.isArray(bidObj.aDomain) && bidObj.aDomain.length) ? bidObj.aDomain : []; diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index 38dbaf348fd..13c02cc9bae 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -975,6 +975,41 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.adUnits[0].gpid).to.exist.and.equal(gpid); }); }); + + describe('with DSA', function() { + it('should add DSA to the request', function() { + const dsaObject = { + dsarequired: 1, + pubrender: 1, + datatopub: 2, + transparency: [{ + domain: 'domain.com', + dsaparams: [1, 2] + }] + } + + const bid01 = new BidRequestBuilder().withParams().build(); + + const bidderRequest = new BidderRequestBuilder({ + ortb2: { + regs: { + ext: { + dsa: dsaObject + } + } + } + }).build(); + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests[0].data.regs.dsa).to.deep.equal(dsaObject); + }); + + it('should not add DSA to the request if not present', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + const bidderRequest = new BidderRequestBuilder().build(); + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests[0].data.regs.dsa).to.be.undefined; + }); + }) }); describe('interpretResponse()', function() { @@ -1129,7 +1164,7 @@ describe('Adagio bid adapter', () => { utilsMock.verify(); }); - describe('Response with video outstream', () => { + describe('Response with video outstream', function() { const bidRequestWithOutstream = utils.deepClone(bidRequest); bidRequestWithOutstream.data.adUnits[0].mediaTypes.video = { context: 'outstream', @@ -1202,7 +1237,7 @@ describe('Adagio bid adapter', () => { }); }); - describe('Response with native add', () => { + describe('Response with native add', function() { const serverResponseWithNative = utils.deepClone(serverResponse) serverResponseWithNative.body.bids[0].mediaType = 'native'; serverResponseWithNative.body.bids[0].admNative = { @@ -1379,6 +1414,24 @@ describe('Adagio bid adapter', () => { expect(r[0].native.javascriptTrackers).to.equal(expected); }); }); + + describe('Response with DSA', function() { + const dsaResponseObj = { + 'behalf': 'Advertiser', + 'paid': 'Advertiser', + 'transparency': { + 'domain': 'dsp1domain.com', + 'params': [1, 2] + }, + 'adrender': 1 + }; + + const serverResponseWithDsa = utils.deepClone(serverResponse); + serverResponseWithDsa.body.bids[0].meta.dsa = dsaResponseObj; + + const bidResponse = spec.interpretResponse(serverResponseWithDsa, bidRequest)[0]; + expect(bidResponse.meta.dsa).to.to.deep.equals(dsaResponseObj); + }) }); describe('getUserSyncs()', function() { From 9379982f8c1b72d78af5d5325230f9f6b5a07b1e Mon Sep 17 00:00:00 2001 From: Piotr Jaworski <109736938+piotrj-rtbh@users.noreply.github.com> Date: Thu, 15 Feb 2024 20:05:41 +0100 Subject: [PATCH 14/21] RTB House Bid Adapter: add DSA support (#11097) * RTB House adapter: add DSA support * RTB House: add DSA support with extended field control --- modules/rtbhouseBidAdapter.js | 51 ++++- test/spec/modules/rtbhouseBidAdapter_spec.js | 217 ++++++++++++++++++- 2 files changed, 255 insertions(+), 13 deletions(-) diff --git a/modules/rtbhouseBidAdapter.js b/modules/rtbhouseBidAdapter.js index 5f94ec3dea4..cfad8fce966 100644 --- a/modules/rtbhouseBidAdapter.js +++ b/modules/rtbhouseBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, isArray, logError, logInfo, mergeDeep} from '../src/utils.js'; +import {deepAccess, deepClone, isArray, logError, logInfo, mergeDeep, isEmpty, isPlainObject, isNumber, isStr} from '../src/utils.js'; import {getOrigin} from '../libraries/getOrigin/index.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; @@ -18,6 +18,12 @@ const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE]; const TTL = 55; const GVLID = 16; +const DSA_ATTRIBUTES = [ + { name: 'dsarequired', 'min': 0, 'max': 3 }, + { name: 'pubrender', 'min': 0, 'max': 2 }, + { name: 'datatopub', 'min': 0, 'max': 2 } +]; + // Codes defined by OpenRTB Native Ads 1.1 specification export const OPENRTB = { NATIVE: { @@ -95,6 +101,17 @@ export const spec = { } }); + const dsa = deepAccess(ortb2Params, 'regs.ext.dsa'); + if (validateDSA(dsa)) { + mergeDeep(request, { + regs: { + ext: { + dsa + } + } + }); + } + let computedEndpointUrl = ENDPOINT_URL; if (bidderRequest.fledgeEnabled) { @@ -133,7 +150,13 @@ export const spec = { } else { interpretedBid = interpretBannerBid(serverBid); } - if (serverBid.ext) interpretedBid.ext = serverBid.ext; + + if (serverBid.ext) { + interpretedBid.ext = deepClone(serverBid.ext); + if (serverBid.ext.dsa) { + interpretedBid.meta = Object.assign({}, interpretedBid.meta, { dsa: serverBid.ext.dsa }); + } + } bids.push(interpretedBid); }); @@ -518,3 +541,27 @@ function interpretNativeAd(adm) { }); return result; } + +/** + * https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md + * + * @param {object} dsa + * @returns {boolean} whether dsa object contains valid attributes values + */ +function validateDSA(dsa) { + if (isEmpty(dsa) || !isPlainObject(dsa)) return false; + + return DSA_ATTRIBUTES.reduce((prev, attr) => { + const dsaEntry = dsa[attr.name]; + return prev && ( + !dsa.hasOwnProperty(attr.name) || + (isNumber(dsaEntry) && dsaEntry >= attr.min && dsaEntry <= attr.max) + ) + }, true) && + (!dsa.hasOwnProperty('transparency') || + (isArray(dsa.transparency) && dsa.transparency.every( + v => isPlainObject(v) && isStr(v.domain) && v.domain && isArray(v.dsaparams) && + v.dsaparams.every(x => isNumber(x)) + )) + ) +} diff --git a/test/spec/modules/rtbhouseBidAdapter_spec.js b/test/spec/modules/rtbhouseBidAdapter_spec.js index 0b944dcb077..77b746b9b69 100644 --- a/test/spec/modules/rtbhouseBidAdapter_spec.js +++ b/test/spec/modules/rtbhouseBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { OPENRTB, spec } from 'modules/rtbhouseBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; +import { mergeDeep } from '../../../src/utils'; describe('RTBHouseAdapter', () => { const adapter = newBidder(spec); @@ -304,6 +305,152 @@ describe('RTBHouseAdapter', () => { expect(data.user).to.nested.include({'ext.data': 'some user data'}); }); + context('DSA', () => { + const validDSAObject = { + 'dsarequired': 3, + 'pubrender': 0, + 'datatopub': 2, + 'transparency': [ + { + 'domain': 'platform1domain.com', + 'dsaparams': [1] + }, + { + 'domain': 'SSP2domain.com', + 'dsaparams': [1, 2] + } + ] + }; + const invalidDSAObjects = [ + -1, + 0, + '', + 'x', + true, + [], + [1], + {}, + { + 'dsarequired': -1 + }, + { + 'pubrender': -1 + }, + { + 'datatopub': -1 + }, + { + 'dsarequired': 4 + }, + { + 'pubrender': 3 + }, + { + 'datatopub': 3 + }, + { + 'dsarequired': '1' + }, + { + 'pubrender': '1' + }, + { + 'datatopub': '1' + }, + { + 'transparency': '1' + }, + { + 'transparency': 2 + }, + { + 'transparency': [ + 1, 2 + ] + }, + { + 'transparency': [ + { + domain: '', + dsaparams: [] + } + ] + }, + { + 'transparency': [ + { + domain: 'x', + dsaparams: null + } + ] + }, + { + 'transparency': [ + { + domain: 'x', + dsaparams: [1, '2'] + } + ] + }, + ]; + let bidRequest; + + beforeEach(() => { + bidRequest = Object.assign([], bidRequests); + }); + + it('should add dsa information to the request via bidderRequest.ortb2.regs.ext.dsa', function () { + const localBidderRequest = { + ...bidderRequest, + ortb2: { + regs: { + ext: { + dsa: validDSAObject + } + } + } + }; + + const request = spec.buildRequests(bidRequest, localBidderRequest); + const data = JSON.parse(request.data); + + expect(data).to.have.nested.property('regs.ext.dsa'); + expect(data.regs.ext.dsa.dsarequired).to.equal(3); + expect(data.regs.ext.dsa.pubrender).to.equal(0); + expect(data.regs.ext.dsa.datatopub).to.equal(2); + expect(data.regs.ext.dsa.transparency).to.deep.equal([ + { + 'domain': 'platform1domain.com', + 'dsaparams': [1] + }, + { + 'domain': 'SSP2domain.com', + 'dsaparams': [1, 2] + } + ]); + }); + + invalidDSAObjects.forEach((invalidDSA, index) => { + it(`should not add dsa information to the request via bidderRequest.ortb2.regs.ext.dsa; test# ${index}`, function () { + const localBidderRequest = { + ...bidderRequest, + ortb2: { + regs: { + ext: { + dsa: invalidDSA + } + } + } + }; + + const request = spec.buildRequests(bidRequest, localBidderRequest); + const data = JSON.parse(request.data); + + expect(data).to.not.have.nested.property('regs.ext.dsa'); + }); + }); + }); + context('FLEDGE', function() { afterEach(function () { config.resetConfig(); @@ -563,17 +710,20 @@ describe('RTBHouseAdapter', () => { }); describe('interpretResponse', function () { - let response = [{ - 'id': 'bidder_imp_identifier', - 'impid': '552b8922e28f27', - 'price': 0.5, - 'adid': 'Ad_Identifier', - 'adm': '', - 'adomain': ['rtbhouse.com'], - 'cid': 'Ad_Identifier', - 'w': 300, - 'h': 250 - }]; + let response; + beforeEach(() => { + response = [{ + 'id': 'bidder_imp_identifier', + 'impid': '552b8922e28f27', + 'price': 0.5, + 'adid': 'Ad_Identifier', + 'adm': '', + 'adomain': ['rtbhouse.com'], + 'cid': 'Ad_Identifier', + 'w': 300, + 'h': 250 + }]; + }); let fledgeResponse = { 'id': 'bid-identifier', @@ -638,6 +788,51 @@ describe('RTBHouseAdapter', () => { }); }); + context('when the response contains DSA object', function () { + it('should get correct bid response', function () { + const dsa = { + 'dsa': { + 'behalf': 'Advertiser', + 'paid': 'Advertiser', + 'transparency': [{ + 'domain': 'dsp1domain.com', + 'dsaparams': [1, 2] + }], + 'adrender': 1 + } + }; + mergeDeep(response[0], { ext: dsa }); + + const expectedResponse = [ + { + 'requestId': '552b8922e28f27', + 'cpm': 0.5, + 'creativeId': 29681110, + 'width': 300, + 'height': 250, + 'ad': '', + 'mediaType': 'banner', + 'currency': 'USD', + 'ttl': 300, + 'meta': { + 'advertiserDomains': ['rtbhouse.com'], + ...dsa + }, + 'netRevenue': true, + ext: { ...dsa } + } + ]; + let bidderRequest; + let result = spec.interpretResponse({body: response}, {bidderRequest}); + + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + expect(result[0]).to.have.nested.property('meta.dsa'); + expect(result[0]).to.have.nested.property('ext.dsa'); + expect(result[0].meta.dsa).to.deep.equal(expectedResponse[0].meta.dsa); + expect(result[0].ext.dsa).to.deep.equal(expectedResponse[0].meta.dsa); + }); + }); + describe('native', () => { const adm = { native: { From 57325f2279dfd320591663f72d1eab0cb89d1274 Mon Sep 17 00:00:00 2001 From: The Moneytizer <155010605+themoneytizer@users.noreply.github.com> Date: Thu, 15 Feb 2024 14:58:00 -0500 Subject: [PATCH 15/21] The Moneytizer Bid Adapter: initial release (#11047) --- modules/themoneytizerBidAdapter.js | 102 +++++++ modules/themoneytizerBidAdapter.md | 44 +++ .../modules/themoneytizerBidAdapter_spec.js | 289 ++++++++++++++++++ 3 files changed, 435 insertions(+) create mode 100644 modules/themoneytizerBidAdapter.js create mode 100644 modules/themoneytizerBidAdapter.md create mode 100644 test/spec/modules/themoneytizerBidAdapter_spec.js diff --git a/modules/themoneytizerBidAdapter.js b/modules/themoneytizerBidAdapter.js new file mode 100644 index 00000000000..9f187478fa7 --- /dev/null +++ b/modules/themoneytizerBidAdapter.js @@ -0,0 +1,102 @@ +import { logInfo, logWarn } from '../src/utils.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'themoneytizer'; +const GVLID = 1265; +const ENDPOINT_URL = 'https://ads.biddertmz.com/m/'; + +export const spec = { + aliases: [BIDDER_CODE], + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + gvlid: GVLID, + + isBidRequestValid: function (bid) { + if (!(bid && bid.params.pid)) { + logWarn('Invalid bid request - missing required bid params'); + return false; + } + + return true; + }, + + buildRequests: function (validBidRequests, bidderRequest) { + return validBidRequests.map((bidRequest) => { + const payload = { + ext: bidRequest.ortb2Imp.ext, + params: bidRequest.params, + size: bidRequest.mediaTypes, + adunit: bidRequest.adUnitCode, + request_id: bidRequest.bidId, + timeout: bidderRequest.timeout, + ortb2: bidderRequest.ortb2, + eids: bidRequest.userIdAsEids, + id: bidRequest.auctionId, + schain: bidRequest.schain, + version: '$prebid.version$', + excl_sync: window.tmzrBidderExclSync + }; + + const baseUrl = bidRequest.params.baseUrl || ENDPOINT_URL; + + if (bidderRequest && bidderRequest.refererInfo) { + payload.referer = bidderRequest.refererInfo.topmostLocation; + payload.referer_canonical = bidderRequest.refererInfo.canonicalUrl; + } + + if (bidderRequest && bidderRequest.gdprConsent) { + payload.consent_string = bidderRequest.gdprConsent.consentString; + payload.consent_required = bidderRequest.gdprConsent.gdprApplies; + } + + if (bidRequest.params.test) { + payload.test = bidRequest.params.test; + } + + payload.userEids = bidRequest.userIdAsEids || []; + + return { + method: 'POST', + url: baseUrl, + data: JSON.stringify(payload), + }; + }); + }, + + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = []; + const response = serverResponse.body; + + if (response && response.bid && !response.timeout && !!response.bid.ad) { + bidResponses.push(response.bid); + } + + return bidResponses; + }, + getUserSyncs: function (syncOptions, serverResponses) { + if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { + return []; + } + + let s = []; + serverResponses.map((c) => { + if (c.body.c_sync) { + c.body.c_sync.bidder_status.map((p) => { + if (p.usersync.type === 'redirect') { + p.usersync.type = 'image'; + } + s.push(p.usersync); + }) + } + }); + + return s; + }, + + onTimeout: function onTimeout(timeoutData) { + logInfo('The Moneytizer - Timeout from adapter', timeoutData); + }, +}; + +registerBidder(spec); diff --git a/modules/themoneytizerBidAdapter.md b/modules/themoneytizerBidAdapter.md new file mode 100644 index 00000000000..5515013575c --- /dev/null +++ b/modules/themoneytizerBidAdapter.md @@ -0,0 +1,44 @@ +# Overview + +``` +Module Name: The Moneytizer Bid Adapter +Module Type: Bidder Adapter +Maintainer: tech@themoneytizer.com +``` + +## Description + +Module that connects to The Moneytizer demand sources + +## Bid Parameters + +| Key | Required | Example | Description | +| --------------- | -------- | ---------------------------------------------| ---------------------------------------| +| `pid` | yes | `12345` | The Moneytizer's publisher token | +| `test` | no | `1` | Set to 1 to receive a test bid response| +| `baseUrl` | no | `'https://custom-endpoint.biddertmz.com/m/'` | Call on custom endpoint | + +## Test parameters + +```js + +var adUnits = [ + { + code: 'your-adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bids: [ + { + bidder: "themoneytizer", + params: { + pid: -1, + test: 1 + }, + }, + ], + }, +]; +``` diff --git a/test/spec/modules/themoneytizerBidAdapter_spec.js b/test/spec/modules/themoneytizerBidAdapter_spec.js new file mode 100644 index 00000000000..8cff7a57e69 --- /dev/null +++ b/test/spec/modules/themoneytizerBidAdapter_spec.js @@ -0,0 +1,289 @@ +import { spec } from '../../../modules/themoneytizerBidAdapter.js' + +const ENDPOINT_URL = 'https://ads.biddertmz.com/m/'; + +const VALID_BID_BANNER = { + bidder: 'themoneytizer', + ortb2Imp: { + ext: {} + }, + params: { + pid: 123456, + }, + mediaTypes: { + banner: { + sizes: [[970, 250]] + } + }, + adUnitCode: 'ad-unit-code', + bidId: '82376dbe72be72', + timeout: 3000, + ortb2: {}, + userIdAsEids: [], + auctionId: '123456-abcdef-7890', + schain: {}, +} + +const VALID_TEST_BID_BANNER = { + bidder: 'themoneytizer', + ortb2Imp: { + ext: {} + }, + params: { + pid: 123456, + test: 1, + baseUrl: 'https://custom-endpoint.biddertmz.com/m/' + }, + mediaTypes: { + banner: { + sizes: [[970, 250]] + } + }, + adUnitCode: 'ad-unit-code', + bidId: '82376dbe72be72', + timeout: 3000, + ortb2: {}, + userIdAsEids: [], + auctionId: '123456-abcdef-7890', + schain: {} +} + +const BIDDER_REQUEST_BANNER = { + bids: [VALID_BID_BANNER, VALID_TEST_BID_BANNER], + refererInfo: { + topmostLocation: 'http://prebid.org/', + canonicalUrl: 'http://prebid.org/' + }, + gdprConsent: { + gdprApplies: true, + consentString: 'abcdefghxyz' + } +} + +const SERVER_RESPONSE = { + c_sync: { + status: 'ok', + bidder_status: [ + { + bidder: 'bidder-A', + usersync: { + url: 'https://syncurl.com', + type: 'redirect' + } + }, + { + bidder: 'bidder-B', + usersync: { + url: 'https://syncurl2.com', + type: 'image' + } + } + ] + }, + bid: { + requestId: '17750222eb16825', + cpm: 0.098, + currency: 'USD', + width: 300, + height: 600, + creativeId: '44368852571075698202250', + dealId: '', + netRevenue: true, + ttl: 5, + ad: '

This is an ad

', + mediaType: 'banner', + } +}; + +describe('The Moneytizer Bidder Adapter', function () { + describe('codes', function () { + it('should return a bidder code of themoneytizer', function () { + expect(spec.code).to.equal('themoneytizer'); + }); + }); + + describe('gvlid', function () { + it('should expose gvlid', function () { + expect(spec.gvlid).to.equal(1265) + }); + }); + + describe('isBidRequestValid', function () { + it('should return true for a bid with all required fields', function () { + const validBid = spec.isBidRequestValid(VALID_BID_BANNER); + expect(validBid).to.be.true; + }); + + it('should return false for an invalid bid', function () { + const invalidBid = spec.isBidRequestValid(null); + expect(invalidBid).to.be.false; + }); + + it('should return false when params are incomplete', function () { + const bidWithIncompleteParams = { + ...VALID_BID_BANNER, + params: {} + }; + expect(spec.isBidRequestValid(bidWithIncompleteParams)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let requests, request, requests_test, request_test; + + before(function () { + requests = spec.buildRequests([VALID_BID_BANNER], BIDDER_REQUEST_BANNER); + request = requests[0]; + + requests_test = spec.buildRequests([VALID_TEST_BID_BANNER], BIDDER_REQUEST_BANNER); + request_test = requests_test[0]; + }); + + it('should build a request array for valid bids', function () { + expect(requests).to.be.an('array').that.is.not.empty; + }); + + it('should build a request array for valid test bids', function () { + expect(requests_test).to.be.an('array').that.is.not.empty; + }); + + it('should build a request with the correct method, URL, and data type', function () { + expect(request).to.include.keys(['method', 'url', 'data']); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT_URL); + expect(request.data).to.be.a('string'); + }); + + it('should build a test request with the correct method, URL, and data type', function () { + expect(request_test).to.include.keys(['method', 'url', 'data']); + expect(request_test.method).to.equal('POST'); + expect(request_test.url).to.equal(VALID_TEST_BID_BANNER.params.baseUrl); + expect(request_test.data).to.be.a('string'); + }); + + describe('Payload structure', function () { + let payload; + + before(function () { + payload = JSON.parse(request.data); + }); + + it('should have correct payload structure', function () { + expect(payload).to.be.an('object'); + expect(payload.size).to.be.an('object'); + expect(payload.params).to.be.an('object'); + }); + }); + + describe('Payload structure optional params', function () { + let payload; + + before(function () { + payload = JSON.parse(request_test.data); + }); + + it('should have correct params', function () { + expect(payload.params.pid).to.equal(123456); + }); + + it('should have correct referer info', function () { + expect(payload.referer).to.equal(BIDDER_REQUEST_BANNER.refererInfo.topmostLocation); + expect(payload.referer_canonical).to.equal(BIDDER_REQUEST_BANNER.refererInfo.canonicalUrl); + }); + + it('should have correct GDPR consent', function () { + expect(payload.consent_string).to.equal(BIDDER_REQUEST_BANNER.gdprConsent.consentString); + expect(payload.consent_required).to.equal(BIDDER_REQUEST_BANNER.gdprConsent.gdprApplies); + }); + }); + }); + + describe('interpretResponse', function () { + let bidResponse, receivedBid; + const responseBody = SERVER_RESPONSE; + + before(function () { + receivedBid = responseBody.bid; + const response = { body: responseBody }; + bidResponse = spec.interpretResponse(response, null); + }); + + it('should not return an empty response', function () { + expect(bidResponse).to.not.be.empty; + }); + + describe('Parsed Bid Object', function () { + let bid; + + before(function () { + bid = bidResponse[0]; + }); + + it('should not be empty', function () { + expect(bid).to.not.be.empty; + }); + + it('should correctly interpret ad markup', function () { + expect(bid.ad).to.equal(receivedBid.ad); + }); + + it('should correctly interpret CPM', function () { + expect(bid.cpm).to.equal(receivedBid.cpm); + }); + + it('should correctly interpret dimensions', function () { + expect(bid.height).to.equal(receivedBid.height); + expect(bid.width).to.equal(receivedBid.width); + }); + + it('should correctly interpret request ID', function () { + expect(bid.requestId).to.equal(receivedBid.requestId); + }); + }); + }); + + describe('onTimeout', function () { + const timeoutData = [{ + timeout: null + }]; + + it('should exists and be a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should include timeoutData', function () { + expect(spec.onTimeout(timeoutData)).to.be.undefined; + }) + }); + + describe('getUserSyncs', function () { + const response = { body: SERVER_RESPONSE }; + + it('should have empty user sync with iframeEnabled to false and pixelEnabled to false', function () { + const result = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: false }, [response]); + + expect(result).to.be.empty; + }); + + it('should have user sync with iframeEnabled to true', function () { + const result = spec.getUserSyncs({ iframeEnabled: true }, [response]); + + expect(result).to.not.be.empty; + expect(result[0].type).to.equal('image'); + expect(result[0].url).to.equal(SERVER_RESPONSE.c_sync.bidder_status[0].usersync.url); + }); + + it('should have user sync with pixelEnabled to true', function () { + const result = spec.getUserSyncs({ pixelEnabled: true }, [response]); + + expect(result).to.not.be.empty; + expect(result[0].type).to.equal('image'); + expect(result[0].url).to.equal(SERVER_RESPONSE.c_sync.bidder_status[0].usersync.url); + }); + + it('should transform type redirect into image', function () { + const result = spec.getUserSyncs({ iframeEnabled: true }, [response]); + + expect(result[1].type).to.equal('image'); + }); + }); +}); From 45521111c5a2b6aa28397407b2ab92051c7c8b41 Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Thu, 15 Feb 2024 21:55:31 +0000 Subject: [PATCH 16/21] Prebid 8.37.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 4686462599c..d05031ed335 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.37.0-pre", + "version": "8.37.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 1954a8c1294..9406d6d5b3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.37.0-pre", + "version": "8.37.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 11c77af9a7f37b6562b0848cbb3a10b651b2a56f Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Thu, 15 Feb 2024 21:55:32 +0000 Subject: [PATCH 17/21] Increment version to 8.38.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 d05031ed335..592c1984509 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.37.0", + "version": "8.38.0-pre", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 9406d6d5b3c..60a42e6afd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.37.0", + "version": "8.38.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From a0194cf33bfe67f69744d646c32a02ee7447c287 Mon Sep 17 00:00:00 2001 From: asurovenko-zeta <80847074+asurovenko-zeta@users.noreply.github.com> Date: Sun, 18 Feb 2024 23:38:25 +0100 Subject: [PATCH 18/21] ZetaGlobalSsp Analytics Adapter: keep only needed fields in event (#11107) * ZetaGlobalSspAnalyticsAdapter: keep only needed fields in event * - --------- Co-authored-by: Surovenko Alexey Co-authored-by: Alexey Surovenko --- modules/zeta_global_sspAnalyticsAdapter.js | 44 +++++++++++++++--- .../zeta_global_sspAnalyticsAdapter_spec.js | 45 ++++++++++--------- 2 files changed, 61 insertions(+), 28 deletions(-) diff --git a/modules/zeta_global_sspAnalyticsAdapter.js b/modules/zeta_global_sspAnalyticsAdapter.js index 751b55ec673..1eb4cab93b0 100644 --- a/modules/zeta_global_sspAnalyticsAdapter.js +++ b/modules/zeta_global_sspAnalyticsAdapter.js @@ -92,14 +92,44 @@ function auctionEndHandler(args) { logInfo(LOG_PREFIX + 'handle ' + eventType + ' event'); const event = { - adUnitCodes: args.adUnitCodes, - adUnits: args.adUnits, - auctionEnd: args.auctionEnd, auctionId: args.auctionId, - bidderRequests: args.bidderRequests, - bidsReceived: args.bidsReceived, - noBids: args.noBids, - winningBids: args.winningBids + adUnits: args.adUnits, + bidderRequests: args.bidderRequests?.map(br => ({ + bidderCode: br?.bidderCode, + refererInfo: br?.refererInfo, + bids: br?.bids?.map(b => ({ + adUnitCode: b?.adUnitCode, + auctionId: b?.auctionId, + bidId: b?.bidId, + requestId: b?.requestId, + bidderCode: b?.bidderCode, + mediaTypes: b?.mediaTypes, + sizes: b?.sizes, + bidder: b?.bidder, + params: b?.params + })) + })), + bidsReceived: args.bidsReceived?.map(br => ({ + adId: br?.adId, + adserverTargeting: { + hb_adomain: br?.adserverTargeting?.hb_adomain + }, + cpm: br?.cpm, + creativeId: br?.creativeId, + mediaType: br?.mediaType, + renderer: br?.renderer, + size: br?.size, + timeToRespond: br?.timeToRespond, + adUnitCode: br?.adUnitCode, + auctionId: br?.auctionId, + bidId: br?.bidId, + requestId: br?.requestId, + bidderCode: br?.bidderCode, + mediaTypes: br?.mediaTypes, + sizes: br?.sizes, + bidder: br?.bidder, + params: br?.params + })) } // save zetaParams to cache diff --git a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js index 5194a6a526a..54b61f19506 100644 --- a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js @@ -9,7 +9,7 @@ let events = require('src/events'); const EVENTS = { AUCTION_END: { - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'timestamp': 1638441234544, 'auctionEnd': 1638441234784, 'auctionStatus': 'completed', @@ -61,7 +61,7 @@ const EVENTS = { 600 ] ], - 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83' + 'transactionId': '6b29369c' } ], 'adUnitCodes': [ @@ -70,7 +70,7 @@ const EVENTS = { 'bidderRequests': [ { 'bidderCode': 'zeta_global_ssp', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'bidderRequestId': '1207cb49191887', 'bids': [ { @@ -90,7 +90,7 @@ const EVENTS = { } }, 'adUnitCode': '/19968336/header-bid-tag-0', - 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83', + 'transactionId': '6b29369c', 'sizes': [ [ 300, @@ -103,7 +103,7 @@ const EVENTS = { ], 'bidId': '206be9a13236af', 'bidderRequestId': '1207cb49191887', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, @@ -126,7 +126,7 @@ const EVENTS = { }, { 'bidderCode': 'appnexus', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'bidderRequestId': '32b97f0a935422', 'bids': [ { @@ -149,7 +149,7 @@ const EVENTS = { } }, 'adUnitCode': '/19968336/header-bid-tag-0', - 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83', + 'transactionId': '6b29369c', 'sizes': [ [ 300, @@ -162,7 +162,7 @@ const EVENTS = { ], 'bidId': '41badc0e164c758', 'bidderRequestId': '32b97f0a935422', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, @@ -205,7 +205,7 @@ const EVENTS = { } }, 'adUnitCode': '/19968336/header-bid-tag-0', - 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83', + 'transactionId': '6b29369c', 'sizes': [ [ 300, @@ -218,7 +218,7 @@ const EVENTS = { ], 'bidId': '41badc0e164c758', 'bidderRequestId': '32b97f0a935422', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, @@ -243,12 +243,12 @@ const EVENTS = { 'netRevenue': true, 'meta': { 'advertiserDomains': [ - 'viaplay.fi' + 'example.adomain' ] }, 'originalCpm': 2.258302852806723, 'originalCurrency': 'USD', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'responseTimestamp': 1638441234670, 'requestTimestamp': 1638441234547, 'bidder': 'zeta_global_ssp', @@ -268,7 +268,7 @@ const EVENTS = { 'hb_size': '480x320', 'hb_source': 'client', 'hb_format': 'banner', - 'hb_adomain': 'viaplay.fi' + 'hb_adomain': 'example.adomain' } } ], @@ -311,12 +311,12 @@ const EVENTS = { 'netRevenue': true, 'meta': { 'advertiserDomains': [ - 'viaplay.fi' + 'example.adomain' ] }, 'originalCpm': 2.258302852806723, 'originalCurrency': 'USD', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'responseTimestamp': 1638441234670, 'requestTimestamp': 1638441234547, 'bidder': 'zeta_global_ssp', @@ -336,7 +336,7 @@ const EVENTS = { 'hb_size': '480x320', 'hb_source': 'client', 'hb_format': 'banner', - 'hb_adomain': 'viaplay.fi' + 'hb_adomain': 'example.adomain' }, 'status': 'rendered', 'params': [ @@ -409,16 +409,19 @@ describe('Zeta Global SSP Analytics Adapter', function() { const auctionEnd = JSON.parse(requests[0].requestBody); const auctionSucceeded = JSON.parse(requests[1].requestBody); - expect(auctionEnd.adUnitCodes[0]).to.be.equal('/19968336/header-bid-tag-0'); + expect(auctionEnd.adUnitCodes).to.be.undefined; expect(auctionEnd.adUnits[0].bids[0].bidder).to.be.equal('zeta_global_ssp'); - expect(auctionEnd.auctionEnd).to.be.equal(1638441234784); - expect(auctionEnd.auctionId).to.be.equal('75e394d9-ccce-4978-9238-91e6a1ac88a1'); + expect(auctionEnd.auctionEnd).to.be.undefined; + expect(auctionEnd.auctionId).to.be.equal('75e394d9'); expect(auctionEnd.bidderRequests[0].bidderCode).to.be.equal('zeta_global_ssp'); + expect(auctionEnd.bidderRequests[0].bids[0].bidId).to.be.equal('206be9a13236af'); + expect(auctionEnd.bidderRequests[0].bids[0].adUnitCode).to.be.equal('/19968336/header-bid-tag-0'); expect(auctionEnd.bidsReceived[0].bidderCode).to.be.equal('zeta_global_ssp'); - expect(auctionEnd.noBids[0].bidder).to.be.equal('appnexus'); + expect(auctionEnd.bidsReceived[0].adserverTargeting.hb_adomain).to.be.equal('example.adomain'); + expect(auctionEnd.bidsReceived[0].auctionId).to.be.equal('75e394d9'); expect(auctionSucceeded.adId).to.be.equal('5759bb3ef7be1e8'); - expect(auctionSucceeded.bid.auctionId).to.be.equal('75e394d9-ccce-4978-9238-91e6a1ac88a1'); + expect(auctionSucceeded.bid.auctionId).to.be.equal('75e394d9'); expect(auctionSucceeded.bid.requestId).to.be.equal('206be9a13236af'); expect(auctionSucceeded.bid.bidderCode).to.be.equal('zeta_global_ssp'); expect(auctionSucceeded.bid.creativeId).to.be.equal('456456456'); From cd328b676e1dc217d6e103ec0baf34fa40c73880 Mon Sep 17 00:00:00 2001 From: Trevor <42976142+trevoradbutler@users.noreply.github.com> Date: Mon, 19 Feb 2024 07:01:34 -0700 Subject: [PATCH 19/21] Add AdButler bid adapter (#11011) --- modules/adbutlerBidAdapter.js | 113 +++++++ modules/adbutlerBidAdapter.md | 31 ++ test/spec/modules/adbutlerBidAdapter_spec.js | 329 +++++++++++++++++++ 3 files changed, 473 insertions(+) create mode 100644 modules/adbutlerBidAdapter.js create mode 100644 modules/adbutlerBidAdapter.md create mode 100644 test/spec/modules/adbutlerBidAdapter_spec.js diff --git a/modules/adbutlerBidAdapter.js b/modules/adbutlerBidAdapter.js new file mode 100644 index 00000000000..de430a5c916 --- /dev/null +++ b/modules/adbutlerBidAdapter.js @@ -0,0 +1,113 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'adbutler'; + +function getTrackingPixelsMarkup(pixelURLs) { + return pixelURLs + .map(pixelURL => ``) + .join(); +} + +export const spec = { + code: BIDDER_CODE, + pageID: Math.floor(Math.random() * 10e6), + aliases: ['divreach', 'doceree'], + supportedMediaTypes: [BANNER], + + isBidRequestValid(bid) { + return !!(bid.params.accountID && bid.params.zoneID); + }, + + buildRequests(validBidRequests) { + const zoneCounters = {}; + + return utils._map(validBidRequests, function (bidRequest) { + const zoneID = bidRequest.params?.zoneID; + + zoneCounters[zoneID] ??= 0; + + const domain = bidRequest.params?.domain ?? 'servedbyadbutler.com'; + const adserveBase = `https://${domain}/adserve`; + const params = { + ...(bidRequest.params?.extra ?? {}), + ID: bidRequest.params?.accountID, + type: 'hbr', + setID: zoneID, + pid: spec.pageID, + place: zoneCounters[zoneID], + kw: bidRequest.params?.keyword, + }; + + const paramsString = Object.entries(params).map(([key, value]) => `${key}=${value}`).join(';'); + const requestURI = `${adserveBase}/;${paramsString};`; + + zoneCounters[zoneID]++; + + return { + method: 'GET', + url: requestURI, + data: {}, + bidRequest, + }; + }); + }, + + interpretResponse(serverResponse, serverRequest) { + const bidObj = serverRequest.bidRequest; + const response = serverResponse.body ?? {}; + + if (!bidObj || response.status !== 'SUCCESS') { + return []; + } + + const width = parseInt(response.width); + const height = parseInt(response.height); + + const sizeValid = (bidObj.mediaTypes?.banner?.sizes ?? []).some(([w, h]) => w === width && h === height); + + if (!sizeValid) { + return []; + } + + const cpm = response.cpm; + const minCPM = bidObj.params?.minCPM ?? null; + const maxCPM = bidObj.params?.maxCPM ?? null; + + if (minCPM !== null && cpm < minCPM) { + return []; + } + + if (maxCPM !== null && cpm > maxCPM) { + return []; + } + + let advertiserDomains = []; + + if (response.advertiser?.domain) { + advertiserDomains.push(response.advertiser.domain); + } + + const bidResponse = { + requestId: bidObj.bidId, + cpm, + currency: 'USD', + width, + height, + ad: response.ad_code + getTrackingPixelsMarkup(response.tracking_pixels), + ttl: 360, + creativeId: response.placement_id, + netRevenue: true, + meta: { + advertiserId: response.advertiser?.id, + advertiserName: response.advertiser?.name, + advertiserDomains, + }, + }; + + return [bidResponse]; + }, +}; + +registerBidder(spec); diff --git a/modules/adbutlerBidAdapter.md b/modules/adbutlerBidAdapter.md new file mode 100644 index 00000000000..88b5cf64475 --- /dev/null +++ b/modules/adbutlerBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +**Module Name**: AdButler Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: trevor@sparklit.com + +# Description + +Bid Adapter for creating a bid from an AdButler zone. + +# Test Parameters +``` + var adUnits = [ + { + code: 'display-div', + sizes: [[300, 250]], // a display size + bids: [ + { + bidder: "adbutler", + params: { + accountID: '181556', + zoneID: '705374', + keyword: 'red', //optional + minCPM: '1.00', //optional + maxCPM: '5.00' //optional + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/adbutlerBidAdapter_spec.js b/test/spec/modules/adbutlerBidAdapter_spec.js new file mode 100644 index 00000000000..6c38de717a3 --- /dev/null +++ b/test/spec/modules/adbutlerBidAdapter_spec.js @@ -0,0 +1,329 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adbutlerBidAdapter.js'; + +describe('AdButler adapter', function () { + let validBidRequests; + + beforeEach(function () { + validBidRequests = [ + { + bidder: 'adbutler', + params: { + accountID: '181556', + zoneID: '705374', + keyword: 'red', + minCPM: '1.00', + maxCPM: '5.00', + }, + placementCode: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + }, + bidId: '23acc48ad47af5', + auctionId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729', + }, + ]; + }); + + describe('for requests', function () { + describe('without account ID', function () { + it('rejects the bid', function () { + const invalidBid = { + bidder: 'adbutler', + params: { + zoneID: '210093', + }, + }; + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + }); + + describe('without a zone ID', function () { + it('rejects the bid', function () { + const invalidBid = { + bidder: 'adbutler', + params: { + accountID: '167283', + }, + }; + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + }); + + describe('with a valid bid', function () { + describe('with a custom domain', function () { + it('uses the custom domain', function () { + validBidRequests[0].params.domain = 'customadbutlerdomain.com'; + + const requests = spec.buildRequests(validBidRequests); + const requestURL = requests[0].url; + + expect(requestURL).to.have.string('customadbutlerdomain.com'); + }); + }); + + it('accepts the bid', function () { + const validBid = { + bidder: 'adbutler', + params: { + accountID: '167283', + zoneID: '210093', + }, + }; + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.equal(true); + }); + + it('sets default domain', function () { + const requests = spec.buildRequests(validBidRequests); + const request = requests[0]; + + let [domain] = request.url.split('/adserve/'); + + expect(domain).to.equal('https://servedbyadbutler.com'); + }); + + it('sets the keyword parameter', function () { + const requests = spec.buildRequests(validBidRequests); + const requestURL = requests[0].url; + + expect(requestURL).to.have.string(';kw=red;'); + }); + + describe('with extra params', function () { + beforeEach(function() { + validBidRequests[0].params.extra = { + foo: 'bar', + }; + }); + + it('sets the extra parameter', function () { + const requests = spec.buildRequests(validBidRequests); + const requestURL = requests[0].url; + + expect(requestURL).to.have.string(';foo=bar;'); + }); + }); + + describe('with multiple bids to the same zone', function () { + it('increments the place count', function () { + const requests = spec.buildRequests([validBidRequests[0], validBidRequests[0]]); + const firstRequest = requests[0].url; + const secondRequest = requests[1].url; + + expect(firstRequest).to.have.string(';place=0;'); + expect(secondRequest).to.have.string(';place=1;'); + }); + }); + }); + }); + + describe('for server responses', function () { + let serverResponse; + + describe('with no body', function () { + beforeEach(function() { + serverResponse = { + body: null, + }; + }); + + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(0); + }); + }); + + describe('with an incorrect size', function () { + beforeEach(function() { + serverResponse = { + body: { + status: 'SUCCESS', + account_id: 167283, + zone_id: 210083, + cpm: 1.5, + width: 728, + height: 90, + place: 0, + }, + }; + }); + + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(0); + }); + }); + + describe('with a failed status', function () { + beforeEach(function() { + serverResponse = { + body: { + status: 'NO_ELIGIBLE_ADS', + zone_id: 210083, + width: 300, + height: 250, + place: 0, + }, + }; + }); + + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(0); + }); + }); + + describe('with low CPM', function () { + beforeEach(function() { + serverResponse = { + body: { + status: 'SUCCESS', + account_id: 167283, + zone_id: 210093, + cpm: 0.75, + width: 300, + height: 250, + place: 0, + ad_code: '', + tracking_pixels: [], + }, + } + }); + + describe('with a minimum CPM', function () { + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + expect(bids).to.be.length(0); + }); + }); + + describe('with no minimum CPM', function () { + beforeEach(function() { + delete validBidRequests[0].params.minCPM; + }); + + it('returns a bid', function() { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(1); + }); + }); + }); + + describe('with high CPM', function () { + beforeEach(function() { + serverResponse = { + body: { + status: 'SUCCESS', + account_id: 167283, + zone_id: 210093, + cpm: 999, + width: 300, + height: 250, + place: 0, + ad_code: '', + tracking_pixels: [], + }, + } + }); + + describe('with a maximum CPM', function () { + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(0); + }); + }); + + describe('with no maximum CPM', function () { + beforeEach(function() { + delete validBidRequests[0].params.maxCPM; + }); + + it('returns a bid', function() { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(1); + }); + }); + }); + + describe('with a valid ad', function () { + beforeEach(function() { + serverResponse = { + body: { + status: 'SUCCESS', + account_id: 167283, + zone_id: 210093, + cpm: 1.5, + width: 300, + height: 250, + place: 0, + ad_code: '', + tracking_pixels: [ + 'http://tracking.pixel.com/params=info', + ], + }, + }; + }); + + it('returns a complete bid', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(1); + expect(bids[0].cpm).to.equal(1.5); + expect(bids[0].width).to.equal(300); + expect(bids[0].height).to.equal(250); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].ad).to.have.length.above(1); + expect(bids[0].ad).to.have.string('http://tracking.pixel.com/params=info'); + }); + + describe('for a bid request without banner media type', function () { + beforeEach(function() { + delete validBidRequests[0].mediaTypes.banner; + }); + + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(0); + }); + }); + + describe('with advertiser meta', function () { + beforeEach(function() { + serverResponse.body.advertiser = { + id: 123, + name: 'Advertiser Name', + domain: 'advertiser.com', + }; + }); + + it('returns a bid including advertiser meta', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(1); + expect(bids[0]).to.have.property('meta'); + expect(bids[0].meta.advertiserId).to.equal(123); + expect(bids[0].meta.advertiserName).to.equal('Advertiser Name'); + expect(bids[0].meta.advertiserDomains).to.contain('advertiser.com'); + }); + }); + }); + }); +}); From aaf295103387e0e900a6d8a79a425e321eb2b5b1 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Date: Mon, 19 Feb 2024 16:50:26 +0100 Subject: [PATCH 20/21] define split between exploratory and non exploratory sides of the deterministic sampling hash (#11104) --- modules/greenbidsAnalyticsAdapter.js | 20 ++++++++++++++++--- .../modules/greenbidsAnalyticsAdapter_spec.js | 16 +++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/modules/greenbidsAnalyticsAdapter.js b/modules/greenbidsAnalyticsAdapter.js index 9b23dba01b9..f60dee99efe 100644 --- a/modules/greenbidsAnalyticsAdapter.js +++ b/modules/greenbidsAnalyticsAdapter.js @@ -27,18 +27,24 @@ export const BIDDER_STATUS = { const analyticsOptions = {}; -export const isSampled = function(greenbidsId, samplingRate) { +export const isSampled = function(greenbidsId, samplingRate, exploratorySamplingSplit) { if (samplingRate < 0 || samplingRate > 1) { logWarn('Sampling rate must be between 0 and 1'); return true; } + const exploratorySamplingRate = samplingRate * exploratorySamplingSplit; + const throttledSamplingRate = samplingRate * (1.0 - exploratorySamplingSplit); const hashInt = parseInt(greenbidsId.slice(-4), 16); - return hashInt < samplingRate * (0xFFFF + 1); + const isPrimarySampled = hashInt < exploratorySamplingRate * (0xFFFF + 1); + if (isPrimarySampled) return true; + const isExtraSampled = hashInt >= (1 - throttledSamplingRate) * (0xFFFF + 1); + return isExtraSampled; } export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER, analyticsType}), { cachedAuctions: {}, + exploratorySamplingSplit: 0.9, initConfig(config) { analyticsOptions.options = deepClone(config.options); @@ -68,6 +74,14 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER analyticsOptions.options.greenbidsSampling = 1; } + /** + * Add optional debug parameter to override exploratorySamplingSplit + */ + if (typeof analyticsOptions.options.exploratorySamplingSplit === 'number') { + logInfo('Greenbids Analytics: Overriding "exploratorySamplingSplit".'); + this.exploratorySamplingSplit = analyticsOptions.options.exploratorySamplingSplit; + } + analyticsOptions.pbuid = config.options.pbuid analyticsOptions.server = ANALYTICS_SERVER; @@ -172,7 +186,7 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER logInfo("Couldn't find Greenbids RTD info, assuming analytics only"); cachedAuction.greenbidsId = generateUUID(); } - cachedAuction.isSampled = isSampled(cachedAuction.greenbidsId, analyticsOptions.options.greenbidsSampling); + cachedAuction.isSampled = isSampled(cachedAuction.greenbidsId, analyticsOptions.options.greenbidsSampling, this.exploratorySamplingSplit); }, handleAuctionEnd(auctionEndArgs) { const cachedAuction = this.getCachedAuction(auctionEndArgs.auctionId); diff --git a/test/spec/modules/greenbidsAnalyticsAdapter_spec.js b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js index 30361ca5661..7b68b0dea46 100644 --- a/test/spec/modules/greenbidsAnalyticsAdapter_spec.js +++ b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js @@ -404,16 +404,24 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { describe('isSampled', function() { it('should return true for invalid sampling rates', function() { - expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', -1)).to.be.true; - expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 1.2)).to.be.true; + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', -1, 0.0)).to.be.true; + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 1.2, 0.0)).to.be.true; }); it('should return determinist falsevalue for valid sampling rate given the predifined id and rate', function() { - expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.0001)).to.be.false; + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.0001, 0.0)).to.be.false; }); it('should return determinist true value for valid sampling rate given the predifined id and rate', function() { - expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.9999)).to.be.true; + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.9999, 0.0)).to.be.true; + }); + + it('should return determinist true value for valid sampling rate given the predifined id and rate when we split to non exploration first', function() { + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.9999, 0.0, 1.0)).to.be.true; + }); + + it('should return determinist false value for valid sampling rate given the predifined id and rate when we split to non exploration first', function() { + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.0001, 0.0, 1.0)).to.be.false; }); }); }); From 67210fa06895b4cac33932b0d343bb8dec2329cf Mon Sep 17 00:00:00 2001 From: ahmadlob <109217988+ahmadlob@users.noreply.github.com> Date: Mon, 19 Feb 2024 20:55:47 +0200 Subject: [PATCH 21/21] Taboola Bid Adapter: fix cookie look up logic and gpp extracting (#11109) * cookie-look-up-logic-fix-gpp-fix * pass-version --- modules/taboolaBidAdapter.js | 16 +++- test/spec/modules/taboolaBidAdapter_spec.js | 101 +++++++++++++++++++- 2 files changed, 112 insertions(+), 5 deletions(-) diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index 3b83514b6fd..f0f11ea113e 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -18,6 +18,7 @@ const USER_ID = 'user-id'; const STORAGE_KEY = `taboola global:${USER_ID}`; const COOKIE_KEY = 'trc_cookie_storage'; const TGID_COOKIE_KEY = 't_gid'; +const TGID_PT_COOKIE_KEY = 't_pt_gid'; const TBLA_ID_COOKIE_KEY = 'tbla_id'; export const EVENT_ENDPOINT = 'https://beacon.bidder.taboola.com'; @@ -43,7 +44,10 @@ export const userData = { const {cookiesAreEnabled, getCookie} = userData.storageManager; if (cookiesAreEnabled()) { const cookieData = getCookie(COOKIE_KEY); - let userId = userData.getCookieDataByKey(cookieData, USER_ID); + let userId; + if (cookieData) { + userId = userData.getCookieDataByKey(cookieData, USER_ID); + } if (userId) { return userId; } @@ -51,6 +55,10 @@ export const userData = { if (userId) { return userId; } + userId = getCookie(TGID_PT_COOKIE_KEY); + if (userId) { + return userId; + } const tblaId = getCookie(TBLA_ID_COOKIE_KEY); if (tblaId) { return tblaId; @@ -58,6 +66,9 @@ export const userData = { } }, getCookieDataByKey(cookieData, key) { + if (!cookieData) { + return undefined; + } const [, value = ''] = cookieData.split(`${key}=`) return value; }, @@ -166,7 +177,7 @@ export const spec = { } if (gppConsent) { - queryParams.push('gpp=' + encodeURIComponent(gppConsent)); + queryParams.push('gpp=' + encodeURIComponent(gppConsent.gppString || '') + '&gpp_sid=' + encodeURIComponent((gppConsent.applicableSections || []).join(','))); } if (syncOptions.iframeEnabled) { @@ -258,6 +269,7 @@ function fillTaboolaReqData(bidderRequest, bidRequest, data) { 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$'); } function fillTaboolaImpData(bid, imp) { diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index dd91c410d08..8a121865cf2 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -6,6 +6,10 @@ import {server} from '../../mocks/xhr' describe('Taboola Adapter', function () { let sandbox, hasLocalStorage, cookiesAreEnabled, getDataFromLocalStorage, localStorageIsEnabled, getCookie, commonBidRequest; + const COOKIE_KEY = 'trc_cookie_storage'; + const TGID_COOKIE_KEY = 't_gid'; + const TGID_PT_COOKIE_KEY = 't_pt_gid'; + const TBLA_ID_COOKIE_KEY = 'tbla_id'; beforeEach(() => { sandbox = sinon.sandbox.create(); @@ -214,7 +218,11 @@ describe('Taboola Adapter', function () { 'ext': {}, }, 'regs': {'coppa': 0, 'ext': {}}, - 'ext': {} + 'ext': { + 'prebid': { + 'version': '$prebid.version$' + } + } }; expect(res.url).to.equal(`${END_POINT_URL}?publisher=${commonBidRequest.params.publisherId}`); @@ -471,6 +479,90 @@ describe('Taboola Adapter', function () { 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); + localStorageIsEnabled.returns(false); + cookiesAreEnabled.returns(true); + getCookie.callsFake(function (cookieKey) { + if (cookieKey === COOKIE_KEY) { + return 'should:not:return:this'; + } + if (cookieKey === TGID_COOKIE_KEY) { + return 'user:12121212'; + } + return undefined; + }); + const bidderRequest = { + ...commonBidderRequest + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.user.buyeruid).to.equal('user:12121212'); + }); + + it('should get user id from cookie if local storage isn`t defined, only TGID_PT_COOKIE_KEY exists', function () { + getDataFromLocalStorage.returns(51525152); + hasLocalStorage.returns(false); + localStorageIsEnabled.returns(false); + cookiesAreEnabled.returns(true); + getCookie.callsFake(function (cookieKey) { + if (cookieKey === TGID_PT_COOKIE_KEY) { + return 'user:12121212'; + } + return undefined; + }); + const bidderRequest = { + ...commonBidderRequest + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.user.buyeruid).to.equal('user:12121212'); + }); + + it('should get user id from cookie if local storage isn`t defined, only TBLA_ID_COOKIE_KEY exists', function () { + getDataFromLocalStorage.returns(51525152); + hasLocalStorage.returns(false); + localStorageIsEnabled.returns(false); + cookiesAreEnabled.returns(true); + getCookie.callsFake(function (cookieKey) { + if (cookieKey === TBLA_ID_COOKIE_KEY) { + return 'user:tbla:12121212'; + } + return undefined; + }); + const bidderRequest = { + ...commonBidderRequest + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.user.buyeruid).to.equal('user:tbla:12121212'); + }); + + it('should get user id from cookie if local storage isn`t defined, all cookie keys exist', function () { + getDataFromLocalStorage.returns(51525152); + hasLocalStorage.returns(false); + localStorageIsEnabled.returns(false); + cookiesAreEnabled.returns(true); + getCookie.callsFake(function (cookieKey) { + if (cookieKey === COOKIE_KEY) { + return 'taboola%20global%3Auser-id=cookie:1'; + } + if (cookieKey === TGID_COOKIE_KEY) { + return 'cookie:2'; + } + if (cookieKey === TGID_PT_COOKIE_KEY) { + return 'cookie:3'; + } + if (cookieKey === TBLA_ID_COOKIE_KEY) { + return 'cookie:4'; + } + return undefined; + }); + const bidderRequest = { + ...commonBidderRequest + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.user.buyeruid).to.equal('cookie:1'); + }); + it('should get user id from tgid cookie if local storage isn`t defined', function () { getDataFromLocalStorage.returns(51525152); hasLocalStorage.returns(false); @@ -892,8 +984,11 @@ describe('Taboola Adapter', function () { expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, 'USP_CONSENT')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?us_privacy=USP_CONSENT` }]); - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, 'USP_CONSENT', 'GPP_STRING')).to.deep.equal([{ - type: 'image', url: `${usersyncUrl}?us_privacy=USP_CONSENT&gpp=GPP_STRING` + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, 'USP_CONSENT', {gppString: 'GPP_STRING', applicableSections: []})).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?us_privacy=USP_CONSENT&gpp=GPP_STRING&gpp_sid=` + }]); + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, 'USP_CONSENT', {gppString: 'GPP_STRING', applicableSections: [32, 51]})).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?us_privacy=USP_CONSENT&gpp=GPP_STRING&gpp_sid=32%2C51` }]); }); })