From f74a4b34349d824653e4f28c40fba5f01a01e4af Mon Sep 17 00:00:00 2001 From: Shashank Pradeep <101392500+shashankatd@users.noreply.github.com> Date: Mon, 31 Jul 2023 16:31:49 +0530 Subject: [PATCH 01/88] Automatad Analytics Adapter: Initial Release (#10181) * Initial commit for addition of automatad's analytics adapter * Updated import path * Added listener for bidRequested Event * Added listener for bidRejected Event * Removed whitespace --------- Co-authored-by: Shashank <=> --- modules/automatadAnalyticsAdapter.js | 325 +++++++++++ modules/automatadAnalyticsAdapter.md | 23 + .../modules/automatadAnalyticsAdapter_spec.js | 533 ++++++++++++++++++ 3 files changed, 881 insertions(+) create mode 100644 modules/automatadAnalyticsAdapter.js create mode 100644 modules/automatadAnalyticsAdapter.md create mode 100644 test/spec/modules/automatadAnalyticsAdapter_spec.js diff --git a/modules/automatadAnalyticsAdapter.js b/modules/automatadAnalyticsAdapter.js new file mode 100644 index 00000000000..7d7bd8cb34c --- /dev/null +++ b/modules/automatadAnalyticsAdapter.js @@ -0,0 +1,325 @@ +import { + logError, + logInfo, + logMessage +} from '../src/utils.js'; + +import CONSTANTS from '../src/constants.json'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import { config } from '../src/config.js' + +/** Prebid Event Handlers */ + +const ADAPTER_CODE = 'automatadAnalytics' +const trialCountMilsMapping = [1500, 3000, 5000, 10000]; + +var isLoggingEnabled; var queuePointer = 0; var retryCount = 0; var timer = null; var __atmtdAnalyticsQueue = []; + +const prettyLog = (level, text, isGroup = false, cb = () => {}) => { + if (self.isLoggingEnabled === undefined) { + if (window.localStorage.getItem('__aggLoggingEnabled')) { + self.isLoggingEnabled = true + } else { + const queryParams = new URLSearchParams(new URL(window.location.href).search) + self.isLoggingEnabled = queryParams.has('aggLoggingEnabled') + } + } + + if (self.isLoggingEnabled) { + if (isGroup) { + logInfo(`ATD Analytics Adapter: ${level.toUpperCase()}: ${text} --- Group Start ---`) + try { + cb(); + } catch (error) { + logError(`ATD Analytics Adapter: ERROR: ${'Error during cb function in prettyLog'}`) + } + logInfo(`ATD Analytics Adapter: ${level.toUpperCase()}: ${text} --- Group End ---`) + } else { + logInfo(`ATD Analytics Adapter: ${level.toUpperCase()}: ${text}`) + } + } +} + +const processEvents = () => { + if (self.retryCount === trialCountMilsMapping.length) { + self.prettyLog('error', `Aggregator still hasn't loaded. Processing que stopped`, trialCountMilsMapping, self.retryCount) + return; + } + + self.prettyLog('status', `Que has been inactive for a while. Adapter starting to process que now... Trial Count = ${self.retryCount + 1}`) + + let shouldTryAgain = false + + while (self.queuePointer < self.__atmtdAnalyticsQueue.length) { + const eventType = self.__atmtdAnalyticsQueue[self.queuePointer][0] + const args = self.__atmtdAnalyticsQueue[self.queuePointer][1] + + try { + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionInitHandler) { + window.atmtdAnalytics.auctionInitHandler(args); + } else { + shouldTryAgain = true + } + break; + case CONSTANTS.EVENTS.BID_REQUESTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRequestedHandler) { + window.atmtdAnalytics.bidRequestedHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_RESPONSE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidResponseHandler) { + window.atmtdAnalytics.bidResponseHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_REJECTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRejectedHandler) { + window.atmtdAnalytics.bidRejectedHandler(args); + } + break; + case CONSTANTS.EVENTS.BIDDER_DONE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderDoneHandler) { + window.atmtdAnalytics.bidderDoneHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_WON: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidWonHandler) { + window.atmtdAnalytics.bidWonHandler(args); + } + break; + case CONSTANTS.EVENTS.NO_BID: + if (window.atmtdAnalytics && window.atmtdAnalytics.noBidHandler) { + window.atmtdAnalytics.noBidHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_TIMEOUT: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderTimeoutHandler) { + window.atmtdAnalytics.bidderTimeoutHandler(args); + } + break; + case CONSTANTS.EVENTS.AUCTION_DEBUG: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionDebugHandler) { + window.atmtdAnalytics.auctionDebugHandler(args); + } + break; + case 'slotRenderEnded': + if (window.atmtdAnalytics && window.atmtdAnalytics.slotRenderEndedGPTHandler) { + window.atmtdAnalytics.slotRenderEndedGPTHandler(args); + } else { + shouldTryAgain = true + } + break; + case 'impressionViewable': + if (window.atmtdAnalytics && window.atmtdAnalytics.impressionViewableHandler) { + window.atmtdAnalytics.impressionViewableHandler(args); + } else { + shouldTryAgain = true + } + break; + } + + if (shouldTryAgain) break; + } catch (error) { + self.prettyLog('error', `Unhandled Error while processing ${eventType} of ${self.queuePointer}th index in the que. Will not be retrying this raw event ...`, true, () => { + logError(`The error is `, error) + }) + } + + self.queuePointer = self.queuePointer + 1 + } + + if (shouldTryAgain) { + if (trialCountMilsMapping[self.retryCount]) self.prettyLog('warn', `Adapter failed to process event as aggregator has not loaded. Retrying in ${trialCountMilsMapping[self.retryCount]}ms ...`); + setTimeout(self.processEvents, trialCountMilsMapping[self.retryCount]) + self.retryCount = self.retryCount + 1 + } +} + +const addGPTHandlers = () => { + const googletag = window.googletag || {} + googletag.cmd = googletag.cmd || [] + googletag.cmd.push(() => { + googletag.pubads().addEventListener('slotRenderEnded', (event) => { + if (window.atmtdAnalytics && window.atmtdAnalytics.slotRenderEndedGPTHandler) { + if (window.__atmtdAggregatorFirstAuctionInitialized === true) { + window.atmtdAnalytics.slotRenderEndedGPTHandler(event) + return; + } + } + self.__atmtdAnalyticsQueue.push(['slotRenderEnded', event]) + self.prettyLog(`warn`, `Aggregator not initialised at auctionInit, exiting slotRenderEnded handler and pushing to que instead`) + }) + + googletag.pubads().addEventListener('impressionViewable', (event) => { + if (window.atmtdAnalytics && window.atmtdAnalytics.impressionViewableHandler) { + if (window.__atmtdAggregatorFirstAuctionInitialized === true) { + window.atmtdAnalytics.impressionViewableHandler(event) + return; + } + } + self.__atmtdAnalyticsQueue.push(['impressionViewable', event]) + self.prettyLog(`warn`, `Aggregator not initialised at auctionInit, exiting impressionViewable handler and pushing to que instead`) + }) + }) +} + +const initializeQueue = () => { + self.__atmtdAnalyticsQueue.push = (args) => { + Array.prototype.push.apply(self.__atmtdAnalyticsQueue, [args]); + if (timer) { + clearTimeout(timer); + timer = null; + } + + if (args[0] === CONSTANTS.EVENTS.AUCTION_INIT) { + const timeout = parseInt(config.getConfig('bidderTimeout')) + 1500 + timer = setTimeout(() => { + self.processEvents() + }, timeout); + } else { + timer = setTimeout(() => { + self.processEvents() + }, 1500); + } + }; +} + +// ANALYTICS ADAPTER + +let baseAdapter = adapter({analyticsType: 'bundle'}); +let atmtdAdapter = Object.assign({}, baseAdapter, { + + disableAnalytics() { + baseAdapter.disableAnalytics.apply(this, arguments); + }, + + track({eventType, args}) { + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionInitHandler) { + self.prettyLog('status', 'Aggregator loaded, initialising auction through handlers'); + window.atmtdAnalytics.auctionInitHandler(args); + } else { + self.prettyLog('warn', 'Aggregator not loaded, initialising auction through que ...'); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_REQUESTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRequestedHandler) { + window.atmtdAnalytics.bidRequestedHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_REJECTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRejectedHandler) { + window.atmtdAnalytics.bidRejectedHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_RESPONSE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidResponseHandler) { + window.atmtdAnalytics.bidResponseHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BIDDER_DONE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderDoneHandler) { + window.atmtdAnalytics.bidderDoneHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_WON: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidWonHandler) { + window.atmtdAnalytics.bidWonHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.NO_BID: + if (window.atmtdAnalytics && window.atmtdAnalytics.noBidHandler) { + window.atmtdAnalytics.noBidHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.AUCTION_DEBUG: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionDebugHandler) { + window.atmtdAnalytics.auctionDebugHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_TIMEOUT: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderTimeoutHandler) { + window.atmtdAnalytics.bidderTimeoutHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + } + } +}); + +atmtdAdapter.originEnableAnalytics = atmtdAdapter.enableAnalytics + +atmtdAdapter.enableAnalytics = function (configuration) { + if ((configuration === undefined && typeof configuration !== 'object') || configuration.options === undefined) { + logError('A valid configuration must be passed to the Atmtd Analytics Adapter.'); + return; + } + + const conf = configuration.options + + if (conf === undefined || typeof conf !== 'object' || conf.siteID === undefined || conf.publisherID === undefined) { + logError('A valid publisher ID and siteID must be passed to the Atmtd Analytics Adapter.'); + return; + } + + self.initializeQueue() + self.addGPTHandlers() + + window.__atmtdSDKConfig = { + publisherID: conf.publisherID, + siteID: conf.siteID, + collectDebugMessages: conf.logDebug ? conf.logDebug : false + } + + logMessage(`Automatad Analytics Adapter enabled with sdk config`, window.__atmtdSDKConfig) + + // eslint-disable-next-line + atmtdAdapter.originEnableAnalytics(configuration) +}; + +/// /////////// ADAPTER REGISTRATION ////////////// + +adapterManager.registerAnalyticsAdapter({ + adapter: atmtdAdapter, + code: ADAPTER_CODE +}); + +export var self = { + __atmtdAnalyticsQueue, + processEvents, + initializeQueue, + addGPTHandlers, + prettyLog, + queuePointer, + retryCount, + isLoggingEnabled +} + +export default atmtdAdapter; diff --git a/modules/automatadAnalyticsAdapter.md b/modules/automatadAnalyticsAdapter.md new file mode 100644 index 00000000000..2be1af87f20 --- /dev/null +++ b/modules/automatadAnalyticsAdapter.md @@ -0,0 +1,23 @@ + +# Overview + +Module Name: Automatad Analytics Adapter +Module Type: Analytics Adapter +Maintainer: tech@automatad.com + +# Description + +Analytics adapter for automatad.com. Contact tech@automatad.com for information. + +# Test Parameters + +``` +{ + provider: 'automatadAnalytics', + options: { + publisherID: 'N8vZLx', + siteID: 'PXfvBq' + } +} + +``` \ No newline at end of file diff --git a/test/spec/modules/automatadAnalyticsAdapter_spec.js b/test/spec/modules/automatadAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..e591f7e8e95 --- /dev/null +++ b/test/spec/modules/automatadAnalyticsAdapter_spec.js @@ -0,0 +1,533 @@ +import * as events from 'src/events'; +import * as utils from 'src/utils.js'; + +import spec, {self as exports} from 'modules/automatadAnalyticsAdapter.js'; + +import CONSTANTS from 'src/constants.json'; +import { expect } from 'chai'; + +const { + AUCTION_DEBUG, + BID_REQUESTED, + BID_REJECTED, + AUCTION_INIT, + BIDDER_DONE, + BID_RESPONSE, + BID_TIMEOUT, + BID_WON, + NO_BID +} = CONSTANTS.EVENTS + +const CONFIG_WITH_DEBUG = { + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421' + }, + includeEvents: [AUCTION_DEBUG, AUCTION_INIT, BIDDER_DONE, BID_RESPONSE, BID_TIMEOUT, NO_BID, BID_WON, BID_REQUESTED, BID_REJECTED] +} + +describe('Automatad Analytics Adapter', () => { + var sandbox, clock; + + describe('Adapter Setup Configuration', () => { + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(utils, 'logMessage') + sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(utils, 'logError'); + }); + afterEach(() => { + sandbox.restore(); + }); + + it('Should log error and return false if nothing is passed as the param in the enable analytics call', () => { + spec.enableAnalytics() + + expect(utils.logError.called).to.equal(true) + }); + + it('Should log error and return false if object type is not passed as the param in the enable analytics call', () => { + spec.enableAnalytics('hello world') + + expect(utils.logError.called).to.equal(true) + }); + + it('Should log error and return false if options is not defined in the enable analytics call', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter' + }) + + expect(utils.logError.called).to.equal(true) + }); + it('Should log error and return false if pub id is not defined in the enable analytics call', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + siteID: '230' + } + }) + + expect(utils.logError.called).to.equal(true) + }); + it('Should log error and return false if pub id is not defined in the enable analytics call', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230' + } + }) + + expect(utils.logError.called).to.equal(true) + }); + it('Should successfully configure the adapter and set global log debug messages flag to false', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421', + logDebug: false + } + }); + expect(utils.logError.called).to.equal(false) + expect(utils.logMessage.called).to.equal(true) + spec.disableAnalytics(); + }); + it('Should successfully configure the adapter and set global log debug messages flag to true', () => { + sandbox.stub(exports, 'initializeQueue').callsFake(() => {}); + sandbox.stub(exports, 'addGPTHandlers').callsFake(() => {}); + const config = { + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '410', + logDebug: true + } + } + + spec.enableAnalytics(config) + expect(utils.logError.called).to.equal(false) + expect(exports.initializeQueue.called).to.equal(true) + expect(exports.addGPTHandlers.called).to.equal(true) + expect(utils.logMessage.called).to.equal(true) + spec.disableAnalytics(); + }); + }); + + describe('Behaviour of the adapter when the sdk has loaded', () => { + before(() => { + spec.enableAnalytics(CONFIG_WITH_DEBUG); + const obj = { + auctionInitHandler: (args) => {}, + bidResponseHandler: (args) => {}, + bidderDoneHandler: (args) => {}, + bidWonHandler: (args) => {}, + noBidHandler: (args) => {}, + auctionDebugHandler: (args) => {}, + bidderTimeoutHandler: (args) => {}, + bidRequestedHandler: (args) => {}, + bidRejectedHandler: (args) => {} + } + + global.window.atmtdAnalytics = obj + + Object.keys(obj).forEach((fn) => sandbox.spy(global.window.atmtdAnalytics, fn)) + }) + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(utils, 'logMessage'); + sandbox.stub(utils, 'logError'); + }); + afterEach(() => { + sandbox.restore(); + }); + after(() => { + global.window.atmtdAnalytics = undefined; + spec.disableAnalytics(); + }) + + it('Should call the auctionInitHandler when the auction init event is fired', () => { + events.emit(AUCTION_INIT, {type: AUCTION_INIT}) + expect(global.window.atmtdAnalytics.auctionInitHandler.called).to.equal(true) + }); + + it('Should call the bidRequested when the bidRequested event is fired', () => { + events.emit(BID_REQUESTED, {type: BID_REQUESTED}) + expect(global.window.atmtdAnalytics.bidRequestedHandler.called).to.equal(true) + }); + + it('Should call the bidRejected when the bidRejected event is fired', () => { + events.emit(BID_REJECTED, {type: BID_REJECTED}) + expect(global.window.atmtdAnalytics.bidRejectedHandler.called).to.equal(true) + }); + + it('Should call the bidResponseHandler when the bidResponse event is fired', () => { + events.emit(BID_RESPONSE, {type: BID_RESPONSE}) + expect(global.window.atmtdAnalytics.bidResponseHandler.called).to.equal(true) + }); + + it('Should call the bidderDoneHandler when the bidderDone event is fired', () => { + events.emit(BIDDER_DONE, {type: BIDDER_DONE}) + expect(global.window.atmtdAnalytics.bidderDoneHandler.called).to.equal(true) + }); + + it('Should call the bidWonHandler when the bidWon event is fired', () => { + events.emit(BID_WON, {type: BID_WON}) + expect(global.window.atmtdAnalytics.bidWonHandler.called).to.equal(true) + }); + + it('Should call the noBidHandler when the noBid event is fired', () => { + events.emit(NO_BID, {type: NO_BID}) + expect(global.window.atmtdAnalytics.noBidHandler.called).to.equal(true) + }); + + it('Should call the bidTimeoutHandler when the bidTimeout event is fired', () => { + events.emit(BID_TIMEOUT, {type: BID_TIMEOUT}) + expect(global.window.atmtdAnalytics.bidderTimeoutHandler.called).to.equal(true) + }); + + it('Should call the auctionDebugHandler when the auctionDebug event is fired', () => { + events.emit(AUCTION_DEBUG, {type: AUCTION_DEBUG}) + expect(global.window.atmtdAnalytics.auctionDebugHandler.called).to.equal(true) + }); + }); + + describe('Behaviour of the adapter when the SDK has not loaded', () => { + before(() => { + spec.enableAnalytics(CONFIG_WITH_DEBUG); + }) + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(utils, 'logMessage'); + sandbox.stub(utils, 'logError'); + + global.window.atmtdAnalytics = undefined + exports.__atmtdAnalyticsQueue.length = 0 + sandbox.stub(exports.__atmtdAnalyticsQueue, 'push').callsFake((args) => { + Array.prototype.push.apply(exports.__atmtdAnalyticsQueue, [args]); + }) + }); + afterEach(() => { + sandbox.restore(); + }); + after(() => { + spec.disableAnalytics(); + }) + + it('Should push to the que when the auctionInit event is fired', () => { + events.emit(AUCTION_INIT, {type: AUCTION_INIT}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(AUCTION_INIT) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(AUCTION_INIT) + }); + + it('Should push to the que when the bidResponse event is fired', () => { + events.emit(BID_RESPONSE, {type: BID_RESPONSE}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_RESPONSE) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_RESPONSE) + }); + + it('Should push to the que when the bidRequested event is fired', () => { + events.emit(BID_REQUESTED, {type: BID_REQUESTED}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_REQUESTED) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_REQUESTED) + }); + + it('Should push to the que when the bidRejected event is fired', () => { + events.emit(BID_REJECTED, {type: BID_REJECTED}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_REJECTED) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_REJECTED) + }); + + it('Should push to the que when the bidderDone event is fired', () => { + events.emit(BIDDER_DONE, {type: BIDDER_DONE}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BIDDER_DONE) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BIDDER_DONE) + }); + + it('Should push to the que when the bidWon event is fired', () => { + events.emit(BID_WON, {type: BID_WON}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_WON) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_WON) + }); + + it('Should push to the que when the noBid event is fired', () => { + events.emit(NO_BID, {type: NO_BID}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(NO_BID) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(NO_BID) + }); + + it('Should push to the que when the auctionDebug is fired', () => { + events.emit(AUCTION_DEBUG, {type: AUCTION_DEBUG}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(AUCTION_DEBUG) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(AUCTION_DEBUG) + }); + + it('Should push to the que when the bidderTimeout event is fired', () => { + events.emit(BID_TIMEOUT, {type: BID_TIMEOUT}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_TIMEOUT) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_TIMEOUT) + }); + }); + + describe('Process Events from Que when SDK still has not loaded', () => { + before(() => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421' + } + }); + global.window.atmtdAnalytics = undefined + + sandbox.stub(exports.__atmtdAnalyticsQueue, 'push').callsFake((args) => { + Array.prototype.push.apply(exports.__atmtdAnalyticsQueue, [args]); + }) + }) + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(events, 'getEvents').returns([]); + sandbox.spy(exports, 'prettyLog') + sandbox.spy(exports, 'processEvents') + + clock = sandbox.useFakeTimers(); + exports.__atmtdAnalyticsQueue.length = 0 + }); + afterEach(() => { + sandbox.restore(); + exports.queuePointer = 0; + exports.retryCount = 0; + exports.__atmtdAnalyticsQueue = [] + spec.disableAnalytics(); + }) + + it('Should retry processing auctionInit in certain intervals', () => { + expect(exports.queuePointer).to.equal(0) + expect(exports.retryCount).to.equal(0) + const que = [[AUCTION_INIT, {type: AUCTION_INIT}]] + exports.__atmtdAnalyticsQueue.push(que[0]) + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.prettyLog.getCall(1).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(1).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 1500ms ...`) + clock.tick(1510) + expect(exports.prettyLog.getCall(2).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(2).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 2`) + expect(exports.prettyLog.getCall(3).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(3).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 3000ms ...`) + clock.tick(3010) + expect(exports.prettyLog.getCall(4).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(4).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 3`) + expect(exports.prettyLog.getCall(5).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(5).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 5000ms ...`) + clock.tick(5010) + expect(exports.prettyLog.getCall(6).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(6).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 4`) + expect(exports.prettyLog.getCall(7).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(7).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 10000ms ...`) + clock.tick(10010) + expect(exports.prettyLog.getCall(8).args[0]).to.equal('error') + expect(exports.prettyLog.getCall(8).args[1]).to.equal(`Aggregator still hasn't loaded. Processing que stopped`) + expect(exports.queuePointer).to.equal(0) + expect(exports.processEvents.callCount).to.equal(5) + }) + + it('Should retry processing slotRenderEnded in certain intervals', () => { + expect(exports.queuePointer).to.equal(0) + expect(exports.retryCount).to.equal(0) + const que = [['slotRenderEnded', {type: 'slotRenderEnded'}]] + exports.__atmtdAnalyticsQueue.push(que[0]) + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.prettyLog.getCall(1).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(1).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 1500ms ...`) + clock.tick(1510) + expect(exports.prettyLog.getCall(2).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(2).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 2`) + expect(exports.prettyLog.getCall(3).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(3).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 3000ms ...`) + clock.tick(3010) + expect(exports.prettyLog.getCall(4).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(4).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 3`) + expect(exports.prettyLog.getCall(5).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(5).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 5000ms ...`) + clock.tick(5010) + expect(exports.prettyLog.getCall(6).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(6).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 4`) + expect(exports.prettyLog.getCall(7).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(7).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 10000ms ...`) + clock.tick(10010) + expect(exports.prettyLog.getCall(8).args[0]).to.equal('error') + expect(exports.prettyLog.getCall(8).args[1]).to.equal(`Aggregator still hasn't loaded. Processing que stopped`) + expect(exports.queuePointer).to.equal(0) + expect(exports.processEvents.callCount).to.equal(5) + }) + + it('Should retry processing impressionViewable in certain intervals', () => { + expect(exports.queuePointer).to.equal(0) + expect(exports.retryCount).to.equal(0) + const que = [['impressionViewable', {type: 'impressionViewable'}]] + exports.__atmtdAnalyticsQueue.push(que[0]) + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.prettyLog.getCall(1).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(1).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 1500ms ...`) + clock.tick(1510) + expect(exports.prettyLog.getCall(2).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(2).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 2`) + expect(exports.prettyLog.getCall(3).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(3).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 3000ms ...`) + clock.tick(3010) + expect(exports.prettyLog.getCall(4).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(4).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 3`) + expect(exports.prettyLog.getCall(5).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(5).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 5000ms ...`) + clock.tick(5010) + expect(exports.prettyLog.getCall(6).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(6).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 4`) + expect(exports.prettyLog.getCall(7).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(7).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 10000ms ...`) + clock.tick(10010) + expect(exports.prettyLog.getCall(8).args[0]).to.equal('error') + expect(exports.prettyLog.getCall(8).args[1]).to.equal(`Aggregator still hasn't loaded. Processing que stopped`) + expect(exports.queuePointer).to.equal(0) + expect(exports.processEvents.callCount).to.equal(5) + }) + }); + + describe('Process Events from Que when SDK has loaded', () => { + before(() => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421' + } + }); + sandbox = sinon.createSandbox(); + sandbox.reset() + const obj = { + auctionInitHandler: (args) => {}, + bidResponseHandler: (args) => {}, + bidderDoneHandler: (args) => {}, + bidWonHandler: (args) => {}, + noBidHandler: (args) => {}, + auctionDebugHandler: (args) => {}, + bidderTimeoutHandler: (args) => {}, + impressionViewableHandler: (args) => {}, + slotRenderEndedGPTHandler: (args) => {}, + bidRequestedHandler: (args) => {}, + bidRejectedHandler: (args) => {} + } + + global.window.atmtdAnalytics = obj; + + Object.keys(obj).forEach((fn) => sandbox.spy(global.window.atmtdAnalytics, fn)) + sandbox.stub(events, 'getEvents').returns([]); + sandbox.spy(exports, 'prettyLog') + exports.retryCount = 0; + exports.queuePointer = 0; + exports.__atmtdAnalyticsQueue = [ + [AUCTION_INIT, {type: AUCTION_INIT}], + [BID_RESPONSE, {type: BID_RESPONSE}], + [BID_REQUESTED, {type: BID_REQUESTED}], + [BID_REJECTED, {type: BID_REJECTED}], + [NO_BID, {type: NO_BID}], + [BID_WON, {type: BID_WON}], + [BIDDER_DONE, {type: BIDDER_DONE}], + [AUCTION_DEBUG, {type: AUCTION_DEBUG}], + [BID_TIMEOUT, {type: BID_TIMEOUT}], + ['slotRenderEnded', {type: 'slotRenderEnded'}], + ['impressionViewable', {type: 'impressionViewable'}] + ] + }); + after(() => { + sandbox.restore(); + spec.disableAnalytics(); + }) + + it('Should make calls to appropriate SDK event handlers', () => { + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.retryCount).to.equal(0) + expect(exports.prettyLog.callCount).to.equal(1) + expect(exports.queuePointer).to.equal(exports.__atmtdAnalyticsQueue.length) + expect(global.window.atmtdAnalytics.auctionInitHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidResponseHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidRejectedHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidRequestedHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.noBidHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidWonHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.auctionDebugHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidderTimeoutHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidderDoneHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.slotRenderEndedGPTHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.impressionViewableHandler.calledOnce).to.equal(true) + }) + }); + + describe('Prettylog fn tests', () => { + beforeEach(() => { + sandbox = sinon.createSandbox() + sandbox.spy(utils, 'logInfo') + sandbox.spy(utils, 'logError') + exports.isLoggingEnabled = true + }) + + afterEach(() => { + sandbox.restore() + }) + + it('Should call logMessage once in normal mode', () => { + exports.prettyLog('status', 'Hello world') + expect(utils.logInfo.callCount).to.equal(1) + }) + + it('Should call logMessage twice in group mode and have the cb called', () => { + const spy = sandbox.spy() + exports.prettyLog('status', 'Hello world', true, spy) + expect(utils.logInfo.callCount).to.equal(2) + expect(spy.called).to.equal(true) + }) + + it('Should call logMessage twice in group mode and have the cb which throws an error', () => { + const spy = sandbox.stub().throws() + exports.prettyLog('status', 'Hello world', true, spy) + expect(utils.logInfo.callCount).to.equal(2) + expect(utils.logError.called).to.equal(true) + }) + }); +}); From 2b422f908305d85d62267d985effcc4027a447eb Mon Sep 17 00:00:00 2001 From: Rares Mihai Preda <54801398+rares-mihai-preda@users.noreply.github.com> Date: Wed, 2 Aug 2023 14:09:42 +0300 Subject: [PATCH 02/88] Connatix bid adapter (#10186) --- modules/connatixBidAdapter.js | 185 ++++++++++ modules/connatixBidAdapter.md | 37 ++ test/spec/modules/connatixBidAdapter_spec.js | 366 +++++++++++++++++++ 3 files changed, 588 insertions(+) create mode 100644 modules/connatixBidAdapter.js create mode 100644 modules/connatixBidAdapter.md create mode 100644 test/spec/modules/connatixBidAdapter_spec.js diff --git a/modules/connatixBidAdapter.js b/modules/connatixBidAdapter.js new file mode 100644 index 00000000000..df56ad580bc --- /dev/null +++ b/modules/connatixBidAdapter.js @@ -0,0 +1,185 @@ +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; + +import { + deepAccess, + isFn, + logError, + isArray, + formatQS +} from '../src/utils.js'; + +import { + BANNER, +} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'connatix'; +const AD_URL = 'https://capi.connatix.com/rtb/hba'; +const DEFAULT_MAX_TTL = '3600'; +const DEFAULT_CURRENCY = 'USD'; + +/* + * Get the bid floor value from the bid object, either using the getFloor function or by accessing the 'params.bidfloor' property. + * If the bid floor cannot be determined, return 0 as a fallback value. + */ +export function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + gvlid: 143, + supportedMediaTypes: [BANNER], + + /* + * Validate the bid request. + * If the request is valid, Connatix is trying to obtain at least one bid. + * Otherwise, the request to the Connatix server is not made + */ + isBidRequestValid: (bid = {}) => { + const bidId = deepAccess(bid, 'bidId'); + const mediaTypes = deepAccess(bid, 'mediaTypes', {}); + const params = deepAccess(bid, 'params', {}); + const bidder = deepAccess(bid, 'bidder'); + + const banner = deepAccess(mediaTypes, BANNER, {}); + + const hasBidId = Boolean(bidId); + const isValidBidder = (bidder === BIDDER_CODE); + const isValidSize = (Boolean(banner.sizes) && isArray(mediaTypes[BANNER].sizes) && mediaTypes[BANNER].sizes.length > 0); + const hasSizes = mediaTypes[BANNER] ? isValidSize : false; + const hasRequiredBidParams = Boolean(params.placementId); + + const isValid = isValidBidder && hasBidId && hasSizes && hasRequiredBidParams; + if (!isValid) { + logError(`Invalid bid request: isValidBidder: ${isValidBidder} hasBidId: ${hasBidId}, hasSizes: ${hasSizes}, hasRequiredBidParams: ${hasRequiredBidParams}`); + } + return isValid; + }, + + /* + * Build the request payload by processing valid bid requests and extracting the necessary information. + * Determine the host and page from the bidderRequest's refferUrl, and include ccpa and gdpr consents. + * Return an object containing the request method, url, and the constructed payload. + */ + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + const bidRequests = validBidRequests.map(bid => { + const { + bidId, + mediaTypes, + params, + sizes, + } = bid; + return { + bidId, + mediaTypes, + sizes, + placementId: params.placementId, + floor: getBidFloor(bid), + }; + }); + + const requestPayload = { + ortb2: bidderRequest.ortb2, + gdprConsent: bidderRequest.gdprConsent, + uspConsent: bidderRequest.uspConsent, + refererInfo: bidderRequest.refererInfo, + bidRequests, + }; + + return { + method: 'POST', + url: AD_URL, + data: requestPayload + }; + }, + + /* + * Interpret the server response and create an array of bid responses by extracting and formatting + * relevant information such as requestId, cpm, ttl, width, height, creativeId, referrer and ad + * Returns an array of bid responses by extracting and formatting the server response + */ + interpretResponse: (serverResponse) => { + const responseBody = serverResponse.body; + const bids = responseBody.Bids; + const playerId = responseBody.PlayerId; + const customerId = responseBody.CustomerId; + + if (!isArray(bids) || !playerId || !customerId) { + return []; + } + + return bids.map(bidResponse => ({ + requestId: bidResponse.RequestId, + cpm: bidResponse.Cpm, + ttl: bidResponse.Ttl || DEFAULT_MAX_TTL, + currency: 'USD', + mediaType: BANNER, + netRevenue: true, + width: bidResponse.Width, + height: bidResponse.Height, + creativeId: bidResponse.CreativeId, + referrer: bidResponse.Referrer, + ad: bidResponse.Ad, + })); + }, + + /* + * Determine the user sync type (either 'iframe' or 'image') based on syncOptions. + * Construct the sync URL by appending required query parameters such as gdpr, ccpa, and coppa consents. + * Return an array containing an object with the sync type and the constructed URL. + */ + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { + if (!syncOptions.iframeEnabled) { + return []; + } + + if (!serverResponses || !serverResponses.length) { + return []; + } + + const params = {}; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params['gdpr'] = Number(gdprConsent.gdprApplies); + } else { + params['gdpr'] = 0; + } + + if (typeof gdprConsent.consentString === 'string') { + params['gdpr_consent'] = encodeURIComponent(gdprConsent.consentString); + } + } + + if (typeof uspConsent === 'string') { + params['us_privacy'] = encodeURIComponent(uspConsent); + } + + const syncUrl = serverResponses[0].body.UserSyncEndpoint; + const queryParams = Object.keys(params).length > 0 ? formatQS(params) : ''; + + const url = queryParams ? `${syncUrl}?${queryParams}` : syncUrl; + return [{ + type: 'iframe', + url + }]; + } +}; + +registerBidder(spec); diff --git a/modules/connatixBidAdapter.md b/modules/connatixBidAdapter.md new file mode 100644 index 00000000000..7ac04a64245 --- /dev/null +++ b/modules/connatixBidAdapter.md @@ -0,0 +1,37 @@ + +# Overview + +``` +Module Name: Connatix Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid_integration@connatix.com +``` + +# Description +Connects to Connatix demand source to fetch bids. +Please use ```connatix``` as the bidder code. + +# Test Parameters +``` +var adUnits = [ + { + code: '1', + mediaTypes: { + banner: { + sizes: [[640, 480], [320, 180]], + }, + }, + bids: [ + { + bidder: 'connatix', + params: { + placementId: 'e4984e88-9ff4-45a3-8b9d-33aabcad634e', // required + bidfloor: 2.5, // optional + }, + }, + // Add more bidders and their parameters as needed + ], + }, + // Define more ad units here if necessary +]; +``` \ No newline at end of file diff --git a/test/spec/modules/connatixBidAdapter_spec.js b/test/spec/modules/connatixBidAdapter_spec.js new file mode 100644 index 00000000000..16ead9f9458 --- /dev/null +++ b/test/spec/modules/connatixBidAdapter_spec.js @@ -0,0 +1,366 @@ +import { expect } from 'chai'; +import { + spec, + getBidFloor as connatixGetBidFloor +} from '../../../modules/connatixBidAdapter.js'; +import { BANNER } from '../../../src/mediaTypes.js'; + +describe('connatixBidAdapter', function () { + let bid; + + function mockBidRequest() { + const mediaTypes = { + banner: { + sizes: [16, 9], + } + }; + return { + bidId: 'testing', + bidder: 'connatix', + params: { + placementId: '30e91414-545c-4f45-a950-0bec9308ff22' + }, + mediaTypes + }; + }; + + describe('isBidRequestValid', function () { + this.beforeEach(function () { + bid = mockBidRequest(); + }); + + it('Should return true if all required fileds are present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if bidder does not correspond', function () { + bid.bidder = 'abc'; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if bidId is missing', function () { + delete bid.bidId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if params object is missing', function () { + delete bid.params; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if placementId is missing from params', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if mediaTypes is missing', function () { + delete bid.mediaTypes; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if banner is missing from mediaTypes ', function () { + delete bid.mediaTypes.banner; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if sizes is missing from banner object', function () { + delete bid.mediaTypes.banner.sizes; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if sizes is not an array', function () { + bid.mediaTypes.banner.sizes = 'test'; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if sizes is an empty array', function () { + bid.mediaTypes.banner.sizes = []; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return true if add an extra field was added to the bidRequest', function () { + bid.params.test = 1; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + }); + + describe('buildRequests', function () { + let serverRequest; + let bidderRequest = { + refererInfo: { + canonicalUrl: '', + numIframes: 0, + reachedTop: true, + referer: 'http://example.com', + stack: ['http://example.com'] + }, + gdprConsent: { + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + vendorData: {}, + gdprApplies: true + }, + uspConsent: '1YYY', + ortb2: { + site: { + data: { + pageType: 'article' + } + } + } + }; + + this.beforeEach(function () { + bid = mockBidRequest(); + serverRequest = spec.buildRequests([bid], bidderRequest); + }) + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://capi.connatix.com/rtb/hba'); + }); + it('Returns request payload', function () { + expect(serverRequest.data).to.not.empty; + }); + it('Validate request payload', function () { + expect(serverRequest.data.bidRequests[0].bidId).to.equal(bid.bidId); + expect(serverRequest.data.bidRequests[0].placementId).to.equal(bid.params.placementId); + expect(serverRequest.data.bidRequests[0].floor).to.equal(0); + expect(serverRequest.data.bidRequests[0].mediaTypes).to.equal(bid.mediaTypes); + expect(serverRequest.data.bidRequests[0].sizes).to.equal(bid.mediaTypes.sizes); + expect(serverRequest.data.refererInfo).to.equal(bidderRequest.refererInfo); + expect(serverRequest.data.gdprConsent).to.equal(bidderRequest.gdprConsent); + expect(serverRequest.data.uspConsent).to.equal(bidderRequest.uspConsent); + expect(serverRequest.data.ortb2).to.equal(bidderRequest.ortb2); + }); + }); + + describe('interpretResponse', function () { + const CustomerId = '99f20d18-c4b4-4a28-3d8e-d43e2c8cb4ac'; + const PlayerId = 'e4984e88-9ff4-45a3-8b9d-33aabcad634f'; + const Bid = {Cpm: 0.1, LineItems: [], RequestId: '2f897340c4eaa3', Ttl: 86400}; + + let serverResponse; + this.beforeEach(function () { + serverResponse = { + body: { + CustomerId, + PlayerId, + Bids: [ Bid ] + }, + headers: function() { } + }; + }); + + it('Should return an empty array if Bids is null', function () { + serverResponse.body.Bids = null; + + const response = spec.interpretResponse(serverResponse); + expect(response).to.be.an('array').that.is.empty; + }); + + it('Should return an empty array if Bids is empty array', function () { + serverResponse.body.Bids = []; + const response = spec.interpretResponse(serverResponse); + expect(response).to.be.an('array').that.is.empty; + }); + + it('Should return an empty array if CustomerId is null', function () { + serverResponse.body.CustomerId = null; + const response = spec.interpretResponse(serverResponse); + expect(response).to.be.an('array').that.is.empty; + }); + + it('Should return an empty array if PlayerId is null', function () { + serverResponse.body.PlayerId = null; + const response = spec.interpretResponse(serverResponse); + expect(response).to.be.an('array').that.is.empty; + }); + + it('Should return one bid response for one bid', function() { + const bidResponses = spec.interpretResponse(serverResponse); + expect(bidResponses.length).to.equal(1); + }); + + it('Should contains the same values as in the serverResponse', function() { + const bidResponses = spec.interpretResponse(serverResponse); + + const [ bidResponse ] = bidResponses; + expect(bidResponse.requestId).to.equal(serverResponse.body.Bids[0].RequestId); + expect(bidResponse.cpm).to.equal(serverResponse.body.Bids[0].Cpm); + expect(bidResponse.ttl).to.equal(serverResponse.body.Bids[0].Ttl); + expect(bidResponse.currency).to.equal('USD'); + expect(bidResponse.mediaType).to.equal(BANNER); + expect(bidResponse.netRevenue).to.be.true; + }); + + it('Should return n bid responses for n bids', function() { + serverResponse.body.Bids = [ { ...Bid }, { ...Bid } ]; + + const firstBidCpm = 4; + serverResponse.body.Bids[0].Cpm = firstBidCpm; + + const secondBidCpm = 13; + serverResponse.body.Bids[1].Cpm = secondBidCpm; + + const bidResponses = spec.interpretResponse(serverResponse); + expect(bidResponses.length).to.equal(2); + + expect(bidResponses[0].cpm).to.equal(firstBidCpm); + expect(bidResponses[1].cpm).to.equal(secondBidCpm); + }); + }); + + describe('getUserSyncs', function() { + const CustomerId = '99f20d18-c4b4-4a28-3d8e-d43e2c8cb4ac'; + const PlayerId = 'e4984e88-9ff4-45a3-8b9d-33aabcad634f'; + const UserSyncEndpoint = 'https://connatix.com/sync' + const Bid = {Cpm: 0.1, LineItems: [], RequestId: '2f897340c4eaa3', Ttl: 86400}; + + const serverResponse = { + body: { + CustomerId, + PlayerId, + UserSyncEndpoint, + Bids: [ Bid ] + }, + headers: function() { } + }; + + it('Should return an empty array when iframeEnabled: false', function () { + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [], {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return an empty array when serverResponses is emprt array', function () { + expect(spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [], {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return an empty array when iframeEnabled: true but serverResponses in an empty array', function () { + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [serverResponse], {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return an empty array when iframeEnabled: true but serverResponses in an not defined or null', function () { + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, undefined, {}, {}, {})).to.be.an('array').that.is.empty; + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, null, {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return one user sync object when iframeEnabled is true and serverResponses is not an empry array', function () { + expect(spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [serverResponse], {}, {}, {})).to.be.an('array').that.is.not.empty; + }); + it('Should return a list containing a single object having type: iframe and url: syncUrl', function () { + const userSyncList = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [serverResponse], undefined, undefined, undefined); + const { type, url } = userSyncList[0]; + expect(type).to.equal('iframe'); + expect(url).to.equal(UserSyncEndpoint); + }); + it('Should append gdpr: 0 if gdprConsent object is provided but gdprApplies field is not provided', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=0`); + }); + it('Should append gdpr having the value of gdprApplied if gdprConsent object is present and have gdprApplies field', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1`); + }); + it('Should append gdpr_consent if gdprConsent object is present and have gdprApplies field', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'alabala'}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=alabala`); + }); + it('Should encodeURI gdpr_consent corectly', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'test&2'}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=test%262`); + }); + it('Should append usp_consent to the url if uspConsent is provided', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'test&2'}, + '1YYYN', + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=test%262&us_privacy=1YYYN`); + }); + it('Should not modify the sync url if gppConsent param is provided', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'test&2'}, + '1YYYN', + {consent: '1'} + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=test%262&us_privacy=1YYYN`); + }); + }); + + describe('getBidFloor', function () { + this.beforeEach(function () { + bid = mockBidRequest(); + }); + + it('Should return 0 if both getFloor method and bidfloor param from bid are absent.', function () { + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(0); + }); + + it('Should return the value of the bidfloor parameter if the getFloor method is not defined but the bidfloor parameter is defined', function () { + const floorValue = 3; + bid.params.bidfloor = floorValue; + + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(floorValue); + }); + + it('Should return the value of the getFloor method if the getFloor method is defined but the bidfloor parameter is not defined', function () { + const floorValue = 7; + bid.getFloor = function() { + return { floor: floorValue }; + }; + + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(floorValue); + }); + + it('Should return the value of the getFloor method if both getFloor method and bidfloor parameter are defined', function () { + const floorParamValue = 3; + bid.params.bidfloor = floorParamValue; + + const floorMethodValue = 7; + bid.getFloor = function() { + return { floor: floorMethodValue }; + }; + + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(floorMethodValue); + }); + + it('Should return 0 if the getFloor method is defined and it crash when call it', function () { + bid.getFloor = function() { + throw new Error('error'); + }; + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(0); + }); + }); +}); From b2ca20e757fe06dc61ff1cbdc55548c796c55661 Mon Sep 17 00:00:00 2001 From: tamarm-undertone <140964382+tamarm-undertone@users.noreply.github.com> Date: Wed, 2 Aug 2023 22:40:45 +0300 Subject: [PATCH 03/88] Undertone Bid Adapter : documentation update (#10293) * * Update undertone adapter - change parameters - placementId parameter is now optional and not mandatory - undertoneBidAdapter.js * Updated undertone bid adapter tests accordingly - undertoneBidAdapter_spec.js * * Update undertone adapter - change parameters - placementId parameter is now optional and not mandatory - undertoneBidAdapter.js * Updated undertone bid adapter tests accordingly - undertoneBidAdapter_spec.js * fix lint issue in undertone adapter spec * added user sync function to undertone adapter * * Update undertone adapter - change parameters - placementId parameter is now optional and not mandatory - undertoneBidAdapter.js * Updated undertone bid adapter tests accordingly - undertoneBidAdapter_spec.js * added user sync function to undertone adapter * added user sync function to undertone adapter * revert package-lock.json * added user sync function to undertone adapter * Update undertoneBidAdapter.js * Update browsers.json * Undertone: added GPP support and video plcmt * Fix lint issues * undertone doc --------- Co-authored-by: omerko Co-authored-by: Omer Koren Co-authored-by: AnnaPerion Co-authored-by: Oran Hollaender Co-authored-by: tamirnPerion <44399211+tamirnPerion@users.noreply.github.com> Co-authored-by: tamarm Co-authored-by: tamarm <40788385+tamarm-perion@users.noreply.github.com> Co-authored-by: Idan Botbol Co-authored-by: Keren Gattegno --- modules/undertoneBidAdapter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/undertoneBidAdapter.md b/modules/undertoneBidAdapter.md index 8e0b234fd7a..1cfc912e360 100644 --- a/modules/undertoneBidAdapter.md +++ b/modules/undertoneBidAdapter.md @@ -23,7 +23,7 @@ Module that connects to Undertone's demand sources { bidder: "undertone", params: { - placementId: '10433394', + placementId: 1234, publisherId: 12345 } } From f41c26a2f50651f2357a35b8287e5ddf3d9b4582 Mon Sep 17 00:00:00 2001 From: matthieularere-msq <63732822+matthieularere-msq@users.noreply.github.com> Date: Wed, 2 Aug 2023 22:01:01 +0200 Subject: [PATCH 04/88] convert bidwon attributes to string (#10307) --- modules/mediasquareBidAdapter.js | 3 +++ test/spec/modules/mediasquareBidAdapter_spec.js | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/modules/mediasquareBidAdapter.js b/modules/mediasquareBidAdapter.js index 25b8c509477..87404b7a9ff 100644 --- a/modules/mediasquareBidAdapter.js +++ b/modules/mediasquareBidAdapter.js @@ -179,6 +179,9 @@ export const spec = { paramsToSearchFor.forEach(param => { if (bid['mediasquare'].hasOwnProperty(param)) { params[param] = bid['mediasquare'][param]; + if (typeof params[param] == 'number') { + params[param] = params[param].toString(); + } } }); }; diff --git a/test/spec/modules/mediasquareBidAdapter_spec.js b/test/spec/modules/mediasquareBidAdapter_spec.js index b2fbcb1ba59..125d4bef02b 100644 --- a/test/spec/modules/mediasquareBidAdapter_spec.js +++ b/test/spec/modules/mediasquareBidAdapter_spec.js @@ -1,5 +1,6 @@ import {expect} from 'chai'; import {spec} from 'modules/mediasquareBidAdapter.js'; +import { server } from 'test/mocks/xhr.js'; describe('MediaSquare bid adapter tests', function () { var DEFAULT_PARAMS = [{ @@ -208,6 +209,10 @@ describe('MediaSquare bid adapter tests', function () { const response = spec.interpretResponse(BID_RESPONSE, request); const won = spec.onBidWon(response[0]); expect(won).to.equal(true); + expect(server.requests.length).to.equal(1); + let message = JSON.parse(server.requests[0].requestBody); + expect(message).to.have.property('increment').exist; + expect(message).to.have.property('increment').and.to.equal('1'); }); it('Verifies user sync without cookie in bid response', function () { var syncs = spec.getUserSyncs({}, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); From 234cc6f126269ff02fdea97e54c65e59761a4776 Mon Sep 17 00:00:00 2001 From: balintvargha <122350182+balintvargha@users.noreply.github.com> Date: Wed, 2 Aug 2023 22:07:38 +0200 Subject: [PATCH 05/88] AdsInteractive Bid Adapter : added gvlid (#10301) * added gvlid to bid adapter * removed empty line --------- Co-authored-by: Balint Vargha --- modules/adsinteractiveBidAdapter.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/adsinteractiveBidAdapter.js b/modules/adsinteractiveBidAdapter.js index 3dbdb01ac5d..ad6bdfeb299 100644 --- a/modules/adsinteractiveBidAdapter.js +++ b/modules/adsinteractiveBidAdapter.js @@ -8,10 +8,12 @@ import { BANNER } from '../src/mediaTypes.js'; const ADSINTERACTIVE_CODE = 'adsinteractive'; const USER_SYNC_URL_IMAGE = 'https://sync.adsinteractive.com/img'; const USER_SYNC_URL_IFRAME = 'https://sync.adsinteractive.com/sync'; +const GVLID = 1212; export const spec = { code: ADSINTERACTIVE_CODE, supportedMediaTypes: [BANNER], + gvlid: GVLID, isBidRequestValid: (bid) => { return ( From 5eb2bab394642649895acf7750e61815b51cc6ff Mon Sep 17 00:00:00 2001 From: dzhang-criteo <87757739+dzhang-criteo@users.noreply.github.com> Date: Wed, 2 Aug 2023 22:12:30 +0200 Subject: [PATCH 06/88] Criteo Bid Adapter: do not call API when dataDeletionRequest has no id (#10314) --- modules/criteoBidAdapter.js | 14 +++++++------- test/spec/modules/criteoBidAdapter_spec.js | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 046c17ef5ec..0be40f1f3bc 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -315,14 +315,14 @@ export const spec = { const id = readFromAllStorages(BUNDLE_COOKIE_NAME); if (id) { deleteFromAllStorages(BUNDLE_COOKIE_NAME); + ajax('https://privacy.criteo.com/api/privacy/datadeletionrequest', + null, + JSON.stringify({ publisherUserId: id }), + { + contentType: 'application/json', + method: 'POST' + }); } - ajax('https://privacy.criteo.com/api/privacy/datadeletionrequest', - null, - JSON.stringify({ publisherUserId: id }), - { - contentType: 'application/json', - method: 'POST' - }); } }; diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index f28c6515651..c9f3b874b4e 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -217,15 +217,36 @@ describe('The Criteo bidding adapter', function () { } getCookieStub.callsFake(cookieName => cookieData[cookieName]); setCookieStub.callsFake((cookieName, value, expires) => cookieData[cookieName] = value); + getDataFromLocalStorageStub.callsFake(name => lsData[name]); removeDataFromLocalStorageStub.callsFake(name => lsData[name] = ''); spec.onDataDeletionRequest([]); expect(getCookieStub.calledOnce).to.equal(true); expect(setCookieStub.calledOnce).to.equal(true); + expect(getDataFromLocalStorageStub.calledOnce).to.equal(true); expect(removeDataFromLocalStorageStub.calledOnce).to.equal(true); expect(cookieData.cto_bundle).to.equal(''); expect(lsData.cto_bundle).to.equal(''); expect(ajaxStub.calledOnce).to.equal(true); }); + + it('should not call API when calling onDataDeletionRequest with no id', () => { + const cookieData = { + 'cto_bundle': '' + }; + const lsData = { + 'cto_bundle': '' + } + getCookieStub.callsFake(cookieName => cookieData[cookieName]); + setCookieStub.callsFake((cookieName, value, expires) => cookieData[cookieName] = value); + getDataFromLocalStorageStub.callsFake(name => lsData[name]); + removeDataFromLocalStorageStub.callsFake(name => lsData[name] = ''); + spec.onDataDeletionRequest([]); + expect(getCookieStub.calledOnce).to.be.true; + expect(setCookieStub.called).to.be.false; + expect(getDataFromLocalStorageStub.calledOnce).to.be.true + expect(removeDataFromLocalStorageStub.called).to.be.false; + expect(ajaxStub.called).to.be.false; + }); }); describe('isBidRequestValid', function () { From 5c7dab5b999a08ecc3d2127fd1117cf168908bae Mon Sep 17 00:00:00 2001 From: jdwieland8282 Date: Wed, 2 Aug 2023 14:17:11 -0600 Subject: [PATCH 07/88] Update eids.md (#10295) Adding source value for EUID --- modules/userId/eids.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/userId/eids.md b/modules/userId/eids.md index 63dbe475cc0..04073923ed1 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -272,6 +272,13 @@ userIdAsEids = [ id: 'some-random-id-value', atype: 3 }] + }, + { + source: 'euid.eu', + uids: [{ + id: 'some-random-id-value', + atype: 3 + }] } ] ``` From 9d8dc18d7e730ebb55361436e985b5e9b8e44efc Mon Sep 17 00:00:00 2001 From: "Takaaki.Kojima" Date: Thu, 3 Aug 2023 05:38:47 +0900 Subject: [PATCH 08/88] Update AdGenerationAdapter: fix userSync (#10310) --- modules/adgenerationBidAdapter.js | 7 ++++--- test/spec/modules/adgenerationBidAdapter_spec.js | 12 ++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/modules/adgenerationBidAdapter.js b/modules/adgenerationBidAdapter.js index 9cd7fbc1b80..bf75756174d 100644 --- a/modules/adgenerationBidAdapter.js +++ b/modules/adgenerationBidAdapter.js @@ -28,7 +28,7 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - const ADGENE_PREBID_VERSION = '1.6.1'; + const ADGENE_PREBID_VERSION = '1.6.2'; let serverRequests = []; for (let i = 0, len = validBidRequests.length; i < len; i++) { const validReq = validBidRequests[i]; @@ -41,6 +41,7 @@ export const spec = { const imuid = deepAccess(validReq, 'userId.imuid'); const gpid = deepAccess(validReq, 'ortb2Imp.ext.gpid'); const sua = deepAccess(validReq, 'ortb2.device.sua'); + const uid2 = deepAccess(validReq, 'userId.uid2.id'); let data = ``; data = tryAppendQueryString(data, 'posall', 'SSPLOC'); const id = getBidIdParameter('id', validReq.params); @@ -58,8 +59,8 @@ export const spec = { data = tryAppendQueryString(data, 'adgext_id5_id', id5id); data = tryAppendQueryString(data, 'adgext_id5_id_link_type', id5LinkType); data = tryAppendQueryString(data, 'adgext_imuid', imuid); - data = tryAppendQueryString(data, 'adgext_uid2', validReq.userId ? validReq.userId.uid2 : null); - data = tryAppendQueryString(data, 'gpid', gpid || null); + data = tryAppendQueryString(data, 'adgext_uid2', uid2); + data = tryAppendQueryString(data, 'gpid', gpid); data = tryAppendQueryString(data, 'uach', sua ? JSON.stringify(sua) : null); data = tryAppendQueryString(data, 'schain', validReq.schain ? JSON.stringify(validReq.schain) : null); diff --git a/test/spec/modules/adgenerationBidAdapter_spec.js b/test/spec/modules/adgenerationBidAdapter_spec.js index 55ba5654245..adfd38d22cc 100644 --- a/test/spec/modules/adgenerationBidAdapter_spec.js +++ b/test/spec/modules/adgenerationBidAdapter_spec.js @@ -184,12 +184,12 @@ describe('AdgenerationAdapter', function () { } }; const data = { - banner: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.1&imark=1&tp=https%3A%2F%2Fexample.com`, - bannerUSD: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=USD&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.1&imark=1&tp=https%3A%2F%2Fexample.com`, - native: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=1x1¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.1&tp=https%3A%2F%2Fexample.com`, - bannerWithHyperId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.1&imark=1&tp=https%3A%2F%2Fexample.com&hyper_id=novatiqId`, - bannerWithAdgextCriteoId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.1&adgext_criteo_id=criteo-id-test-1234567890&imark=1&tp=https%3A%2F%2Fexample.com`, - bannerWithAdgextIds: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.1&adgext_id5_id=id5-id-test-1234567890&adgext_id5_id_link_type=2&adgext_imuid=i.KrAH6ZAZTJOnH5S4N2sogA&adgext_uid2=%5Bobject%20Object%5D&gpid=%2F1111%2Fhomepage%23300x250&uach=%7B%22source%22%3A2%2C%22platform%22%3A%7B%22brand%22%3A%22macOS%22%7D%2C%22browsers%22%3A%5B%7B%22brand%22%3A%22Chromium%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Google%20Chrome%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Not%3AA-Brand%22%2C%22version%22%3A%5B%2299%22%5D%7D%5D%2C%22mobile%22%3A0%7D&schain=%7B%22ver%22%3A%221.0%22%2C%22complete%22%3A1%2C%22nodes%22%3A%5B%7B%22asi%22%3A%22indirectseller.com%22%2C%22sid%22%3A%2200001%22%2C%22hp%22%3A1%7D%5D%7D&imark=1&tp=https%3A%2F%2Fexample.com`, + banner: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&imark=1&tp=https%3A%2F%2Fexample.com`, + bannerUSD: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=USD&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&imark=1&tp=https%3A%2F%2Fexample.com`, + native: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=1x1¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&tp=https%3A%2F%2Fexample.com`, + bannerWithHyperId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&imark=1&tp=https%3A%2F%2Fexample.com&hyper_id=novatiqId`, + bannerWithAdgextCriteoId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&adgext_criteo_id=criteo-id-test-1234567890&imark=1&tp=https%3A%2F%2Fexample.com`, + bannerWithAdgextIds: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&adgext_id5_id=id5-id-test-1234567890&adgext_id5_id_link_type=2&adgext_imuid=i.KrAH6ZAZTJOnH5S4N2sogA&adgext_uid2=AgAAAAVacu1uAxgAxH%2BHJ8%2BnWlS2H4uVqr6i%2BHBDCNREHD8WKsio%2Fx7D8xXFuq1cJycUU86yXfTH9Xe%2F4C8KkH%2B7UCiU7uQxhyD7Qxnv251pEs6K8oK%2BBPLYR%2B8BLY%2FsJKesa%2FkoKwx1FHgUzIBum582tSy2Oo%2B7C6wYUaaV4QcLr%2F4LPA%3D&gpid=%2F1111%2Fhomepage%23300x250&uach=%7B%22source%22%3A2%2C%22platform%22%3A%7B%22brand%22%3A%22macOS%22%7D%2C%22browsers%22%3A%5B%7B%22brand%22%3A%22Chromium%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Google%20Chrome%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Not%3AA-Brand%22%2C%22version%22%3A%5B%2299%22%5D%7D%5D%2C%22mobile%22%3A0%7D&schain=%7B%22ver%22%3A%221.0%22%2C%22complete%22%3A1%2C%22nodes%22%3A%5B%7B%22asi%22%3A%22indirectseller.com%22%2C%22sid%22%3A%2200001%22%2C%22hp%22%3A1%7D%5D%7D&imark=1&tp=https%3A%2F%2Fexample.com`, }; it('sends bid request to ENDPOINT via GET', function () { const request = spec.buildRequests(bidRequests, bidderRequest)[0]; From c2cf1220851b556d88387faaae22250d355e345d Mon Sep 17 00:00:00 2001 From: dzhang-criteo <87757739+dzhang-criteo@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:30:32 +0200 Subject: [PATCH 09/88] Grid Bid Adapter: Fix GPID priorities (#10315) Issue: https://github.com/prebid/Prebid.js/issues/10187 --- modules/gridBidAdapter.js | 9 +--- test/spec/modules/gridBidAdapter_spec.js | 68 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index ee8712b1de3..2e44ac46f91 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -137,16 +137,9 @@ export const spec = { } if (ortb2Imp.ext) { + impObj.ext.gpid = ortb2Imp.ext.gpid?.toString() || ortb2Imp.ext.data?.pbadslot?.toString() || ortb2Imp.ext.data?.adserver?.adslot?.toString(); if (ortb2Imp.ext.data) { impObj.ext.data = ortb2Imp.ext.data; - if (impObj.ext.data.adserver && impObj.ext.data.adserver.adslot) { - impObj.ext.gpid = impObj.ext.data.adserver.adslot.toString(); - } else if (ortb2Imp.ext.data.pbadslot) { - impObj.ext.gpid = ortb2Imp.ext.data.pbadslot.toString(); - } - } - if (ortb2Imp.ext.gpid) { - impObj.ext.gpid = ortb2Imp.ext.gpid.toString(); } } } diff --git a/test/spec/modules/gridBidAdapter_spec.js b/test/spec/modules/gridBidAdapter_spec.js index 2f6e3990d82..da51ed058be 100644 --- a/test/spec/modules/gridBidAdapter_spec.js +++ b/test/spec/modules/gridBidAdapter_spec.js @@ -786,6 +786,74 @@ describe('TheMediaGrid Adapter', function () { }); }); + it('should prioritize pbadslot over adslot', function() { + const ortb2Imp = [{ + ext: { + data: { + adserver: { + adslot: 'adslot' + } + } + } + }, { + ext: { + data: { + adserver: { + adslot: 'adslot' + }, + pbadslot: 'pbadslot' + } + } + }]; + const bidRequestsWithOrtb2Imp = bidRequests.slice(0, 2).map((bid, ind) => { + return Object.assign({}, bid, ortb2Imp[ind] ? { ortb2Imp: {...bid.ortb2Imp, ...ortb2Imp[ind]} } : {}); + }); + const [request] = spec.buildRequests(bidRequestsWithOrtb2Imp, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].ext.gpid).to.equal(ortb2Imp[0].ext.data.adserver.adslot); + expect(payload.imp[1].ext.gpid).to.equal(ortb2Imp[1].ext.data.pbadslot); + }); + + it('should prioritize gpid over pbadslot and adslot', function() { + const ortb2Imp = [{ + ext: { + gpid: 'gpid', + data: { + adserver: { + adslot: 'adslot' + }, + pbadslot: 'pbadslot' + } + } + }, { + ext: { + gpid: 'gpid', + data: { + adserver: { + adslot: 'adslot' + } + } + } + }, { + ext: { + gpid: 'gpid', + data: { + pbadslot: 'pbadslot' + } + } + }]; + const bidRequestsWithOrtb2Imp = bidRequests.slice(0, 3).map((bid, ind) => { + return Object.assign({}, bid, ortb2Imp[ind] ? { ortb2Imp: {...bid.ortb2Imp, ...ortb2Imp[ind]} } : {}); + }); + const [request] = spec.buildRequests(bidRequestsWithOrtb2Imp, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].ext.gpid).to.equal(ortb2Imp[0].ext.gpid); + expect(payload.imp[1].ext.gpid).to.equal(ortb2Imp[1].ext.gpid); + expect(payload.imp[2].ext.gpid).to.equal(ortb2Imp[2].ext.gpid); + }); + it('should contain imp[].instl if available', function() { const ortb2Imp = [{ instl: 1 From 74d03dd3d164d59920c1fadd2a41e2f198bdea0b Mon Sep 17 00:00:00 2001 From: aplio Date: Thu, 3 Aug 2023 22:45:10 +0900 Subject: [PATCH 10/88] FreepassBidAdaptor. add publisher param, also set site,source (#10303) --- modules/freepassBidAdapter.js | 21 +++++++++- modules/freepassBidAdapter.md | 5 ++- test/spec/modules/freepassBidAdapter_spec.js | 44 ++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/modules/freepassBidAdapter.js b/modules/freepassBidAdapter.js index 02e433fa8fc..cdcc3c6a4b0 100644 --- a/modules/freepassBidAdapter.js +++ b/modules/freepassBidAdapter.js @@ -48,7 +48,7 @@ export const spec = { isBidRequestValid(bid) { logMessage('Validating bid: ', bid); - return !!bid.adUnitCode; + return !(!bid.adUnitCode || !bid.params || !bid.params.publisherId); }, buildRequests(validBidRequests, bidderRequest) { @@ -72,6 +72,25 @@ export const spec = { data.user = prepareUserInfo(data.user, freepassId); data.device = prepareDeviceInfo(data.device, freepassId); + // set site.page & site.publisher + data.site = data.site || {}; + data.site.publisher = data.site.publisher || {}; + // set site.publisher.id. from params.publisherId required + data.site.publisher.id = validBidRequests[0].params.publisherId; + // set site.publisher.domain from params.publisherUrl. optional + data.site.publisher.domain = validBidRequests[0].params?.publisherUrl; + + // set source + data.source = data.source || {}; + data.source.fd = 0; + data.source.tid = validBidRequests.ortb2?.source?.tid; + data.source.pchain = ''; + + // set imp.ext + validBidRequests.forEach((bidRequest, index) => { + data.imp[index].tagId = bidRequest.adUnitCode; + }); + data.test = validBidRequests[0].test || 0; logMessage('FreePass BidAdapter augmented ORTB bid request user: ', data.user); diff --git a/modules/freepassBidAdapter.md b/modules/freepassBidAdapter.md index 60957a1fbe5..7b56a469583 100644 --- a/modules/freepassBidAdapter.md +++ b/modules/freepassBidAdapter.md @@ -23,7 +23,10 @@ This BidAdapter requires the FreePass IdSystem to be configured. Please contact } }, bids: [{ - bidder: 'freepass' + bidder: 'freepass', + params: { + publisherId: '12345' + } }] } ]; diff --git a/test/spec/modules/freepassBidAdapter_spec.js b/test/spec/modules/freepassBidAdapter_spec.js index 5d75bd8e0f7..da73924c916 100644 --- a/test/spec/modules/freepassBidAdapter_spec.js +++ b/test/spec/modules/freepassBidAdapter_spec.js @@ -19,6 +19,9 @@ describe('FreePass adapter', function () { } }, adUnitCode: 'adunit-code', + params: { + publisherId: 'publisherIdValue' + } }; it('should return true when required params found', function () { @@ -30,6 +33,12 @@ describe('FreePass adapter', function () { delete localBid.adUnitCode; expect(spec.isBidRequestValid(localBid)).to.equal(false); }); + + it('should return false when params.publisherId is missing', function () { + let localBid = Object.assign({}, bid); + delete localBid.params.publisherId; + expect(spec.isBidRequestValid(localBid)).to.equal(false); + }); }); describe('buildRequests', function () { @@ -43,6 +52,10 @@ describe('FreePass adapter', function () { 'userId': '56c4c789-71ce-46f5-989e-9e543f3d5f96', 'commonId': 'commonIdValue' } + }, + 'adUnitCode': 'adunit-code', + 'params': { + 'publisherId': 'publisherIdValue' } }]; bidderRequest = {}; @@ -108,6 +121,33 @@ describe('FreePass adapter', function () { expect(ortbData.device.ext).to.be.an('object'); expect(ortbData.device.ext.is_accurate_ip).to.equal(0); }); + + it('it should add publisher related information w/o publisherUrl', function () { + const bidRequest = spec.buildRequests(bidRequests, bidderRequest); + const ortbData = bidRequest.data; + expect(ortbData.site).to.be.an('object'); + expect(ortbData.site.publisher.id).to.equal('publisherIdValue'); + // publisher.domain is optional + expect(ortbData.site.publisher.domain).to.be.undefined; + }); + + it('it should add publisher related information w/ publisherUrl', function () { + const PUBLISHER_URL = 'publisherUrlValue'; + let localBidRequests = [Object.assign({}, bidRequests[0])]; + localBidRequests[0].params.publisherUrl = PUBLISHER_URL; + const bidRequest = spec.buildRequests(localBidRequests, bidderRequest); + const ortbData = bidRequest.data; + expect(ortbData.site).to.be.an('object'); + expect(ortbData.site.publisher.id).to.equal('publisherIdValue'); + // publisher.domain is optional. set when given + expect(ortbData.site.publisher.domain).to.equal(PUBLISHER_URL); + }); + + it('it should imp.tagId from adUnitCode', function () { + const bidRequest = spec.buildRequests(bidRequests, bidderRequest); + const ortbData = bidRequest.data; + expect(ortbData.imp[0].tagId).to.equal('adunit-code'); + }); }); describe('interpretResponse', function () { @@ -122,6 +162,10 @@ describe('FreePass adapter', function () { 'userId': '56c4c789-71ce-46f5-989e-9e543f3d5f96', 'commonId': 'commonIdValue' } + }, + 'adUnitCode': 'adunit-code', + 'params': { + 'publisherId': 'publisherIdValue' } }]; bidderRequest = {}; From bad831ec84120e2a12ca5aa5ebba283dbf658a71 Mon Sep 17 00:00:00 2001 From: Adish Rao <30475159+adish1997@users.noreply.github.com> Date: Thu, 3 Aug 2023 19:30:36 +0530 Subject: [PATCH 11/88] added trustedstack bidder alias (#10302) Co-authored-by: Umer Qureshi --- modules/medianetBidAdapter.js | 11 +++--- test/spec/modules/medianetBidAdapter_spec.js | 36 ++++++++++---------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/modules/medianetBidAdapter.js b/modules/medianetBidAdapter.js index b9e00f45df9..659da0c16fb 100644 --- a/modules/medianetBidAdapter.js +++ b/modules/medianetBidAdapter.js @@ -20,7 +20,9 @@ import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import {getGlobal} from '../src/prebidGlobal.js'; const BIDDER_CODE = 'medianet'; +const TRUSTEDSTACK_CODE = 'trustedstack'; const BID_URL = 'https://prebid.media.net/rtb/prebid'; +const TRUSTEDSTACK_URL = 'https://prebid.trustedstack.com/rtb/trustedstack'; const PLAYER_URL = 'https://prebid.media.net/video/bundle.js'; const SLOT_VISIBILITY = { NOT_DETERMINED: 0, @@ -49,7 +51,7 @@ mnData.urlData = { }; const aliases = [ - { code: 'aax', gvlid: 720 }, + { code: TRUSTEDSTACK_CODE }, ]; getGlobal().medianetGlobals = getGlobal().medianetGlobals || {}; @@ -323,8 +325,9 @@ function normalizeCoordinates(coordinates) { } } -function getBidderURL(cid) { - return BID_URL + '?cid=' + encodeURIComponent(cid); +function getBidderURL(bidderCode, cid) { + const url = (bidderCode === TRUSTEDSTACK_CODE) ? TRUSTEDSTACK_URL : BID_URL; + return url + '?cid=' + encodeURIComponent(cid); } function generatePayload(bidRequests, bidderRequests) { @@ -463,7 +466,7 @@ export const spec = { let payload = generatePayload(bidRequests, bidderRequests); return { method: 'POST', - url: getBidderURL(payload.ext.customer_id), + url: getBidderURL(bidderRequests.bidderCode, payload.ext.customer_id), data: JSON.stringify(payload) }; }, diff --git a/test/spec/modules/medianetBidAdapter_spec.js b/test/spec/modules/medianetBidAdapter_spec.js index e29e259ebd7..4a221e97444 100644 --- a/test/spec/modules/medianetBidAdapter_spec.js +++ b/test/spec/modules/medianetBidAdapter_spec.js @@ -915,24 +915,24 @@ let VALID_BID_REQUEST = [{ cid: '8CUV090' } }, - VALID_PARAMS_AAX = { - bidder: 'aax', + VALID_PARAMS_TS = { + bidder: 'trustedstack', params: { - cid: 'AAXG123' + cid: 'TS012345' } }, PARAMS_MISSING = { bidder: 'medianet', }, - PARAMS_MISSING_AAX = { - bidder: 'aax', + PARAMS_MISSING_TS = { + bidder: 'trustedstack', }, PARAMS_WITHOUT_CID = { bidder: 'medianet', params: {} }, - PARAMS_WITHOUT_CID_AAX = { - bidder: 'aax', + PARAMS_WITHOUT_CID_TS = { + bidder: 'trustedstack', params: {} }, PARAMS_WITH_INTEGER_CID = { @@ -941,8 +941,8 @@ let VALID_BID_REQUEST = [{ cid: 8867587 } }, - PARAMS_WITH_INTEGER_CID_AAX = { - bidder: 'aax', + PARAMS_WITH_INTEGER_CID_TS = { + bidder: 'trustedstack', params: { cid: 8867587 } @@ -953,8 +953,8 @@ let VALID_BID_REQUEST = [{ cid: '' } }, - PARAMS_WITH_EMPTY_CID_AAX = { - bidder: 'aax', + PARAMS_WITH_EMPTY_CID_TS = { + bidder: 'trustedstack', params: { cid: '' } @@ -1783,34 +1783,34 @@ describe('Media.net bid adapter', function () { }); }); - describe('isBidRequestValid aax', function () { + describe('isBidRequestValid trustedstack', function () { it('should accept valid bid params', function () { - let isValid = spec.isBidRequestValid(VALID_PARAMS_AAX); + let isValid = spec.isBidRequestValid(VALID_PARAMS_TS); expect(isValid).to.equal(true); }); it('should reject bid if cid is not present', function () { - let isValid = spec.isBidRequestValid(PARAMS_WITHOUT_CID_AAX); + let isValid = spec.isBidRequestValid(PARAMS_WITHOUT_CID_TS); expect(isValid).to.equal(false); }); it('should reject bid if cid is not a string', function () { - let isValid = spec.isBidRequestValid(PARAMS_WITH_INTEGER_CID_AAX); + let isValid = spec.isBidRequestValid(PARAMS_WITH_INTEGER_CID_TS); expect(isValid).to.equal(false); }); it('should reject bid if cid is a empty string', function () { - let isValid = spec.isBidRequestValid(PARAMS_WITH_EMPTY_CID_AAX); + let isValid = spec.isBidRequestValid(PARAMS_WITH_EMPTY_CID_TS); expect(isValid).to.equal(false); }); it('should have missing params', function () { - let isValid = spec.isBidRequestValid(PARAMS_MISSING_AAX); + let isValid = spec.isBidRequestValid(PARAMS_MISSING_TS); expect(isValid).to.equal(false); }); }); - describe('interpretResponse aax', function () { + describe('interpretResponse trustedstack', function () { it('should not push response if no-bid', function () { let validBids = []; let bids = spec.interpretResponse(SERVER_RESPONSE_NOBID, []); From 827c3d5ef5e5710d207a1dd38a1be7ab2cf44613 Mon Sep 17 00:00:00 2001 From: Remi Henriot Date: Thu, 3 Aug 2023 17:09:16 +0200 Subject: [PATCH 12/88] Adagio Bid Adapter : embed gpp standard (#10190) --- modules/adagioBidAdapter.js | 16 +++- test/spec/modules/adagioBidAdapter_spec.js | 89 ++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index a3369ec3357..2e1b7453a7a 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -399,6 +399,17 @@ function _getUspConsent(bidderRequest) { return (deepAccess(bidderRequest, 'uspConsent')) ? { uspConsent: bidderRequest.uspConsent } : false; } +function _getGppConsent(bidderRequest) { + let gpp = deepAccess(bidderRequest, 'gppConsent.gppString') + let gppSid = deepAccess(bidderRequest, 'gppConsent.applicableSections') + + if (!gpp || !gppSid) { + gpp = deepAccess(bidderRequest, 'ortb2.regs.gpp', '') + gppSid = deepAccess(bidderRequest, 'ortb2.regs.gpp_sid', []) + } + return { gpp, gppSid } +} + function _getSchain(bidRequest) { return deepAccess(bidRequest, 'schain'); } @@ -976,6 +987,7 @@ export const spec = { const gdprConsent = _getGdprConsent(bidderRequest) || {}; const uspConsent = _getUspConsent(bidderRequest) || {}; const coppa = _getCoppa(); + const gppConsent = _getGppConsent(bidderRequest) const schain = _getSchain(validBidRequests[0]); const eids = _getEids(validBidRequests[0]) || []; const syncEnabled = deepAccess(config.getConfig('userSync'), 'syncEnabled') @@ -1143,7 +1155,9 @@ export const spec = { regs: { gdpr: gdprConsent, coppa: coppa, - ccpa: uspConsent + ccpa: uspConsent, + gpp: gppConsent.gpp, + gppSid: gppConsent.gppSid }, schain: schain, user: { diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index f9bf62206f5..7fa35456e3b 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -697,6 +697,95 @@ describe('Adagio bid adapter', () => { }); }); + describe('with GPP', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + + const regsGpp = 'regs_gpp_consent_string'; + const regsApplicableSections = [2]; + + const ortb2Gpp = 'ortb2_gpp_consent_string'; + const ortb2GppSid = [1]; + + context('When GPP in regs module', function() { + it('send gpp and gppSid to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + gppConsent: { + gppString: regsGpp, + applicableSections: regsApplicableSections, + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(regsGpp); + expect(requests[0].data.regs.gppSid).to.equal(regsApplicableSections); + }); + }); + + context('When GPP partially defined in regs module', function() { + it('send gpp and gppSid coming from ortb2 to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + gppConsent: { + gppString: regsGpp, + }, + ortb2: { + regs: { + gpp: ortb2Gpp, + gpp_sid: ortb2GppSid, + } + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(ortb2Gpp); + expect(requests[0].data.regs.gppSid).to.equal(ortb2GppSid); + }); + + it('send empty gpp and gppSid if no ortb2 fields to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + gppConsent: { + gppString: regsGpp, + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(''); + expect(requests[0].data.regs.gppSid).to.be.empty; + }); + }); + + context('When GPP defined in ortb2 module', function() { + it('send gpp and gppSid coming from ortb2 to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + ortb2: { + regs: { + gpp: ortb2Gpp, + gpp_sid: ortb2GppSid, + } + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(ortb2Gpp); + expect(requests[0].data.regs.gppSid).to.equal(ortb2GppSid); + }); + }); + + context('When GPP not defined in any modules', function() { + it('send empty gpp and gppSid', function() { + const bidderRequest = new BidderRequestBuilder({}).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(''); + expect(requests[0].data.regs.gppSid).to.be.empty; + }); + }); + }); + describe('with userID modules', function() { const userIdAsEids = [{ 'source': 'pubcid.org', From 2162fda9b7c450d9a901a7a676f8516e2833373d Mon Sep 17 00:00:00 2001 From: optidigital-prebid <124287395+optidigital-prebid@users.noreply.github.com> Date: Thu, 3 Aug 2023 17:12:30 +0200 Subject: [PATCH 13/88] Optidigital Bid Adapter : update usersync (#10279) * add new adapter * update adapter * update unit tests * update adapter --------- Co-authored-by: Dawid W --- modules/optidigitalBidAdapter.js | 41 +++++++++++-------- .../modules/optidigitalBidAdapter_spec.js | 11 ++++- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/modules/optidigitalBidAdapter.js b/modules/optidigitalBidAdapter.js index a0fa641a424..9f84ff034ea 100755 --- a/modules/optidigitalBidAdapter.js +++ b/modules/optidigitalBidAdapter.js @@ -7,6 +7,7 @@ const GVL_ID = 915; const ENDPOINT_URL = 'https://pbs.optidigital.com/bidder'; const USER_SYNC_URL_IFRAME = 'https://scripts.opti-digital.com/js/presync.html?endpoint=optidigital'; let CUR = 'USD'; +let isSynced = false; export const spec = { code: BIDDER_CODE, @@ -46,8 +47,6 @@ export const spec = { referrer: (bidderRequest.refererInfo && bidderRequest.refererInfo.page) ? bidderRequest.refererInfo.page : '', hb_version: '$prebid.version$', deviceWidth: document.documentElement.clientWidth, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: deepAccess(validBidRequests[0], 'auctionId'), bidderRequestId: deepAccess(validBidRequests[0], 'bidderRequestId'), publisherId: deepAccess(validBidRequests[0], 'params.publisherId'), imp: validBidRequests.map(bidRequest => buildImp(bidRequest, ortb2)), @@ -56,6 +55,10 @@ export const spec = { bapp: deepAccess(validBidRequests[0], 'params.bapp') || [] } + if (validBidRequests[0].auctionId) { + payload.auctionId = validBidRequests[0].auctionId; + } + if (validBidRequests[0].params.pageTemplate && validBidRequests[0].params.pageTemplate !== '') { payload.pageTemplate = validBidRequests[0].params.pageTemplate; } @@ -136,21 +139,23 @@ export const spec = { */ getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { let syncurl = ''; + if (!isSynced) { + // Attaching GDPR Consent Params in UserSync url + if (gdprConsent) { + syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + if (uspConsent && uspConsent.consentString) { + syncurl += `&ccpa_consent=${uspConsent.consentString}`; + } - // Attaching GDPR Consent Params in UserSync url - if (gdprConsent) { - syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); - syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); - } - if (uspConsent && uspConsent.consentString) { - syncurl += `&ccpa_consent=${uspConsent.consentString}`; - } - - if (syncOptions.iframeEnabled) { - return [{ - type: 'iframe', - url: USER_SYNC_URL_IFRAME + syncurl - }]; + if (syncOptions.iframeEnabled) { + isSynced = true; + return [{ + type: 'iframe', + url: USER_SYNC_URL_IFRAME + syncurl + }]; + } } }, }; @@ -218,4 +223,8 @@ function _getFloor (bid, sizes, currency) { return floor !== null ? floor : bid.params.floor; } +export function resetSync() { + isSynced = false; +} + registerBidder(spec); diff --git a/test/spec/modules/optidigitalBidAdapter_spec.js b/test/spec/modules/optidigitalBidAdapter_spec.js index caa12483ea9..62c37f85cc8 100755 --- a/test/spec/modules/optidigitalBidAdapter_spec.js +++ b/test/spec/modules/optidigitalBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec } from 'modules/optidigitalBidAdapter.js'; +import { spec, resetSync } from 'modules/optidigitalBidAdapter.js'; import * as utils from 'src/utils.js'; const ENDPOINT = 'https://pbs.optidigital.com/bidder'; @@ -497,6 +497,7 @@ describe('optidigitalAdapterTests', function () { let test; beforeEach(function () { test = sinon.sandbox.create(); + resetSync(); }); afterEach(function() { test.restore(); @@ -508,16 +509,22 @@ describe('optidigitalAdapterTests', function () { }]); }); - it('should return appropriate URL', function() { + it('should return appropriate URL with GDPR equals to 1 and GDPR consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=1&gdpr_consent=foo` }]); + }); + it('should return appropriate URL with GDPR equals to 0 and GDPR consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=0&gdpr_consent=foo` }]); + }); + it('should return appropriate URL with GDPR equals to 1 and no consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=1&gdpr_consent=` }]); + }); + it('should return appropriate URL with GDPR equals to 1, GDPR consent and CCPA consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, {consentString: 'fooUsp'})).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=1&gdpr_consent=foo&ccpa_consent=fooUsp` }]); From e98c38698bc2379422d1e19b24b83504c7c215eb Mon Sep 17 00:00:00 2001 From: Aleksandr <32703851+pro-nsk@users.noreply.github.com> Date: Thu, 3 Aug 2023 17:30:46 +0200 Subject: [PATCH 14/88] Alkimi Bid Adapter : support new parameters (#10296) * Alkimi bid adapter * Alkimi bid adapter * Alkimi bid adapter * alkimi adapter * onBidWon change * sign utils * auction ID as bid request ID * unit test fixes * change maintainer info * Updated the ad unit params * features support added * transfer adUnitCode * transfer adUnitCode: test * AlkimiBidAdapter getFloor() using * ALK-504 Multi size ad slot support * ALK-504 Multi size ad slot support * Support new OpenRTB parameters * Support new oRTB2 parameters * remove pos parameter --------- Co-authored-by: Alexander Bogdanov Co-authored-by: Kalidas Engaiahraj Co-authored-by: mihanikw2g <92710748+mihanikw2g@users.noreply.github.com> Co-authored-by: Nikulin Mikhail Co-authored-by: mik --- modules/alkimiBidAdapter.js | 26 ++++++++++++++++------ test/spec/modules/alkimiBidAdapter_spec.js | 12 +++------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/modules/alkimiBidAdapter.js b/modules/alkimiBidAdapter.js index c087b3061a0..64a46779a06 100644 --- a/modules/alkimiBidAdapter.js +++ b/modules/alkimiBidAdapter.js @@ -1,5 +1,5 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {deepAccess, deepClone} from '../src/utils.js'; +import {deepAccess, deepClone, getDNT, generateUUID} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import {VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; @@ -12,7 +12,7 @@ export const spec = { supportedMediaTypes: ['banner', 'video'], isBidRequestValid: function (bid) { - return !!(bid.params && bid.params.bidFloor && bid.params.token); + return !!(bid.params && bid.params.token); }, buildRequests: function (validBidRequests, bidderRequest) { @@ -28,12 +28,15 @@ export const spec = { bids.push({ token: bidRequest.params.token, - pos: bidRequest.params.pos, + instl: bidRequest.params.instl, + exp: bidRequest.params.exp, bidFloor: getBidFloor(bidRequest, formatTypes), sizes: prepareSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes')), playerSizes: prepareSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize')), impMediaTypes: formatTypes, - adUnitCode: bidRequest.adUnitCode + adUnitCode: bidRequest.adUnitCode, + video: deepAccess(bidRequest, 'mediaTypes.video'), + banner: deepAccess(bidRequest, 'mediaTypes.banner') }) bidIds.push(bidRequest.bidId) }) @@ -41,14 +44,23 @@ export const spec = { const alkimiConfig = config.getConfig('alkimi'); let payload = { - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - requestId: bidderRequest.auctionId, + requestId: generateUUID(), signRequest: {bids, randomUUID: alkimiConfig && alkimiConfig.randomUUID}, bidIds, referer: bidderRequest.refererInfo.page, signature: alkimiConfig && alkimiConfig.signature, schain: validBidRequests[0].schain, - cpp: config.getConfig('coppa') ? 1 : 0 + cpp: config.getConfig('coppa') ? 1 : 0, + device: { + dnt: getDNT() ? 1 : 0, + w: screen.width, + h: screen.height + }, + ortb2: { + at: bidderRequest.ortb2?.at, + bcat: bidderRequest.ortb2?.bcat, + wseat: bidderRequest.ortb2?.wseat + } } if (bidderRequest && bidderRequest.gdprConsent) { diff --git a/test/spec/modules/alkimiBidAdapter_spec.js b/test/spec/modules/alkimiBidAdapter_spec.js index a396e5b8139..08f00186358 100644 --- a/test/spec/modules/alkimiBidAdapter_spec.js +++ b/test/spec/modules/alkimiBidAdapter_spec.js @@ -14,8 +14,7 @@ const REQUEST = { }, 'params': { bidFloor: 0.1, - token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', - pos: 7 + token: 'e64782a4-8e68-4c38-965b-80ccf115d46f' }, 'userIdAsEids': [{ 'source': 'criteo.com', @@ -96,10 +95,6 @@ describe('alkimiBidAdapter', function () { delete bid.params.token expect(spec.isBidRequestValid(bid)).to.equal(false) - bid = Object.assign({}, REQUEST) - delete bid.params.bidFloor - expect(spec.isBidRequestValid(bid)).to.equal(false) - bid = Object.assign({}, REQUEST) delete bid.params expect(spec.isBidRequestValid(bid)).to.equal(false) @@ -109,7 +104,6 @@ describe('alkimiBidAdapter', function () { describe('buildRequests', function () { let bidRequests = [REQUEST] let requestData = { - auctionId: '123', refererInfo: { page: 'http://test.com/path.html' }, @@ -137,10 +131,10 @@ describe('alkimiBidAdapter', function () { it('sends bid request to ENDPOINT via POST', function () { expect(bidderRequest.method).to.equal('POST') - expect(bidderRequest.data.requestId).to.equal('123') + expect(bidderRequest.data.requestId).to.not.equal(undefined) expect(bidderRequest.data.referer).to.equal('http://test.com/path.html') expect(bidderRequest.data.schain).to.deep.contains({ ver: '1.0', complete: 1, nodes: [{ asi: 'alkimi-onboarding.com', sid: '00001', hp: 1 }] }) - expect(bidderRequest.data.signRequest.bids).to.deep.contains({ token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', pos: 7, bidFloor: 0.1, sizes: [{width: 300, height: 250}], playerSizes: [], impMediaTypes: ['Banner'], adUnitCode: 'bannerAdUnitCode' }) + expect(bidderRequest.data.signRequest.bids).to.deep.contains({ token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', bidFloor: 0.1, sizes: [{ width: 300, height: 250 }], playerSizes: [], impMediaTypes: ['Banner'], adUnitCode: 'bannerAdUnitCode', instl: undefined, exp: undefined, banner: { sizes: [[300, 250]] }, video: undefined }) expect(bidderRequest.data.signRequest.randomUUID).to.equal(undefined) expect(bidderRequest.data.bidIds).to.deep.contains('456') expect(bidderRequest.data.signature).to.equal(undefined) From 0809dd2fe6dfb4b704e769e324cd3722b2d5d441 Mon Sep 17 00:00:00 2001 From: prebidtappx <77485538+prebidtappx@users.noreply.github.com> Date: Thu, 3 Aug 2023 18:08:57 +0200 Subject: [PATCH 15/88] Tappx Refactor: Optimizing and adding more checkers and tests (#10317) Co-authored-by: Jordi Arnau --- modules/tappxBidAdapter.js | 31 +++++++++++++---------- test/spec/modules/tappxBidAdapter_spec.js | 19 ++++++++++++++ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/modules/tappxBidAdapter.js b/modules/tappxBidAdapter.js index f45f16a0728..531927114c8 100644 --- a/modules/tappxBidAdapter.js +++ b/modules/tappxBidAdapter.js @@ -12,7 +12,7 @@ const BIDDER_CODE = 'tappx'; const GVLID_CODE = 628; const TTL = 360; const CUR = 'USD'; -const TAPPX_BIDDER_VERSION = '0.1.2'; +const TAPPX_BIDDER_VERSION = '0.1.3'; const TYPE_CNN = 'prebidjs'; const LOG_PREFIX = '[TAPPX]: '; const VIDEO_SUPPORT = ['instream', 'outstream']; @@ -146,22 +146,22 @@ function validBasic(bid) { return false; } - if (bid.params.tappxkey == null) { + if (!bid.params.tappxkey) { logWarn(LOG_PREFIX, 'Please review the mandatory Tappxkey parameter.'); return false; } - if (bid.params.host == null) { + if (!bid.params.host) { logWarn(LOG_PREFIX, 'Please review the mandatory Host parameter.'); return false; } - let classicEndpoint = true + let classicEndpoint = true; if ((new RegExp(`^(vz.*|zz.*)\\.*$`, 'i')).test(bid.params.host)) { - classicEndpoint = false + classicEndpoint = false; } - if (classicEndpoint && bid.params.endpoint == null) { + if (classicEndpoint && !bid.params.endpoint) { logWarn(LOG_PREFIX, 'Please review the mandatory endpoint Tappx parameters.'); return false; } @@ -191,7 +191,7 @@ function validMediaType(bid) { */ function interpretBid(serverBid, request) { let bidReturned = { - requestId: request.bids.bidId, + requestId: request.bids?.bidId, cpm: serverBid.price, currency: serverBid.cur ? serverBid.cur : CUR, width: serverBid.w, @@ -206,7 +206,7 @@ function interpretBid(serverBid, request) { if (typeof serverBid.nurl != 'undefined') { bidReturned.nurl = serverBid.nurl } if (typeof serverBid.burl != 'undefined') { bidReturned.burl = serverBid.burl } - if (typeof request.bids.mediaTypes !== 'undefined' && typeof request.bids.mediaTypes.video !== 'undefined') { + if (typeof request.bids?.mediaTypes !== 'undefined' && typeof request.bids?.mediaTypes.video !== 'undefined') { bidReturned.vastXml = serverBid.adm; bidReturned.vastUrl = serverBid.lurl; bidReturned.ad = serverBid.adm; @@ -214,13 +214,12 @@ function interpretBid(serverBid, request) { bidReturned.width = serverBid.w; bidReturned.height = serverBid.h; - if (request.bids.mediaTypes.video.context === 'outstream') { - const url = (serverBid.ext.purl) ? serverBid.ext.purl : false; - if (typeof url === 'undefined') { + if (request.bids?.mediaTypes.video.context === 'outstream') { + if (!serverBid.ext.purl) { logWarn(LOG_PREFIX, 'Error getting player outstream from tappx'); return false; } - bidReturned.renderer = createRenderer(bidReturned, request, url); + bidReturned.renderer = createRenderer(bidReturned, request, serverBid.ext.purl); } } else { bidReturned.ad = serverBid.adm; @@ -228,7 +227,7 @@ function interpretBid(serverBid, request) { } if (typeof bidReturned.adomain !== 'undefined' || bidReturned.adomain !== null) { - bidReturned.meta = { advertiserDomains: request.bids.adomain }; + bidReturned.meta = { advertiserDomains: request.bids?.adomain }; } return bidReturned; @@ -305,6 +304,8 @@ function buildOneRequest(validBidRequests, bidderRequest) { let h; if (bannerMediaType) { + if (!Array.isArray(bannerMediaType.sizes)) { logWarn(LOG_PREFIX, 'Banner sizes array not found.'); } + let banner = {}; w = bannerMediaType.sizes[0][0]; h = bannerMediaType.sizes[0][1]; @@ -342,7 +343,9 @@ function buildOneRequest(validBidRequests, bidderRequest) { } if ((video.w === undefined || video.w == null || video.w <= 0) || - (video.h === undefined || video.h == null || video.h <= 0)) { + (video.h === undefined || video.h == null || video.h <= 0)) { + if (!Array.isArray(videoMediaType.playerSize)) { logWarn(LOG_PREFIX, 'Video playerSize array not found.'); } + w = videoMediaType.playerSize[0][0]; h = videoMediaType.playerSize[0][1]; video.w = w; diff --git a/test/spec/modules/tappxBidAdapter_spec.js b/test/spec/modules/tappxBidAdapter_spec.js index 58a62fb2869..46fac8de1e2 100644 --- a/test/spec/modules/tappxBidAdapter_spec.js +++ b/test/spec/modules/tappxBidAdapter_spec.js @@ -466,4 +466,23 @@ describe('Tappx bid adapter', function () { assert.isString(_extractPageUrl(validBidRequests, bidderRequest)); }); }) + + describe('Empty params values from bid tests', function() { + let validBidRequest = JSON.parse(JSON.stringify(c_BIDREQUEST)); + + it('should return false when tappxkey is empty', function () { + validBidRequest.bids[0].params.tappxkey = ''; + assert.isFalse(spec.isBidRequestValid(validBidRequest.bids[0])); + }); + + it('should return false when host is empty', function () { + validBidRequest.bids[0].params.host = ''; + assert.isFalse(spec.isBidRequestValid(validBidRequest.bids[0])); + }); + + it('should return false when endpoint is empty', function () { + validBidRequest.bids[0].params.endpoint = ''; + assert.isFalse(spec.isBidRequestValid(validBidRequest.bids[0])); + }); + }); }); From c6c325460d3f74cc89c3c98485be16c13c5ad8cb Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Thu, 3 Aug 2023 16:38:30 +0000 Subject: [PATCH 16/88] Prebid 8.7.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 282e5214691..b83a9d0666f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.7.0-pre", + "version": "8.7.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 2d0d1997662..39fe9872b9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.7.0-pre", + "version": "8.7.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 01348fb545705a4c36d394768d04a2163459af7d Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Thu, 3 Aug 2023 16:38:30 +0000 Subject: [PATCH 17/88] Increment version to 8.8.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 b83a9d0666f..fc17840b5c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.7.0", + "version": "8.8.0-pre", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 39fe9872b9d..4a0e2b1129f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.7.0", + "version": "8.8.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 5360a1b79e125e99c616577d92f2a76a6d3ad509 Mon Sep 17 00:00:00 2001 From: Snigel <108489367+snigelweb@users.noreply.github.com> Date: Thu, 3 Aug 2023 20:02:21 +0200 Subject: [PATCH 18/88] update Snigel bid adapter (#10243) --- modules/snigelBidAdapter.js | 85 ++++++++++++++++++---- modules/snigelBidAdapter.md | 12 ++- test/spec/modules/snigelBidAdapter_spec.js | 63 ++++++++++++++-- 3 files changed, 135 insertions(+), 25 deletions(-) diff --git a/modules/snigelBidAdapter.js b/modules/snigelBidAdapter.js index f41fb98d436..489d0bcdc9e 100644 --- a/modules/snigelBidAdapter.js +++ b/modules/snigelBidAdapter.js @@ -1,7 +1,7 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; -import {deepAccess, isArray, isFn, isPlainObject} from '../src/utils.js'; +import {deepAccess, isArray, isFn, isPlainObject, inIframe, getDNT} from '../src/utils.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; import {getGlobal} from '../src/prebidGlobal.js'; @@ -13,6 +13,7 @@ const DEFAULT_CURRENCIES = ['USD']; const FLOOR_MATCH_ALL_SIZES = '*'; const getConfig = config.getConfig; +const refreshes = {}; export const spec = { code: BIDDER_CODE, @@ -29,12 +30,15 @@ export const spec = { method: 'POST', url: getEndpoint(), data: JSON.stringify({ - id: bidderRequest.bidderRequestId, + id: bidderRequest.auctionId, + accountId: deepAccess(bidRequests, '0.params.accountId'), + site: deepAccess(bidRequests, '0.params.site'), cur: getCurrencies(), test: getTestFlag(), - devw: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth, - devh: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight, version: getGlobal().version, + gpp: deepAccess(bidderRequest, 'gppConsent.gppString') || deepAccess(bidderRequest, 'ortb2.regs.gpp'), + gpp_sid: + deepAccess(bidderRequest, 'gppConsent.applicableSections') || deepAccess(bidderRequest, 'ortb2.regs.gpp_sid'), gdprApplies: gdprApplies, gdprConsentString: gdprApplies === true ? deepAccess(bidderRequest, 'gdprConsent.consentString') : undefined, gdprConsentProv: gdprApplies === true ? deepAccess(bidderRequest, 'gdprConsent.addtlConsent') : undefined, @@ -43,12 +47,24 @@ export const spec = { eids: deepAccess(bidRequests, '0.userIdAsEids'), schain: deepAccess(bidRequests, '0.schain'), page: getPage(bidderRequest), + topframe: inIframe() === true ? 0 : 1, + device: { + w: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth, + h: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight, + dnt: getDNT() ? 1 : 0, + language: getLanguage(), + }, placements: bidRequests.map((r) => { return { - uuid: r.bidId, + id: r.adUnitCode, + tid: r.transactionId, + gpid: deepAccess(r, 'ortb2Imp.ext.gpid'), + pbadslot: deepAccess(r, 'ortb2Imp.ext.data.pbadslot') || deepAccess(r, 'ortb2Imp.ext.gpid'), name: r.params.placement, sizes: r.sizes, floor: getPriceFloor(r, BANNER, FLOOR_MATCH_ALL_SIZES), + refresh: getRefreshInformation(r.adUnitCode), + params: r.params.additionalParams, }; }), }), @@ -56,14 +72,14 @@ export const spec = { }; }, - interpretResponse: function (serverResponse) { + interpretResponse: function (serverResponse, bidRequest) { if (!serverResponse.body || !serverResponse.body.bids) { return []; } return serverResponse.body.bids.map((bid) => { return { - requestId: bid.uuid, + requestId: mapIdToRequestId(bid.id, bidRequest), cpm: bid.price, creativeId: bid.crid, currency: serverResponse.body.cur, @@ -77,9 +93,9 @@ export const spec = { }); }, - getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { const syncUrl = getSyncUrl(responses || []); - if (syncUrl && syncOptions.iframeEnabled && hasSyncConsent(gdprConsent, uspConsent)) { + if (syncUrl && syncOptions.iframeEnabled && hasSyncConsent(gdprConsent, uspConsent, gppConsent)) { return [{type: 'iframe', url: getSyncEndpoint(syncUrl, gdprConsent)}]; } }, @@ -101,6 +117,14 @@ function getTestFlag() { return getConfig(`${BIDDER_CODE}.test`) === true; } +function getLanguage() { + return navigator && navigator.language + ? navigator.language.indexOf('-') != -1 + ? navigator.language.split('-')[0] + : navigator.language + : undefined; +} + function getCurrencies() { const currencyOverrides = getConfig(`${BIDDER_CODE}.cur`); if (currencyOverrides !== undefined && (!isArray(currencyOverrides) || currencyOverrides.length === 0)) { @@ -130,14 +154,43 @@ function getPriceFloor(bidRequest, mediaType, size) { } } -function hasSyncConsent(gdprConsent, uspConsent) { - if (gdprConsent?.gdprApplies && !hasPurpose1Consent(gdprConsent)) { - return false; - } else if (uspConsent && uspConsent[1] === 'Y' && uspConsent[2] === 'Y') { - return false; - } else { - return true; +function getRefreshInformation(adUnitCode) { + const refresh = refreshes[adUnitCode]; + if (!refresh) { + refreshes[adUnitCode] = { + count: 0, + previousTime: new Date(), + }; + return undefined; } + + const currentTime = new Date(); + const timeDifferenceSeconds = Math.floor((currentTime - refresh.previousTime) / 1000); + refresh.count += 1; + refresh.previousTime = currentTime; + return { + count: refresh.count, + time: timeDifferenceSeconds, + }; +} + +function mapIdToRequestId(id, bidRequest) { + return bidRequest.bidderRequest.bids.filter((bid) => bid.adUnitCode === id)[0].bidId; +} + +function hasUspConsent(uspConsent) { + return typeof uspConsent !== 'string' || !(uspConsent[0] === '1' && uspConsent[2] === 'Y'); +} + +function hasGppConsent(gppConsent) { + return ( + !(gppConsent && Array.isArray(gppConsent.applicableSections)) || + gppConsent.applicableSections.every((section) => typeof section === 'number' && section <= 5) + ); +} + +function hasSyncConsent(gdprConsent, uspConsent, gppConsent) { + return hasPurpose1Consent(gdprConsent) && hasUspConsent(uspConsent) && hasGppConsent(gppConsent); } function getSyncUrl(responses) { diff --git a/modules/snigelBidAdapter.md b/modules/snigelBidAdapter.md index a83e133144f..f9bb1951d21 100644 --- a/modules/snigelBidAdapter.md +++ b/modules/snigelBidAdapter.md @@ -15,9 +15,13 @@ Please reach out to us [through our contact form](https://snigel.com/get-in-touc # Parameters -| Name | Required | Description | Example | -| :--- | :-------- | :---------- | :------ | -| placement | Yes | Placement identifier | top_leaderboard | +| Name | Required | Description | +| :-------- | :------- | :------------------- | +| accountId | Yes | Account identifier | +| site | Yes | Site identifier | +| placement | Yes | Placement identifier | + +Snigel will provide all of these parameters to you. # Test @@ -37,6 +41,8 @@ var adUnits = [ { bidder: "snigel", params: { + accountId: "1000", + site: "test.com", placement: "prebid_test_placement", }, }, diff --git a/test/spec/modules/snigelBidAdapter_spec.js b/test/spec/modules/snigelBidAdapter_spec.js index 3fc09493f03..7fe2387ca6c 100644 --- a/test/spec/modules/snigelBidAdapter_spec.js +++ b/test/spec/modules/snigelBidAdapter_spec.js @@ -20,6 +20,7 @@ const makeBidRequest = function (overrides) { }; const BASE_BIDDER_REQUEST = { + auctionId: 'test', bidderRequestId: 'test', refererInfo: { canonicalUrl: 'https://localhost', @@ -53,8 +54,8 @@ describe('snigelBidAdapter', function () { it('should build a single request for every impression and its placement', function () { const bidderRequest = Object.assign({}, BASE_BIDDER_REQUEST); const bidRequests = [ - makeBidRequest({bidId: 'a', params: {placement: 'top_leaderboard'}}), - makeBidRequest({bidId: 'b', params: {placement: 'bottom_leaderboard'}}), + makeBidRequest({bidId: 'a', adUnitCode: 'au_a', params: {placement: 'top_leaderboard'}}), + makeBidRequest({bidId: 'b', adUnitCode: 'au_b', params: {placement: 'bottom_leaderboard'}}), ]; const request = spec.buildRequests(bidRequests, bidderRequest); @@ -70,9 +71,9 @@ describe('snigelBidAdapter', function () { expect(data).to.have.property('page').and.to.equal('https://localhost'); expect(data).to.have.property('placements'); expect(data.placements.length).to.equal(2); - expect(data.placements[0].uuid).to.equal('a'); + expect(data.placements[0].id).to.equal('au_a'); expect(data.placements[0].name).to.equal('top_leaderboard'); - expect(data.placements[1].uuid).to.equal('b'); + expect(data.placements[1].id).to.equal('au_b'); expect(data.placements[1].name).to.equal('bottom_leaderboard'); }); @@ -127,6 +128,56 @@ describe('snigelBidAdapter', function () { const data = JSON.parse(request.data); expect(data).to.have.property('coppa').and.to.equal(true); }); + + it('should forward refresh information', function () { + const bidderRequest = Object.assign({}, BASE_BIDDER_REQUEST); + const topLeaderboard = makeBidRequest({adUnitCode: 'top_leaderboard'}); + const bottomLeaderboard = makeBidRequest({adUnitCode: 'bottom_leaderboard'}); + const sidebar = makeBidRequest({adUnitCode: 'sidebar'}); + + // first auction, no refresh + let request = spec.buildRequests([topLeaderboard, bottomLeaderboard], bidderRequest); + expect(request).to.have.property('data'); + let data = JSON.parse(request.data); + expect(data).to.have.property('placements'); + expect(data.placements.length).to.equal(2); + expect(data.placements[0].id).to.equal('top_leaderboard'); + expect(data.placements[0].refresh).to.be.undefined; + expect(data.placements[1].id).to.equal('bottom_leaderboard'); + expect(data.placements[1].refresh).to.be.undefined; + + // second auction for top leaderboard, was refreshed + request = spec.buildRequests([topLeaderboard, sidebar], bidderRequest); + expect(request).to.have.property('data'); + data = JSON.parse(request.data); + expect(data).to.have.property('placements'); + expect(data.placements.length).to.equal(2); + expect(data.placements[0].id).to.equal('top_leaderboard'); + expect(data.placements[0].refresh).to.not.be.undefined; + expect(data.placements[0].refresh.count).to.equal(1); + expect(data.placements[0].refresh.time).to.be.greaterThanOrEqual(0); + expect(data.placements[1].id).to.equal('sidebar'); + expect(data.placements[1].refresh).to.be.undefined; + + // third auction, all units refreshed at some point + request = spec.buildRequests([topLeaderboard, bottomLeaderboard, sidebar], bidderRequest); + expect(request).to.have.property('data'); + data = JSON.parse(request.data); + expect(data).to.have.property('placements'); + expect(data.placements.length).to.equal(3); + expect(data.placements[0].id).to.equal('top_leaderboard'); + expect(data.placements[0].refresh).to.not.be.undefined; + expect(data.placements[0].refresh.count).to.equal(2); + expect(data.placements[0].refresh.time).to.be.greaterThanOrEqual(0); + expect(data.placements[1].id).to.equal('bottom_leaderboard'); + expect(data.placements[1].refresh).to.not.be.undefined; + expect(data.placements[1].refresh.count).to.equal(1); + expect(data.placements[1].refresh.time).to.be.greaterThanOrEqual(0); + expect(data.placements[2].id).to.equal('sidebar'); + expect(data.placements[2].refresh).to.not.be.undefined; + expect(data.placements[2].refresh.count).to.equal(1); + expect(data.placements[2].refresh.time).to.be.greaterThanOrEqual(0); + }); }); describe('interpretResponse', function () { @@ -146,7 +197,7 @@ describe('snigelBidAdapter', function () { cur: 'USD', bids: [ { - uuid: BASE_BID_REQUEST.bidId, + id: BASE_BID_REQUEST.adUnitCode, price: 0.0575, ad: '

Test Ad

', width: 728, @@ -160,7 +211,7 @@ describe('snigelBidAdapter', function () { }, }; - const bids = spec.interpretResponse(serverResponse, {}); + const bids = spec.interpretResponse(serverResponse, {bidderRequest: {bids: [BASE_BID_REQUEST]}}); expect(bids.length).to.equal(1); const bid = bids[0]; expect(isValid(BASE_BID_REQUEST.adUnitCode, bid)).to.be.true; From ad2ea1f4fc822cce6f89f76e3994c152e4072afd Mon Sep 17 00:00:00 2001 From: vrishko <141218108+vrishko@users.noreply.github.com> Date: Fri, 4 Aug 2023 23:18:13 +0300 Subject: [PATCH 19/88] bid won and error functions (#10316) --- modules/smartyadsBidAdapter.js | 19 ++++++- test/spec/modules/smartyadsBidAdapter_spec.js | 49 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/modules/smartyadsBidAdapter.js b/modules/smartyadsBidAdapter.js index 7dbd2f3993a..1644a15e92d 100644 --- a/modules/smartyadsBidAdapter.js +++ b/modules/smartyadsBidAdapter.js @@ -3,6 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { ajax } from '../src/ajax.js'; const BIDDER_CODE = 'smartyads'; const AD_URL = 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js'; @@ -122,7 +123,23 @@ export const spec = { } return syncs - } + }, + + onBidWon: function(bid) { + if (bid.winUrl) { + ajax(bid.winUrl, () => {}, JSON.stringify(bid)); + } else { + ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&winTest=1', () => {}, JSON.stringify(bid)); + } + }, + + onTimeout: function(bid) { + ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&bidTimeout=1', () => {}, JSON.stringify(bid)); + }, + + onBidderError: function(bid) { + ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&bidderError=1', () => {}, JSON.stringify(bid)); + }, }; diff --git a/test/spec/modules/smartyadsBidAdapter_spec.js b/test/spec/modules/smartyadsBidAdapter_spec.js index cbc4a6405e8..350cad33704 100644 --- a/test/spec/modules/smartyadsBidAdapter_spec.js +++ b/test/spec/modules/smartyadsBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec} from '../../../modules/smartyadsBidAdapter.js'; import { config } from '../../../src/config.js'; +import {server} from '../../mocks/xhr'; describe('SmartyadsAdapter', function () { let bid = { @@ -14,6 +15,21 @@ describe('SmartyadsAdapter', function () { } }; + let bidResponse = { + width: 300, + height: 250, + mediaType: 'banner', + ad: `test mode`, + requestId: '23fhj33i987f', + cpm: 0.1, + ttl: 120, + creativeId: '123', + netRevenue: true, + currency: 'USD', + dealId: 'HASH', + sid: 1234 + }; + describe('isBidRequestValid', function () { it('Should return true if there are bidId, params and sourceid parameters present', function () { expect(spec.isBidRequestValid(bid)).to.be.true; @@ -257,4 +273,37 @@ describe('SmartyadsAdapter', function () { ]); }); }); + + describe('onBidWon', function () { + it('should exists', function () { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + + it('should send a valid bid won notice', function () { + spec.onBidWon(bidResponse); + expect(server.requests.length).to.equal(1); + }); + }); + + describe('onTimeout', function () { + it('should exists', function () { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + + it('should send a valid bid timeout notice', function () { + spec.onTimeout({}); + expect(server.requests.length).to.equal(1); + }); + }); + + describe('onBidderError', function () { + it('should exists', function () { + expect(spec.onBidderError).to.exist.and.to.be.a('function'); + }); + + it('should send a valid bidder error notice', function () { + spec.onBidderError({}); + expect(server.requests.length).to.equal(1); + }); + }); }); From b42001167e0bd0bfa4ae93f522f521ceef365950 Mon Sep 17 00:00:00 2001 From: ChangsikChoi <49671733+ChangsikChoi@users.noreply.github.com> Date: Mon, 7 Aug 2023 10:59:45 +0900 Subject: [PATCH 20/88] A1Media RTD Module: fix cookie name (#10322) Co-authored-by: ChangsikChoi <> --- modules/a1MediaRtdProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/a1MediaRtdProvider.js b/modules/a1MediaRtdProvider.js index eca6b501ec1..9fa6b307b6a 100644 --- a/modules/a1MediaRtdProvider.js +++ b/modules/a1MediaRtdProvider.js @@ -8,7 +8,7 @@ const REAL_TIME_MODULE = 'realTimeData'; const MODULE_NAME = 'a1Media'; const SCRIPT_URL = 'https://linkback.contentsfeed.com/src'; export const A1_SEG_KEY = '__a1tg'; -export const A1_AUD_KEY = 'a1gid'; +export const A1_AUD_KEY = 'a1_gid'; export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: MODULE_NAME}); From 1b8cd389869762bf57d421b2ea5593a56a9811f7 Mon Sep 17 00:00:00 2001 From: jsfledd Date: Mon, 7 Aug 2023 11:07:36 -0700 Subject: [PATCH 21/88] Nativo Bid Adapter : changed request method to POST and added OpenRTB payload (#10323) * Initial nativoBidAdapter document creation (js, md and spec) * Fulling working prebid using nativoBidAdapter. Support for GDPR and CCPA in user syncs. * Added defult size settings based on the largest ad unit. Added response body validation. Added consent to request url qs params. * Changed bidder endpoint url * Changed double quotes to single quotes. * Reverted package-json.lock to remove modifications from PR * Added optional bidder param 'url' so the ad server can force- match an existing placement * Lint fix. Added space after if. * Added new QS param to send various adUnit data to adapter endpopint * Updated unit test for new QS param * Added qs param to keep track of ad unit refreshes * Updated bidMap key default value * Updated refresh increment logic * Refactored spread operator for IE11 support * Updated isBidRequestValid check * Refactored Object.enties to use Object.keys to fix CircleCI testing errors * Updated bid mapping key creation to prioritize ad unit code over placementId * Added filtering by ad, advertiser and campaign. * Merged master * Added more robust bidDataMap with multiple key access * Deduped filer values * Rolled back package.json * Duped upstream/master's package.lock file ... not sure how it got changed in the first place * Small refactor of filterData length check. Removed comparison with 0 since a length value of 0 is already falsy. * Added bid sizes to request * Fixed function name in spec. Added unit tests. * Added priceFloor module support * Added protection agains empty url parameter * Changed ntv_url QS param to use referrer.location instead of referrer.page * Removed testing 'only' flag * Added ntv_url QS param value validation * Added userId support * Added unit tests, refactored for bugs * Wrapped ajax in try/catch * Added more unit testing * Updated eid check for duplicate values. Removed error logging as we no longer need it. * Removed spec test .only. Fixed unit tests that were breaking. * Added Prebid version to nativo exchange request * Removed unused bidder methods * Added OpenRTB payload response. Changes requerst type to POST. * Removed debug log * Added/fixed tests --- modules/nativoBidAdapter.js | 24 +++++++++++++++++++--- test/spec/modules/nativoBidAdapter_spec.js | 7 ++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/modules/nativoBidAdapter.js b/modules/nativoBidAdapter.js index c62a74e6d6c..69a270247cd 100644 --- a/modules/nativoBidAdapter.js +++ b/modules/nativoBidAdapter.js @@ -2,7 +2,20 @@ import { deepAccess, isEmpty } from '../src/utils.js' import { registerBidder } from '../src/adapters/bidderFactory.js' import { BANNER } from '../src/mediaTypes.js' import { getGlobal } from '../src/prebidGlobal.js' -// import { config } from 'src/config' +import { ortbConverter } from '../libraries/ortbConverter/converter.js' + +const converter = ortbConverter({ + context: { + // `netRevenue` and `ttl` are required properties of bid responses - provide a default for them + netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false + ttl: 30 // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + imp.tagid = bidRequest.adUnitCode + return imp; + } +}); const BIDDER_CODE = 'nativo' const BIDDER_ENDPOINT = 'https://exchange.postrelease.com/prebid' @@ -136,6 +149,10 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { + // Get OpenRTB Data + const openRTBData = converter.toORTB({bidRequests: validBidRequests, bidderRequest}) + const openRTBDataString = JSON.stringify(openRTBData) + const requestData = new RequestData() requestData.addBidRequestDataSource(new UserEIDs()) @@ -271,8 +288,9 @@ export const spec = { const requestUrl = buildRequestUrl(BIDDER_ENDPOINT, qsParamStrings) let serverRequest = { - method: 'GET', - url: requestUrl + method: 'POST', + url: requestUrl, + data: openRTBDataString, } return serverRequest diff --git a/test/spec/modules/nativoBidAdapter_spec.js b/test/spec/modules/nativoBidAdapter_spec.js index 51e78d1f6d6..75fb357b196 100644 --- a/test/spec/modules/nativoBidAdapter_spec.js +++ b/test/spec/modules/nativoBidAdapter_spec.js @@ -112,7 +112,7 @@ describe('nativoBidAdapterTests', function () { bidRequests = [JSON.parse(bidRequestString)] }) - it('url should contain query string parameters', function () { + it('Request should be POST, with JSON string payload and QS params should be added to the url', function () { const request = spec.buildRequests(bidRequests, { bidderRequestId: 123456, refererInfo: { @@ -120,6 +120,11 @@ describe('nativoBidAdapterTests', function () { }, }) + expect(request.method).to.equal('POST') + + expect(request.data).to.exist + expect(request.data).to.be.a('string') + expect(request.url).to.exist expect(request.url).to.be.a('string') From 5249b47bad55dfcfe6f1e7ec40fb312ae768171f Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 8 Aug 2023 13:42:53 +0200 Subject: [PATCH 22/88] =?UTF-8?q?=C3=AD=C2=B0=C2=9B=20fix=20bug=20for=20in?= =?UTF-8?q?terpreting=20bid=20response=20without=20zone=20id=20provided=20?= =?UTF-8?q?in=20request=20(#10288)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: v.raybaud --- modules/criteoBidAdapter.js | 106 ++++++----- test/spec/modules/criteoBidAdapter_spec.js | 201 +++++++++++++++++++++ 2 files changed, 265 insertions(+), 42 deletions(-) diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 0be40f1f3bc..472d36655c0 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -3,7 +3,6 @@ import { loadExternalScript } from '../src/adloader.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { find } from '../src/polyfill.js'; import { verify } from 'criteo-direct-rsa-validate/build/verify.js'; // ref#2 import { getStorageManager } from '../src/storageManager.js'; import { getRefererInfo } from '../src/refererDetection.js'; @@ -219,51 +218,53 @@ export const spec = { if (body && body.slots && isArray(body.slots)) { body.slots.forEach(slot => { - const bidRequest = find(request.bidRequests, b => b.adUnitCode === slot.impid && (!b.params.zoneId || parseInt(b.params.zoneId) === slot.zoneid)); - const bidId = bidRequest.bidId; - const bid = { - requestId: bidId, - cpm: slot.cpm, - currency: slot.currency, - netRevenue: true, - ttl: slot.ttl || 60, - creativeId: slot.creativecode, - width: slot.width, - height: slot.height, - dealId: slot.deal, - }; - if (body.ext?.paf?.transmission && slot.ext?.paf?.content_id) { - const pafResponseMeta = { - content_id: slot.ext.paf.content_id, - transmission: response.ext.paf.transmission + const bidRequest = getAssociatedBidRequest(request.bidRequests, slot); + if (bidRequest) { + const bidId = bidRequest.bidId; + const bid = { + requestId: bidId, + cpm: slot.cpm, + currency: slot.currency, + netRevenue: true, + ttl: slot.ttl || 60, + creativeId: slot.creativecode, + width: slot.width, + height: slot.height, + dealId: slot.deal, }; - bid.meta = Object.assign({}, bid.meta, { paf: pafResponseMeta }); - } - if (slot.adomain) { - bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [slot.adomain].flat() }); - } - if (slot.ext?.meta?.networkName) { - bid.meta = Object.assign({}, bid.meta, { networkName: slot.ext.meta.networkName }) - } - if (slot.native) { - if (bidRequest.params.nativeCallback) { - bid.ad = createNativeAd(bidId, slot.native, bidRequest.params.nativeCallback); - } else { - bid.native = createPrebidNativeAd(slot.native); - bid.mediaType = NATIVE; + if (body.ext?.paf?.transmission && slot.ext?.paf?.content_id) { + const pafResponseMeta = { + content_id: slot.ext.paf.content_id, + transmission: response.ext.paf.transmission + }; + bid.meta = Object.assign({}, bid.meta, { paf: pafResponseMeta }); } - } else if (slot.video) { - bid.vastUrl = slot.displayurl; - bid.mediaType = VIDEO; - const context = deepAccess(bidRequest, 'mediaTypes.video.context'); - // if outstream video, add a default render for it. - if (context === OUTSTREAM) { - bid.renderer = createOutstreamVideoRenderer(slot); + if (slot.adomain) { + bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [slot.adomain].flat() }); } - } else { - bid.ad = slot.creative; + if (slot.ext?.meta?.networkName) { + bid.meta = Object.assign({}, bid.meta, { networkName: slot.ext.meta.networkName }) + } + if (slot.native) { + if (bidRequest.params.nativeCallback) { + bid.ad = createNativeAd(bidId, slot.native, bidRequest.params.nativeCallback); + } else { + bid.native = createPrebidNativeAd(slot.native); + bid.mediaType = NATIVE; + } + } else if (slot.video) { + bid.vastUrl = slot.displayurl; + bid.mediaType = VIDEO; + const context = deepAccess(bidRequest, 'mediaTypes.video.context'); + // if outstream video, add a default render for it. + if (context === OUTSTREAM) { + bid.renderer = createOutstreamVideoRenderer(slot); + } + } else { + bid.ad = slot.creative; + } + bids.push(bid); } - bids.push(bid); }); } @@ -786,6 +787,27 @@ function createOutstreamVideoRenderer(slot) { return renderer; } +function getAssociatedBidRequest(bidRequests, slot) { + for (const request of bidRequests) { + if (request.adUnitCode === slot.impid) { + if (request.params.zoneId && parseInt(request.params.zoneId) === slot.zoneid) { + return request; + } else if (slot.native) { + if (request.mediaTypes?.native || request.nativeParams) { + return request; + } + } else if (slot.video) { + if (request.mediaTypes?.video) { + return request; + } + } else if (request.mediaTypes?.banner || request.sizes) { + return request; + } + } + } + return undefined; +} + export function tryGetCriteoFastBid() { // begin ref#1 try { diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index c9f3b874b4e..5f93593fb32 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -1886,6 +1886,11 @@ describe('The Criteo bidding adapter', function () { bidRequests: [{ adUnitCode: 'test-requestId', bidId: 'test-bidId', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { networkId: 456, } @@ -1904,6 +1909,202 @@ describe('The Criteo bidding adapter', function () { expect(bids[0].meta.networkName).to.equal('Criteo'); }); + it('should properly parse a bid response with a networkId with twin ad unit banner win', function () { + const response = { + body: { + slots: [{ + impid: 'test-requestId', + cpm: 1.23, + creative: 'test-ad', + creativecode: 'test-crId', + width: 728, + height: 90, + deal: 'myDealCode', + adomain: ['criteo.com'], + ext: { + meta: { + networkName: 'Criteo' + } + } + }], + }, + }; + const request = { + bidRequests: [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mpeg'], + playerSize: [640, 480], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] + } + }, + params: { + networkId: 456, + }, + }, { + adUnitCode: 'test-requestId', + bidId: 'test-bidId2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + networkId: 456, + } + }] + }; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('test-bidId2'); + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].ad).to.equal('test-ad'); + expect(bids[0].creativeId).to.equal('test-crId'); + expect(bids[0].width).to.equal(728); + expect(bids[0].height).to.equal(90); + expect(bids[0].dealId).to.equal('myDealCode'); + expect(bids[0].meta.advertiserDomains[0]).to.equal('criteo.com'); + expect(bids[0].meta.networkName).to.equal('Criteo'); + }); + + it('should properly parse a bid response with a networkId with twin ad unit video win', function () { + const response = { + body: { + slots: [{ + impid: 'test-requestId', + bidId: 'abc123', + cpm: 1.23, + displayurl: 'http://test-ad', + width: 728, + height: 90, + zoneid: 123, + video: true, + ext: { + meta: { + networkName: 'Criteo' + } + } + }], + }, + }; + const request = { + bidRequests: [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mpeg'], + playerSize: [728, 90], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] + } + }, + params: { + networkId: 456, + }, + }, { + adUnitCode: 'test-requestId', + bidId: 'test-bidId2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + networkId: 456, + } + }] + }; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].vastUrl).to.equal('http://test-ad'); + expect(bids[0].mediaType).to.equal(VIDEO); + }); + + it('should properly parse a bid response with a networkId with twin ad unit native win', function () { + const response = { + body: { + slots: [{ + impid: 'test-requestId', + cpm: 1.23, + creative: 'test-ad', + creativecode: 'test-crId', + width: 728, + height: 90, + deal: 'myDealCode', + adomain: ['criteo.com'], + native: { + 'products': [{ + 'sendTargetingKeys': false, + 'title': 'Product title', + 'description': 'Product desc', + 'price': '100', + 'click_url': 'https://product.click', + 'image': { + 'url': 'https://publisherdirect.criteo.com/publishertag/preprodtest/creative.png', + 'height': 300, + 'width': 300 + }, + 'call_to_action': 'Try it now!' + }], + 'advertiser': { + 'description': 'sponsor', + 'domain': 'criteo.com', + 'logo': { 'url': 'https://www.criteo.com/images/criteo-logo.svg', 'height': 300, 'width': 300 } + }, + 'privacy': { + 'optout_click_url': 'https://info.criteo.com/privacy/informations', + 'optout_image_url': 'https://static.criteo.net/flash/icon/nai_small.png', + }, + 'impression_pixels': [{ 'url': 'https://my-impression-pixel/test/impression' }, { 'url': 'https://cas.com/lg.com' }] + }, + ext: { + meta: { + networkName: 'Criteo' + } + } + }], + }, + }; + const request = { + bidRequests: [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + mediaTypes: { + native: {} + }, + params: { + networkId: 456, + }, + }, { + adUnitCode: 'test-requestId', + bidId: 'test-bidId2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + networkId: 456, + } + }] + }; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].mediaType).to.equal(NATIVE); + }); + it('should properly parse a bid response with a zoneId', function () { const response = { body: { From 8df5e93d45c116c203f5ac091a61a1519dbfdbf7 Mon Sep 17 00:00:00 2001 From: ghguo Date: Tue, 8 Aug 2023 08:16:47 -0400 Subject: [PATCH 23/88] Update adrelevantisBidAdapter.js (#10294) --- modules/adrelevantisBidAdapter.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/adrelevantisBidAdapter.js b/modules/adrelevantisBidAdapter.js index cf785a1fc87..f1f92e5dd5e 100644 --- a/modules/adrelevantisBidAdapter.js +++ b/modules/adrelevantisBidAdapter.js @@ -597,6 +597,8 @@ function parseMediaType(rtbBid) { const adType = rtbBid.ad_type; if (adType === VIDEO) { return VIDEO; + } else if (adType === NATIVE) { + return NATIVE; } else { return BANNER; } From 49418b1673b3c455088ef787db1dc54d1238fe81 Mon Sep 17 00:00:00 2001 From: Christian <98148000+duduchristian@users.noreply.github.com> Date: Tue, 8 Aug 2023 23:59:57 +0800 Subject: [PATCH 24/88] Operaads: add ID System sbumodule (#10270) Co-authored-by: hongxingp --- modules/.submodules.json | 3 +- modules/operaadsBidAdapter.js | 5 + modules/operaadsBidAdapter.md | 10 +- modules/operaadsIdSystem.js | 106 +++++++++++++++++++++ modules/operaadsIdSystem.md | 52 ++++++++++ test/spec/modules/eids_spec.js | 15 +++ test/spec/modules/operaadsIdSystem_spec.js | 53 +++++++++++ 7 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 modules/operaadsIdSystem.js create mode 100644 modules/operaadsIdSystem.md create mode 100644 test/spec/modules/operaadsIdSystem_spec.js diff --git a/modules/.submodules.json b/modules/.submodules.json index b3685658084..fdc79c8b868 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -46,7 +46,8 @@ "zeotapIdPlusIdSystem", "adqueryIdSystem", "gravitoIdSystem", - "freepassIdSystem" + "freepassIdSystem", + "operaadsIdSystem" ], "adpod": [ "freeWheelAdserverVideo", diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index e721fb85fd7..b45c0452319 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -680,6 +680,11 @@ function mapNativeImage(image, type) { * @returns {String} userId */ function getUserId(bidRequest) { + let operaId = deepAccess(bidRequest, 'userId.operaId'); + if (operaId) { + return operaId; + } + let sharedId = deepAccess(bidRequest, 'userId.sharedid.id'); if (sharedId) { return sharedId; diff --git a/modules/operaadsBidAdapter.md b/modules/operaadsBidAdapter.md index 709c67a04a7..6c5a4646dd0 100644 --- a/modules/operaadsBidAdapter.md +++ b/modules/operaadsBidAdapter.md @@ -135,18 +135,18 @@ var adUnits = [{ ### User Ids -Opera Ads Bid Adapter uses `sharedId`, `pubcid` or `tdid`, please config at least one. +Opera Ads Bid Adapter uses `operaId`, please refer to [`Opera ID System`](./operaadsIdSystem.md). ```javascript pbjs.setConfig({ ..., userSync: { userIds: [{ - name: 'sharedId', + name: 'operaId', storage: { - name: '_sharedID', // name of the 1st party cookie - type: 'cookie', - expires: 30 + name: 'operaId', + type: 'html5', + expires: 14 } }] } diff --git a/modules/operaadsIdSystem.js b/modules/operaadsIdSystem.js new file mode 100644 index 00000000000..09dd8512a2b --- /dev/null +++ b/modules/operaadsIdSystem.js @@ -0,0 +1,106 @@ +/** + * This module adds operaId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/operaadsIdSystem + * @requires module:modules/userId + */ +import * as ajax from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { logMessage, logError } from '../src/utils.js'; + +const MODULE_NAME = 'operaId'; +const ID_KEY = MODULE_NAME; +const version = '1.0'; +const SYNC_URL = 'https://t.adx.opera.com/identity/'; +const AJAX_TIMEOUT = 300; +const AJAX_OPTIONS = {method: 'GET', withCredentials: true, contentType: 'application/json'}; + +function constructUrl(pairs) { + const queries = []; + for (let key in pairs) { + queries.push(`${key}=${encodeURIComponent(pairs[key])}`); + } + return `${SYNC_URL}?${queries.join('&')}`; +} + +function asyncRequest(url, cb) { + ajax.ajaxBuilder(AJAX_TIMEOUT)( + url, + { + success: response => { + try { + const jsonResponse = JSON.parse(response); + const { uid: operaId } = jsonResponse; + cb(operaId); + return; + } catch (e) { + logError(`${MODULE_NAME}: invalid response`, response); + } + cb(); + }, + error: (err) => { + logError(`${MODULE_NAME}: ID error response`, err); + cb(); + } + }, + null, + AJAX_OPTIONS + ); +} + +export const operaIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + + /** + * @type {string} + */ + version, + + /** + * decode the stored id value for passing to bid requests + * @function + * @param {string} id + * @returns {{'operaId': string}} + */ + decode: (id) => + id != null && id.length > 0 + ? { [ID_KEY]: id } + : undefined, + + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @returns {IdResponse|undefined} + */ + getId(config, consentData) { + logMessage(`${MODULE_NAME}: start synchronizing opera uid`); + const params = (config && config.params) || {}; + if (typeof params.pid !== 'string' || params.pid.length == 0) { + logError(`${MODULE_NAME}: submodule requires a publisher ID to be defined`); + return; + } + + const { pid, syncUrl = SYNC_URL } = params; + const url = constructUrl(syncUrl, { publisherId: pid }); + + return { + callback: (cb) => { + asyncRequest(url, cb); + } + } + }, + + eids: { + 'operaId': { + source: 't.adx.opera.com', + atype: 1 + }, + } +}; + +submodule('userId', operaIdSubmodule); diff --git a/modules/operaadsIdSystem.md b/modules/operaadsIdSystem.md new file mode 100644 index 00000000000..288fb960b96 --- /dev/null +++ b/modules/operaadsIdSystem.md @@ -0,0 +1,52 @@ +# Opera ID System + +For help adding this module, please contact [adtech-prebid-group@opera.com](adtech-prebid-group@opera.com). + +### Prebid Configuration + +You should configure this module under your `userSync.userIds[]` configuration: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [ + { + name: "operaId", + storage: { + name: "operaId", + type: "html5", + expires: 14 + }, + params: { + pid: "your-pulisher-ID-here" + } + } + ] + } +}) +``` +
+ +| Param under `userSync.userIds[]` | Scope | Type | Description | Example | +| -------------------------------- | -------- | ------ | ----------------------------- | ----------------------------------------- | +| name | Required | string | ID for the operaId module | `"operaId"` | +| storage | Optional | Object | Settings for operaId storage | See [storage settings](#storage-settings) | +| params | Required | Object | Parameters for opreaId module | See [params](#params) | +
+ +### Params + +| Param under `params` | Scope | Type | Description | Example | +| -------------------- | -------- | ------ | ------------------------------ | --------------- | +| pid | Required | string | Publisher ID assigned by Opera | `"pub12345678"` | +
+ +### Storage Settings + +The following settings are suggested for the `storage` property in the `userSync.userIds[]` object: + +| Param under `storage` | Type | Description | Example | +| --------------------- | ------------- | -------------------------------------------------------------------------------- | ----------- | +| name | String | Where the ID will be stored | `"operaId"` | +| type | String | For best performance, this should be `"html5"` | `"html5"` | +| expires | Number <= 30 | number of days until the stored ID expires. **Must be less than or equal to 30** | `14` | \ No newline at end of file diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index 9291ec88569..f82defc3c45 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -547,6 +547,21 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('operaId', function() { + const userId = { + operaId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 't.adx.opera.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }); + }); + it('33acrossId', function() { const userId = { '33acrossId': { diff --git a/test/spec/modules/operaadsIdSystem_spec.js b/test/spec/modules/operaadsIdSystem_spec.js new file mode 100644 index 00000000000..d81f643d62f --- /dev/null +++ b/test/spec/modules/operaadsIdSystem_spec.js @@ -0,0 +1,53 @@ +import { operaIdSubmodule } from 'modules/operaadsIdSystem' +import * as ajaxLib from 'src/ajax.js' + +const TEST_ID = 'opera-test-id'; +const operaIdRemoteResponse = { uid: TEST_ID }; + +describe('operaId submodule properties', () => { + it('should expose a "name" property equal to "operaId"', () => { + expect(operaIdSubmodule.name).to.equal('operaId'); + }); +}); + +function fakeRequest(fn) { + const ajaxBuilderStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(() => { + return (url, cbObj) => { + cbObj.success(JSON.stringify(operaIdRemoteResponse)); + } + }); + fn(); + ajaxBuilderStub.restore(); +} + +describe('operaId submodule getId', function() { + it('request to the fake server to correctly extract test ID', function() { + fakeRequest(() => { + const moduleIdCallbackResponse = operaIdSubmodule.getId({ params: { pid: 'pub123' } }); + moduleIdCallbackResponse.callback((id) => { + expect(id).to.equal(operaIdRemoteResponse.operaId); + }); + }); + }); + + it('request to the fake server without publiser ID', function() { + fakeRequest(() => { + const moduleIdCallbackResponse = operaIdSubmodule.getId({ params: {} }); + expect(moduleIdCallbackResponse).to.equal(undefined); + }); + }); +}); + +describe('operaId submodule decode', function() { + it('should respond with an object containing "operaId" as key with the value', () => { + expect(operaIdSubmodule.decode(TEST_ID)).to.deep.equal({ + operaId: TEST_ID + }); + }); + + it('should respond with undefined if the value is not a string or an empty string', () => { + [1, 2.0, null, undefined, NaN, [], {}].forEach((value) => { + expect(operaIdSubmodule.decode(value)).to.equal(undefined); + }); + }); +}); From 6318fc9aff6492aa1809f3f9227ac9e814947b96 Mon Sep 17 00:00:00 2001 From: johanbrandmetrics <91625093+johanbrandmetrics@users.noreply.github.com> Date: Wed, 9 Aug 2023 12:21:05 +0200 Subject: [PATCH 25/88] Set default result of consent check to undefined (#10327) --- modules/brandmetricsRtdProvider.js | 57 ++++++++++--------- .../modules/brandmetricsRtdProvider_spec.js | 6 ++ 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/modules/brandmetricsRtdProvider.js b/modules/brandmetricsRtdProvider.js index 30844c9c483..bd7a33ff037 100644 --- a/modules/brandmetricsRtdProvider.js +++ b/modules/brandmetricsRtdProvider.js @@ -21,13 +21,14 @@ let billableEventsInitialized = false function init (config, userConsent) { const hasConsent = checkConsent(userConsent) + const initialize = hasConsent !== false - if (hasConsent) { + if (initialize) { const moduleConfig = getMergedConfig(config) initializeBrandmetrics(moduleConfig.params.scriptId) initializeBillableEvents() } - return hasConsent + return initialize } /** @@ -36,33 +37,35 @@ function init (config, userConsent) { * @returns {boolean} */ function checkConsent (userConsent) { - let consent = false - - if (userConsent && userConsent.gdpr && userConsent.gdpr.gdprApplies) { - const gdpr = userConsent.gdpr - - if (gdpr.vendorData) { - const vendor = gdpr.vendorData.vendor - const purpose = gdpr.vendorData.purpose - - let vendorConsent = false - if (vendor.consents) { - vendorConsent = vendor.consents[GVL_ID] + let consent + + if (userConsent) { + if (userConsent.gdpr && userConsent.gdpr.gdprApplies) { + const gdpr = userConsent.gdpr + + if (gdpr.vendorData) { + const vendor = gdpr.vendorData.vendor + const purpose = gdpr.vendorData.purpose + + let vendorConsent = false + if (vendor.consents) { + vendorConsent = vendor.consents[GVL_ID] + } + + if (vendor.legitimateInterests) { + vendorConsent = vendorConsent || vendor.legitimateInterests[GVL_ID] + } + + const purposes = TCF_PURPOSES.map(id => { + return (purpose.consents && purpose.consents[id]) || (purpose.legitimateInterests && purpose.legitimateInterests[id]) + }) + const purposesValid = purposes.filter(p => p === true).length === TCF_PURPOSES.length + consent = vendorConsent && purposesValid } - - if (vendor.legitimateInterests) { - vendorConsent = vendorConsent || vendor.legitimateInterests[GVL_ID] - } - - const purposes = TCF_PURPOSES.map(id => { - return (purpose.consents && purpose.consents[id]) || (purpose.legitimateInterests && purpose.legitimateInterests[id]) - }) - const purposesValid = purposes.filter(p => p === true).length === TCF_PURPOSES.length - consent = vendorConsent && purposesValid + } else if (userConsent.usp) { + const usp = userConsent.usp + consent = usp[1] !== 'N' && usp[2] !== 'Y' } - } else if (userConsent.usp) { - const usp = userConsent.usp - consent = usp[1] !== 'N' && usp[2] !== 'Y' } return consent diff --git a/test/spec/modules/brandmetricsRtdProvider_spec.js b/test/spec/modules/brandmetricsRtdProvider_spec.js index 907c672208f..72a2e4b029c 100644 --- a/test/spec/modules/brandmetricsRtdProvider_spec.js +++ b/test/spec/modules/brandmetricsRtdProvider_spec.js @@ -67,6 +67,8 @@ const NO_USP_CONSENT = { usp: '1NYY' }; +const UNDEFINED_USER_CONSENT = {}; + function mockSurveyLoaded(surveyConf) { const commands = window._brandmetrics || []; commands.forEach(command => { @@ -120,6 +122,10 @@ describe('BrandmetricsRTD module', () => { it('should not init when there is no usp- consent', () => { expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, NO_USP_CONSENT)).to.equal(false); }); + + it('should init if there are no consent- objects defined', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, UNDEFINED_USER_CONSENT)).to.equal(true); + }); }); describe('getBidRequestData', () => { From 8d1c8bef46672bfdc5d8410cbe42bed99e21124c Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 9 Aug 2023 12:31:14 +0200 Subject: [PATCH 26/88] CriteoBidAdapter : Bump fast bid current version to 139 (#10333) --- modules/criteoBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 472d36655c0..5e0cdbe0b70 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -28,7 +28,7 @@ const LOG_PREFIX = 'Criteo: '; Unminified source code can be found in the privately shared repo: https://github.com/Prebid-org/prebid-js-external-js-criteo/blob/master/dist/prod.js */ const FAST_BID_VERSION_PLACEHOLDER = '%FAST_BID_VERSION%'; -export const FAST_BID_VERSION_CURRENT = 136; +export const FAST_BID_VERSION_CURRENT = 139; const FAST_BID_VERSION_LATEST = 'latest'; const FAST_BID_VERSION_NONE = 'none'; const PUBLISHER_TAG_URL_TEMPLATE = 'https://static.criteo.net/js/ld/publishertag.prebid' + FAST_BID_VERSION_PLACEHOLDER + '.js'; From 3d41276b09333ae3800cd2247c0674a38231bc76 Mon Sep 17 00:00:00 2001 From: Robert Ray Martinez III Date: Wed, 9 Aug 2023 03:52:10 -0700 Subject: [PATCH 27/88] Rubicon Bid Adapter: fix saving state of single request option (#10332) * revert PR #9050 * revert pr 9050 --- modules/rubiconBidAdapter.js | 4 ++-- test/spec/modules/rubiconBidAdapter_spec.js | 24 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 2f0dcda8411..a8842facb6a 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -27,7 +27,7 @@ const DEFAULT_PBS_INTEGRATION = 'pbjs'; const DEFAULT_RENDERER_URL = 'https://video-outstream.rubiconproject.com/apex-2.2.1.js'; // renderer code at https://github.com/rubicon-project/apex2 -let rubiConf = {}; +let rubiConf = config.getConfig('rubicon') || {}; // we are saving these as global to this module so that if a pub accidentally overwrites the entire // rubicon object, then we do not lose other data config.getConfig('rubicon', config => { @@ -322,7 +322,7 @@ export const spec = { ) ); }); - if (config.getConfig('rubicon.singleRequest') !== true) { + if (rubiConf.singleRequest !== true) { // bids are not grouped if single request mode is not enabled requests = filteredHttpRequest.concat(bannerBidRequests.map(bidRequest => { const bidParams = spec.createSlotParams(bidRequest, bidderRequest); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 562e956d814..d04b8075134 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -1229,6 +1229,30 @@ describe('the rubicon adapter', function () { }); }); + it('should still use single request if other rubicon configs are set after', function () { + // set single request to true + config.setConfig({ rubicon: { singleRequest: true } }); + + // execute some other rubicon setConfig + config.setConfig({ rubicon: { netRevenue: true } }); + + const bidCopy = utils.deepClone(bidderRequest.bids[0]); + bidderRequest.bids.push(bidCopy); + bidderRequest.bids.push(bidCopy); + bidderRequest.bids.push(bidCopy); + + let serverRequests = spec.buildRequests(bidderRequest.bids, bidderRequest); + + // should have 1 request only + expect(serverRequests).that.is.an('array').of.length(1); + + // get the built query + let data = parseQuery(serverRequests[0].data); + + // num slots should be 4 + expect(data.slots).to.equal('4'); + }); + it('should not group bid requests if singleRequest does not equal true', function () { config.setConfig({rubicon: {singleRequest: false}}); From 865a3db607b3a9f188f27c170d398d2040bbde5f Mon Sep 17 00:00:00 2001 From: onetag-dev <38786435+onetag-dev@users.noreply.github.com> Date: Wed, 9 Aug 2023 13:42:28 +0200 Subject: [PATCH 28/88] Onetag Bid Adapter: add support for FPD (ortb2 field) (#10329) * Onetag Bid Adapter: add support for FPD (ortb2 field) * Onetag Bid Adapter: add support for FPD (ortb2 field) --------- Co-authored-by: federico --- modules/onetagBidAdapter.js | 3 + test/spec/modules/onetagBidAdapter_spec.js | 69 ++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index 724a53a3095..801bb747e34 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -70,6 +70,9 @@ function buildRequests(validBidRequests, bidderRequest) { if (bidderRequest && bidderRequest.uspConsent) { payload.usPrivacy = bidderRequest.uspConsent; } + if (bidderRequest && bidderRequest.ortb2) { + payload.ortb2 = bidderRequest.ortb2; + } if (validBidRequests && validBidRequests.length !== 0 && validBidRequests[0].userIdAsEids) { payload.userId = validBidRequests[0].userIdAsEids; } diff --git a/test/spec/modules/onetagBidAdapter_spec.js b/test/spec/modules/onetagBidAdapter_spec.js index 6c9ba05bacd..df6456db82e 100644 --- a/test/spec/modules/onetagBidAdapter_spec.js +++ b/test/spec/modules/onetagBidAdapter_spec.js @@ -312,6 +312,75 @@ describe('onetag', function () { expect(payload.usPrivacy).to.exist; expect(payload.usPrivacy).to.exist.and.to.equal(consentString); }); + it('Should send FPD (ortb2 field)', function () { + const firtPartyData = { + // this is where the contextual data is placed + site: { + name: 'example', + domain: 'page.example.com', + // OpenRTB 2.5 spec / Content Taxonomy + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + page: 'https://page.example.com/here.html', + ref: 'https://ref.example.com', + keywords: 'power tools, drills', + search: 'drill', + content: { + userrating: '4', + data: [{ + name: 'www.dataprovider1.com', // who resolved the segments + ext: { + segtax: 7, // taxonomy used to encode the segments + cids: ['iris_c73g5jq96mwso4d8'] + }, + // the bare minimum are the IDs. These IDs are the ones from the new IAB Content Taxonomy v3 + segment: [ { id: '687' }, { id: '123' } ] + }] + }, + ext: { + data: { // fields that aren't part of openrtb 2.6 + pageType: 'article', + category: 'repair' + } + } + }, + // this is where the user data is placed + user: { + keywords: 'a,b', + data: [{ + name: 'dataprovider.com', + ext: { + segtax: 4 + }, + segment: [{ + id: '1' + }] + }], + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + }, + regs: { + gpp: 'abc1234', + gpp_sid: [7] + } + }; + let bidderRequest = { + 'bidderCode': 'onetag', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'ortb2': firtPartyData + } + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); + const payload = JSON.parse(serverRequest.data); + expect(payload.ortb2).to.exist; + expect(payload.ortb2).to.exist.and.to.deep.equal(firtPartyData); + }); }); describe('interpretResponse', function () { const request = getBannerVideoRequest(); From 509deaa0649824369907e6b76d0a4d045ec84df8 Mon Sep 17 00:00:00 2001 From: Viktor Dreiling <34981284+3link@users.noreply.github.com> Date: Wed, 9 Aug 2023 14:47:38 +0200 Subject: [PATCH 29/88] Add index id support (#10334) --- modules/liveIntentIdSystem.js | 17 +++++++++- test/spec/modules/eids_spec.js | 33 +++++++++++++++++++ .../modules/liveIntentIdMinimalSystem_spec.js | 5 +++ test/spec/modules/liveIntentIdSystem_spec.js | 5 +++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index 33a702aa81f..70355e9dd09 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -205,6 +205,10 @@ export const liveIntentIdSubmodule = { result.magnite = { 'id': value.magnite, ext: { provider: LI_PROVIDER_DOMAIN } } } + if (value.index) { + result.index = { 'id': value.index, ext: { provider: LI_PROVIDER_DOMAIN } } + } + return result } @@ -294,7 +298,18 @@ export const liveIntentIdSubmodule = { } } }, - + 'index': { + source: 'indexexchange.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + } } }; diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index f82defc3c45..de5ad025f69 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -271,6 +271,39 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('index', function() { + const userId = { + index: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'indexexchange.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('index with ext', function() { + const userId = { + index: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'indexexchange.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + it('liveIntentId; getValue call and NO ext', function() { const userId = { lipb: { diff --git a/test/spec/modules/liveIntentIdMinimalSystem_spec.js b/test/spec/modules/liveIntentIdMinimalSystem_spec.js index 92a15241e3a..0929a022937 100644 --- a/test/spec/modules/liveIntentIdMinimalSystem_spec.js +++ b/test/spec/modules/liveIntentIdMinimalSystem_spec.js @@ -266,6 +266,11 @@ describe('LiveIntentMinimalId', function() { expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'magnite': 'bar'}, 'magnite': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); + it('should decode an index id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', index: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'index': 'bar'}, 'index': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + it('should allow disabling nonId resolution', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index 6b7dfb82d48..4f11af57711 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -378,6 +378,11 @@ describe('LiveIntentId', function() { expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'magnite': 'bar'}, 'magnite': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); + it('should decode an index id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', index: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'index': 'bar'}, 'index': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + it('should allow disabling nonId resolution', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { From 3fba6bd54721f1456ae53071135ea65aac75ea57 Mon Sep 17 00:00:00 2001 From: Jason Quaccia Date: Wed, 9 Aug 2023 08:18:45 -0700 Subject: [PATCH 30/88] Topics Module: Support for Fetch Header Functionality (#10124) * progress * added support for topics api header fetch requests * reverted some changes and comments * refactored how topics are set in ls so that all included instead of just first indecx * reverted a few minor changes * formatting change * reverted back to previous default --- integrationExamples/topics/topics-server.js | 72 ++++++++++++++ modules/topicsFpdModule.js | 44 +++++++-- test/spec/modules/topicsFpdModule_spec.js | 103 +++++++++++++++++++- 3 files changed, 209 insertions(+), 10 deletions(-) create mode 100644 integrationExamples/topics/topics-server.js diff --git a/integrationExamples/topics/topics-server.js b/integrationExamples/topics/topics-server.js new file mode 100644 index 00000000000..0d248e5557c --- /dev/null +++ b/integrationExamples/topics/topics-server.js @@ -0,0 +1,72 @@ +// This is an example of a server-side endpoint that is utilizing the Topics API header functionality. +// Note: This test endpoint requires the following to run: node.js, npm, express, cors, body-parser + +const bodyParser = require('body-parser'); +const cors = require('cors'); +const express = require('express'); + +const port = process.env.PORT || 3000; + +const app = express(); +app.use(cors()); +app.use( + bodyParser.urlencoded({ + extended: true, + }) +); +app.use(bodyParser.json()); +app.use(express.static('public')); +app.set('port', port); + +const listener = app.listen(port, () => { + const host = + listener.address().address === '::' + ? 'http://localhost' + : 'http://' + listener.address().address; + // eslint-disable-next-line no-console + console.log( + `${__filename} is listening on ${host}:${listener.address().port}\n` + ); +}); + +app.get('*', (req, res) => { + res.setHeader('Observe-Browsing-Topics', '?1'); + + const resData = { + segment: { + domain: req.hostname, + topics: generateTopicArrayFromHeader(req.headers['sec-browsing-topics']), + bidder: req.query['bidder'], + }, + date: Date.now(), + }; + + res.json(resData); +}); + +const generateTopicArrayFromHeader = (topicString) => { + const result = []; + const topicArray = topicString.split(', '); + if (topicArray.length > 1) { + topicArray.pop(); + topicArray.map((topic) => { + const topicId = topic.split(';')[0]; + const versionsString = topic.split(';')[1].split('=')[1]; + const [config, taxonomy, model] = versionsString.split(':'); + const numTopicsWithSameVersions = topicId + .substring(1, topicId.length - 1) + .split(' '); + + numTopicsWithSameVersions.map((tpId) => { + result.push({ + topic: tpId, + version: versionsString, + configVersion: config, + taxonomyVersion: taxonomy, + modelVersion: model, + }); + }); + }); + } + return result; +}; diff --git a/modules/topicsFpdModule.js b/modules/topicsFpdModule.js index 14209d55ed3..65c5777acf3 100644 --- a/modules/topicsFpdModule.js +++ b/modules/topicsFpdModule.js @@ -12,6 +12,7 @@ import {MODULE_TYPE_BIDDER} from '../src/activities/modules.js'; const MODULE_NAME = 'topicsFpd'; const DEFAULT_EXPIRATION_DAYS = 21; +const DEFAULT_FETCH_RATE_IN_DAYS = 1; let LOAD_TOPICS_INITIALISE = false; export function reset() { @@ -85,6 +86,7 @@ export function getTopicsData(name, topics, taxonomies = TAXONOMIES) { if (name != null) { datum.name = name; } + return datum; }) ); @@ -96,6 +98,7 @@ function isTopicsSupported(doc = document) { export function getTopics(doc = document) { let topics = null; + try { if (isTopicsSupported(doc)) { topics = GreedyPromise.resolve(doc.browsingTopics()); @@ -106,6 +109,7 @@ export function getTopics(doc = document) { if (topics == null) { topics = GreedyPromise.resolve([]); } + return topics; } @@ -165,7 +169,7 @@ export function receiveMessage(evt) { let data = safeJSONParse(evt.data); if (includes(getLoadedIframeURL(), evt.origin) && data && data.segment && !isEmpty(data.segment.topics)) { const {domain, topics, bidder} = data.segment; - const iframeTopicsData = getTopicsData(domain, topics)[0]; + const iframeTopicsData = getTopicsData(domain, topics); iframeTopicsData && storeInLocalStorage(bidder, iframeTopicsData); } } catch (err) { } @@ -178,13 +182,15 @@ Function to store Topics data recieved from iframe in storage(name: "prebid:topi */ export function storeInLocalStorage(bidder, topics) { const storedSegments = new Map(safeJSONParse(coreStorage.getDataFromLocalStorage(topicStorageName))); - if (storedSegments.has(bidder)) { - storedSegments.get(bidder)[topics['ext']['segclass']] = topics; - storedSegments.get(bidder)[lastUpdated] = new Date().getTime(); - storedSegments.set(bidder, storedSegments.get(bidder)); - } else { - storedSegments.set(bidder, {[topics.ext.segclass]: topics, [lastUpdated]: new Date().getTime()}) - } + const topicsObj = { + [lastUpdated]: new Date().getTime() + }; + + topics.forEach((topic) => { + topicsObj[topic.ext.segclass] = topic; + }); + + storedSegments.set(bidder, topicsObj); coreStorage.setDataInLocalStorage(topicStorageName, JSON.stringify([...storedSegments])); } @@ -215,10 +221,11 @@ function listenMessagesFromTopicIframe() { export function loadTopicsForBidders(doc = document) { if (!isTopicsSupported(doc)) return; const topics = config.getConfig('userSync.topics') || bidderIframeList; + if (topics) { listenMessagesFromTopicIframe(); const randomBidders = getRandomBidders(topics.bidders || [], topics.maxTopicCaller || 1) - randomBidders && randomBidders.forEach(({ bidder, iframeURL }) => { + randomBidders && randomBidders.forEach(({ bidder, iframeURL, fetchUrl, fetchRate }) => { if (bidder && iframeURL) { let ifrm = doc.createElement('iframe'); ifrm.name = 'ifrm_'.concat(bidder); @@ -227,6 +234,25 @@ export function loadTopicsForBidders(doc = document) { setLoadedIframeURL(new URL(iframeURL).origin); iframeURL && doc.documentElement.appendChild(ifrm); } + + if (bidder && fetchUrl) { + let storedSegments = new Map(safeJSONParse(coreStorage.getDataFromLocalStorage(topicStorageName))); + const bidderLsEntry = storedSegments.get(bidder); + + if (!bidderLsEntry || (bidderLsEntry && isCachedDataExpired(bidderLsEntry[lastUpdated], fetchRate || DEFAULT_FETCH_RATE_IN_DAYS))) { + window.fetch(`${fetchUrl}?bidder=${bidder}`, {browsingTopics: true}) + .then(response => { + return response.json(); + }) + .then(data => { + if (data && data.segment && !isEmpty(data.segment.topics)) { + const {domain, topics, bidder} = data.segment; + const fetchTopicsData = getTopicsData(domain, topics); + fetchTopicsData && storeInLocalStorage(bidder, fetchTopicsData); + } + }); + } + } }) } else { logWarn(`Topics config not defined under userSync Object`); diff --git a/test/spec/modules/topicsFpdModule_spec.js b/test/spec/modules/topicsFpdModule_spec.js index 97af0e971c3..bc7df85db0d 100644 --- a/test/spec/modules/topicsFpdModule_spec.js +++ b/test/spec/modules/topicsFpdModule_spec.js @@ -8,9 +8,9 @@ import { reset, topicStorageName } from '../../../modules/topicsFpdModule.js'; +import {config} from 'src/config.js'; import {deepClone, safeJSONParse} from '../../../src/utils.js'; import {getCoreStorageManager} from 'src/storageManager.js'; -import {config} from 'src/config.js'; import * as activities from '../../../src/activities/rules.js'; import {ACTIVITY_ENRICH_UFPD} from '../../../src/activities/activities.js'; @@ -414,3 +414,104 @@ describe('topics', () => { }); }); }); + +describe('handles fetch request for topics api headers', () => { + let stubbedFetch; + const storage = getCoreStorageManager('topicsFpd'); + + beforeEach(() => { + stubbedFetch = sinon.stub(window, 'fetch'); + }); + + afterEach(() => { + stubbedFetch.restore(); + storage.removeDataFromLocalStorage(topicStorageName); + }); + + it('should make a fetch call when a fetchUrl is present for a selected bidder', () => { + config.setConfig({ + userSync: { + topics: { + maxTopicCaller: 3, + bidders: [ + { + bidder: 'pubmatic', + fetchUrl: 'http://localhost:3000/topics-server.js' + } + ], + }, + } + }); + + stubbedFetch.returns(Promise.resolve(true)); + loadTopicsForBidders({ + browsingTopics: true, + featurePolicy: { + allowsFeature() { return true } + } + }); + sinon.assert.calledOnce(stubbedFetch); + stubbedFetch.calledWith('http://localhost:3000/topics-server.js'); + }); + + it('should not make a fetch call when a fetchUrl is not present for a selected bidder', () => { + config.setConfig({ + userSync: { + topics: { + maxTopicCaller: 3, + bidders: [ + { + bidder: 'pubmatic' + } + ], + }, + } + }); + + loadTopicsForBidders({ + browsingTopics: true, + featurePolicy: { + allowsFeature() { return true } + } + }); + sinon.assert.notCalled(stubbedFetch); + }); + + it('a fetch request should not be made if the configured fetch rate duration has not yet passed', () => { + const storedSegments = JSON.stringify( + [['pubmatic', { + '2206021246': { + 'ext': {'segtax': 600, 'segclass': '2206021246'}, + 'segment': [{'id': '243'}, {'id': '265'}], + 'name': 'ads.pubmatic.com' + }, + 'lastUpdated': new Date().getTime() + }]] + ); + + storage.setDataInLocalStorage(topicStorageName, storedSegments); + + config.setConfig({ + userSync: { + topics: { + maxTopicCaller: 3, + bidders: [ + { + bidder: 'pubmatic', + fetchUrl: 'http://localhost:3000/topics-server.js', + fetchRate: 1 // in days. 1 fetch per day + } + ], + }, + } + }); + + loadTopicsForBidders({ + browsingTopics: true, + featurePolicy: { + allowsFeature() { return true } + } + }); + sinon.assert.notCalled(stubbedFetch); + }); +}); From c553226ee47f13e7d32c5d92f3e88e4f08d55248 Mon Sep 17 00:00:00 2001 From: adquery <89853721+adquery@users.noreply.github.com> Date: Wed, 9 Aug 2023 17:23:09 +0200 Subject: [PATCH 31/88] Adquery Bid Adapter : changes in adqueryIdSystem, fix auctionId leak (#10128) * added referrer to bid request * added referrer to bid request - tests * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 * adquery/prebid_qid_work1 --- modules/adqueryBidAdapter.js | 44 ++++++------- modules/adqueryIdSystem.js | 75 ++++++++++++++--------- test/spec/modules/adqueryIdSystem_spec.js | 37 ++++------- 3 files changed, 81 insertions(+), 75 deletions(-) diff --git a/modules/adqueryBidAdapter.js b/modules/adqueryBidAdapter.js index 8a953f0d97f..2f7832a33e9 100644 --- a/modules/adqueryBidAdapter.js +++ b/modules/adqueryBidAdapter.js @@ -1,7 +1,6 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; -import { logInfo, buildUrl, triggerPixel, parseSizesInput } from '../src/utils.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {buildUrl, logInfo, parseSizesInput, triggerPixel} from '../src/utils.js'; const ADQUERY_GVLID = 902; const ADQUERY_BIDDER_CODE = 'adquery'; @@ -11,7 +10,6 @@ const ADQUERY_USER_SYNC_DOMAIN = ADQUERY_BIDDER_DOMAIN_PROTOCOL + '://' + ADQUER const ADQUERY_DEFAULT_CURRENCY = 'PLN'; const ADQUERY_NET_REVENUE = true; const ADQUERY_TTL = 360; -const storage = getStorageManager({bidderCode: ADQUERY_BIDDER_CODE}); /** @type {BidderSpec} */ export const spec = { @@ -55,10 +53,6 @@ export const spec = { * @return {Bid[]} */ interpretResponse: (response, request) => { - logInfo(request); - logInfo(response); - - let qid = null; const res = response && response.body && response.body.data; let bidResponses = []; @@ -87,17 +81,6 @@ export const spec = { bidResponses.push(bidResponse); logInfo('bidResponses', bidResponses); - if (res && res.qid) { - if (storage.getDataFromLocalStorage('qid')) { - qid = storage.getDataFromLocalStorage('qid'); - if (qid && qid.includes('%7B%22')) { - storage.setDataInLocalStorage('qid', res.qid); - } - } else { - storage.setDataInLocalStorage('qid', res.qid); - } - } - return bidResponses; }, @@ -189,8 +172,28 @@ export const spec = { } }; + function buildRequest(validBidRequests, bidderRequest) { let bid = validBidRequests; + logInfo('buildRequest: ', bid); + + let userId = null; + if (window.qid) { + userId = window.qid; + } + + if (bid.userId && bid.userId.qid) { + userId = bid.userId.qid + } + + if (!userId) { + // onetime User ID + const randomValues = Array.from(window.crypto.getRandomValues(new Uint32Array(4))); + userId = randomValues.map(it => it.toString(36)).join().substring(20); + + window.qid = userId; + } + let pageUrl = ''; if (bidderRequest && bidderRequest.refererInfo) { pageUrl = bidderRequest.refererInfo.page || ''; @@ -199,11 +202,10 @@ function buildRequest(validBidRequests, bidderRequest) { return { v: '$prebid.version$', placementCode: bid.params.placementId, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: bid.auctionId, + auctionId: null, type: bid.params.type, adUnitCode: bid.adUnitCode, - bidQid: storage.getDataFromLocalStorage('qid') || null, + bidQid: userId, bidId: bid.bidId, bidder: bid.bidder, bidPageUrl: pageUrl, diff --git a/modules/adqueryIdSystem.js b/modules/adqueryIdSystem.js index 82df787a2b4..d6d609b66e4 100644 --- a/modules/adqueryIdSystem.js +++ b/modules/adqueryIdSystem.js @@ -8,7 +8,7 @@ import {ajax} from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; -import { isFn, isStr, isPlainObject, logError } from '../src/utils.js'; +import {isFn, isPlainObject, isStr, logError, logInfo} from '../src/utils.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'qid'; @@ -51,11 +51,7 @@ export const adqueryIdSubmodule = { * @returns {{qid:Object}} */ decode(value) { - let qid = storage.getDataFromLocalStorage('qid'); - if (isStr(qid)) { - return {qid: qid}; - } - return (value && typeof value['qid'] === 'string') ? { 'qid': value['qid'] } : undefined; + return {qid: value} }, /** * performs action to obtain id and return a value in the callback's response argument @@ -64,38 +60,57 @@ export const adqueryIdSubmodule = { * @returns {IdResponse|undefined} */ getId(config) { + logInfo('adqueryIdSubmodule getId'); if (!isPlainObject(config.params)) { config.params = {}; } - const url = paramOrDefault(config.params.url, + + const url = paramOrDefault( + config.params.url, `https://bidder.adquery.io/prebid/qid`, - config.params.urlArg); + config.params.urlArg + ); const resp = function (callback) { - let qid = storage.getDataFromLocalStorage('qid'); - if (isStr(qid)) { - const responseObj = {qid: qid}; - callback(responseObj); - } else { - const callbacks = { - success: response => { - let responseObj; - if (response) { - try { - responseObj = JSON.parse(response); - } catch (error) { - logError(error); - } + let qid = window.qid; + + if (!qid) { + const ramdomValues = window.crypto.getRandomValues(new Uint32Array(4)); + qid = (ramdomValues[0].toString(36) + + ramdomValues[1].toString(36) + + ramdomValues[2].toString(36) + + ramdomValues[3].toString(36)) + .substring(0, 20); + + const randomValues = Array.from(window.crypto.getRandomValues(new Uint32Array(4))); + qid = randomValues.map(it => it.toString(36)).join().substring(20); + logInfo('adqueryIdSubmodule ID QID GENERTAED:', qid); + } + logInfo('adqueryIdSubmodule ID QID:', qid); + + const callbacks = { + success: response => { + let responseObj; + if (response) { + try { + responseObj = JSON.parse(response); + } catch (error) { + logError(error); } - callback(responseObj); - }, - error: error => { - logError(`${MODULE_NAME}: ID fetch encountered an error`, error); - callback(); } - }; - ajax(url, callbacks, undefined, {method: 'GET'}); - } + if (responseObj.qid) { + let myQid = responseObj.qid; + storage.setDataInLocalStorage('qid', myQid); + return callback(myQid); + } + callback(); + }, + error: error => { + logError(`${MODULE_NAME}: ID fetch encountered an error`, error); + callback(); + } + }; + ajax(url + '?qid=' + qid, callbacks, undefined, {method: 'GET'}); }; return {callback: resp}; }, diff --git a/test/spec/modules/adqueryIdSystem_spec.js b/test/spec/modules/adqueryIdSystem_spec.js index a6b4e9d1529..d6abddf3adb 100644 --- a/test/spec/modules/adqueryIdSystem_spec.js +++ b/test/spec/modules/adqueryIdSystem_spec.js @@ -1,5 +1,6 @@ -import { adqueryIdSubmodule, storage } from 'modules/adqueryIdSystem.js'; -import { server } from 'test/mocks/xhr.js'; +import {adqueryIdSubmodule, storage} from 'modules/adqueryIdSystem.js'; +import {server} from 'test/mocks/xhr.js'; +import sinon from 'sinon'; const config = { storage: { @@ -18,7 +19,7 @@ describe('AdqueryIdSystem', function () { }); }); - describe('getId', function() { + describe('getId', function () { let getDataFromLocalStorageStub; beforeEach(function() { @@ -29,7 +30,7 @@ describe('AdqueryIdSystem', function () { getDataFromLocalStorageStub.restore(); }); - it('gets a adqueryId', function() { + it('gets a adqueryId', function () { const config = { params: {} }; @@ -37,36 +38,24 @@ describe('AdqueryIdSystem', function () { const callback = adqueryIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; - expect(request.url).to.eq(`https://bidder.adquery.io/prebid/qid`); - request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ qid: 'qid' })); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({qid: 'qid'}); + expect(request.url).to.contains(`https://bidder.adquery.io/prebid/qid?qid=`); + request.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({qid: '6dd9eab7dfeab7df6dd9ea'})); + expect(callbackSpy.lastCall.lastArg).to.deep.equal('6dd9eab7dfeab7df6dd9ea'); }); - it('gets a cached adqueryId', function() { - const config = { - params: {} - }; - getDataFromLocalStorageStub.withArgs('qid').returns('qid'); - - const callbackSpy = sinon.spy(); - const callback = adqueryIdSubmodule.getId(config).callback; - callback(callbackSpy); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({qid: 'qid'}); - }); - - it('allows configurable id url', function() { + it('allows configurable id url', function () { const config = { params: { - url: 'https://bidder.adquery.io' + url: 'https://another_bidder.adquery.io/qid' } }; const callbackSpy = sinon.spy(); const callback = adqueryIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; - expect(request.url).to.eq('https://bidder.adquery.io'); - request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ qid: 'testqid' })); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({qid: 'testqid'}); + expect(request.url).to.contains('https://another_bidder.adquery.io'); + request.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({qid: 'testqid'})); + expect(callbackSpy.lastCall.lastArg).to.deep.equal('testqid'); }); }); }); From 543544cc582c2843730ade62f04e7e4cc56ab513 Mon Sep 17 00:00:00 2001 From: Vladimir Fedoseev Date: Wed, 9 Aug 2023 17:30:30 +0200 Subject: [PATCH 32/88] VIS.X: add first-party id (#10162) --- modules/visxBidAdapter.js | 52 +++++++- test/spec/modules/visxBidAdapter_spec.js | 152 ++++++++++++++++++++++- 2 files changed, 202 insertions(+), 2 deletions(-) diff --git a/modules/visxBidAdapter.js b/modules/visxBidAdapter.js index a066d93d69e..c1bb626a39c 100644 --- a/modules/visxBidAdapter.js +++ b/modules/visxBidAdapter.js @@ -3,6 +3,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { INSTREAM as VIDEO_INSTREAM } from '../src/video.js'; +import {getStorageManager} from '../src/storageManager.js'; const BIDDER_CODE = 'visx'; const GVLID = 154; const BASE_URL = 'https://t.visx.net'; @@ -29,6 +30,7 @@ const LOG_ERROR_MESS = { videoMissing: 'Bid request videoType property is missing - ' }; const currencyWhiteList = ['EUR', 'USD', 'GBP', 'PLN']; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -117,10 +119,13 @@ export const spec = { ...(payloadSchain && { schain: payloadSchain }) } }; + + const vads = _getUserId(); const user = { ext: { ...(payloadUserEids && { eids: payloadUserEids }), - ...(payload.gdpr_consent && { consent: payload.gdpr_consent }) + ...(payload.gdpr_consent && { consent: payload.gdpr_consent }), + ...(vads && { vads }) } }; const regs = ('gdpr_applies' in payload) && { @@ -382,4 +387,49 @@ function _isAdSlotExists(adUnitCode) { return false; } +// Generate user id (25 chars) with NanoID +// https://github.com/ai/nanoid/ +function _generateUserId() { + for ( + var t = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict', + e = new Date().getTime() % 1073741824, + i = '', + o = 0; + o < 5; + o++ + ) { + i += t[e % 64]; + e = Math.floor(e / 64); + } + for (o = 20; o--;) i += t[(64 * Math.random()) | 0]; + return i; +} + +function _getUserId() { + const USER_ID_KEY = '__vads'; + let vads; + + if (storage.cookiesAreEnabled()) { + vads = storage.getCookie(USER_ID_KEY); + } else if (storage.localStorageIsEnabled()) { + vads = storage.getDataFromLocalStorage(USER_ID_KEY); + } + + if (vads && vads.length) { + return vads; + } + + vads = _generateUserId(); + if (storage.cookiesAreEnabled()) { + const expires = new Date(Date.now() + 2592e6).toUTCString(); + storage.setCookie(USER_ID_KEY, vads, expires); + return vads; + } else if (storage.localStorageIsEnabled()) { + storage.setDataInLocalStorage(USER_ID_KEY, vads); + return vads; + } + + return null; +} + registerBidder(spec); diff --git a/test/spec/modules/visxBidAdapter_spec.js b/test/spec/modules/visxBidAdapter_spec.js index 9a486cd6c34..139349ceead 100755 --- a/test/spec/modules/visxBidAdapter_spec.js +++ b/test/spec/modules/visxBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec } from 'modules/visxBidAdapter.js'; +import { spec, storage } from 'modules/visxBidAdapter.js'; import { config } from 'src/config.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import * as utils from 'src/utils.js'; @@ -82,6 +82,9 @@ describe('VisxAdapter', function () { }); return res; } + + let cookiesAreEnabledStub, localStorageIsEnabledStub; + const bidderRequest = { timeout: 3000, refererInfo: { @@ -180,6 +183,24 @@ describe('VisxAdapter', function () { 'ext': {'bidder': {'uid': 903537}} }]; + before(() => { + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: false + } + }; + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub.returns(false); + cookiesAreEnabledStub.returns(false); + }); + + after(() => { + localStorageIsEnabledStub.restore(); + cookiesAreEnabledStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should attach valid params to the tag', function () { const firstBid = bidRequests[0]; const bids = [firstBid]; @@ -422,6 +443,7 @@ describe('VisxAdapter', function () { }); return res; } + let cookiesAreEnabledStub, localStorageIsEnabledStub; const bidderRequest = { timeout: 3000, refererInfo: { @@ -449,6 +471,24 @@ describe('VisxAdapter', function () { } ]; + before(() => { + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: false + } + }; + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub.returns(false); + cookiesAreEnabledStub.returns(false); + }); + + after(() => { + localStorageIsEnabledStub.restore(); + cookiesAreEnabledStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should send requst for banner bid', function () { const request = spec.buildRequests([bidRequests[0]], bidderRequest); const payload = parseRequest(request.url); @@ -486,6 +526,7 @@ describe('VisxAdapter', function () { }); return res; } + let cookiesAreEnabledStub, localStorageIsEnabledStub; const bidderRequest = { timeout: 3000, refererInfo: { @@ -529,10 +570,23 @@ describe('VisxAdapter', function () { documentStub.withArgs('visx-adunit-element-2').returns({ id: 'visx-adunit-element-2' }); + + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: false + } + }; + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub.returns(false); + cookiesAreEnabledStub.returns(false); }); after(function() { sandbox.restore(); + localStorageIsEnabledStub.restore(); + cookiesAreEnabledStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; }); it('should find ad slot by ad unit code as element id', function () { @@ -1323,4 +1377,100 @@ describe('VisxAdapter', function () { expect(query).to.deep.equal({}); }); }); + + describe('first party user id', function () { + const USER_ID_KEY = '__vads'; + const USER_ID_DUMMY_VALUE_COOKIE = 'dummy_id_cookie'; + const USER_ID_DUMMY_VALUE_LOCAL_STORAGE = 'dummy_id_local_storage'; + + let getDataFromLocalStorageStub, localStorageIsEnabledStub; + let getCookieStub, cookiesAreEnabledStub; + + const bidRequests = [ + { + 'bidder': 'visx', + 'params': { + 'uid': 903535 + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + const bidderRequest = { + timeout: 3000, + refererInfo: { + page: 'https://example.com' + } + }; + + beforeEach(() => { + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: true + } + }; + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + }); + + afterEach(() => { + cookiesAreEnabledStub.restore(); + localStorageIsEnabledStub.restore(); + getCookieStub && getCookieStub.restore(); + getDataFromLocalStorageStub && getDataFromLocalStorageStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + + it('should not pass user id if both cookies and local storage are not available', function () { + cookiesAreEnabledStub.returns(false); + localStorageIsEnabledStub.returns(false); + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.user).to.be.undefined; + }); + + it('should get user id from cookie if available', function () { + cookiesAreEnabledStub.returns(true); + localStorageIsEnabledStub.returns(false); + getCookieStub = sinon.stub(storage, 'getCookie'); + getCookieStub.withArgs(USER_ID_KEY).returns(USER_ID_DUMMY_VALUE_COOKIE); + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.user.ext.vads).to.equal(USER_ID_DUMMY_VALUE_COOKIE); + }); + + it('should get user id from local storage if available', function () { + cookiesAreEnabledStub.returns(false); + localStorageIsEnabledStub.returns(true); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + getDataFromLocalStorageStub.withArgs(USER_ID_KEY).returns(USER_ID_DUMMY_VALUE_LOCAL_STORAGE); + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.user.ext.vads).to.equal(USER_ID_DUMMY_VALUE_LOCAL_STORAGE); + }); + + it('should create user id and store it in cookies (if user id does not exist)', function () { + cookiesAreEnabledStub.returns(true); + localStorageIsEnabledStub.returns(false); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(storage.getCookie(USER_ID_KEY)).to.be.a('string'); + expect(request.data.user.ext.vads).to.be.a('string'); + }); + + it('should create user id and store it in local storage (if user id does not exist)', function () { + cookiesAreEnabledStub.returns(false); + localStorageIsEnabledStub.returns(true); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(storage.getDataFromLocalStorage(USER_ID_KEY)).to.be.a('string'); + expect(request.data.user.ext.vads).to.be.a('string'); + }); + }); }); From 4e0a780be1a877456d3449a34ee076b210828617 Mon Sep 17 00:00:00 2001 From: JulieLorin Date: Wed, 9 Aug 2023 17:51:50 +0200 Subject: [PATCH 33/88] Prebid core: fix ortb native to legacy to use imptrackers (#10284) --- src/native.js | 4 ++-- test/spec/native_spec.js | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/native.js b/src/native.js index 927423c8d72..66d1fd4becd 100644 --- a/src/native.js +++ b/src/native.js @@ -780,8 +780,8 @@ export function toLegacyResponse(ortbResponse, ortbRequest) { legacyResponse.impressionTrackers = []; let jsTrackers = []; - if (ortbRequest?.imptrackers) { - legacyResponse.impressionTrackers.push(...ortbRequest.imptrackers); + if (ortbResponse.imptrackers) { + legacyResponse.impressionTrackers.push(...ortbResponse.imptrackers); } for (const eventTracker of ortbResponse?.eventtrackers || []) { if (eventTracker.event === TRACKER_EVENTS.impression && eventTracker.method === TRACKER_METHODS.img) { diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js index dee177d4b9b..a1b546e00f4 100644 --- a/test/spec/native_spec.js +++ b/test/spec/native_spec.js @@ -631,7 +631,8 @@ describe('native.js', function () { eventtrackers: [ { event: 1, method: 1, url: 'https://sampleurl.com' }, { event: 1, method: 2, url: 'https://sampleurljs.com' } - ] + ], + imptrackers: [ 'https://sample-imp.com' ] } describe('toLegacyResponse', () => { it('returns assets in legacy format for ortb responses', () => { @@ -640,8 +641,9 @@ describe('native.js', function () { expect(actual.title).to.equal('vtitle'); expect(actual.clickUrl).to.equal('url'); expect(actual.javascriptTrackers).to.equal(''); - expect(actual.impressionTrackers.length).to.equal(1); - expect(actual.impressionTrackers[0]).to.equal('https://sampleurl.com'); + expect(actual.impressionTrackers.length).to.equal(2); + expect(actual.impressionTrackers).to.contain('https://sampleurl.com'); + expect(actual.impressionTrackers).to.contain('https://sample-imp.com'); }); }); }); From b534649b8d21384c636a011940b905d0a7a87a09 Mon Sep 17 00:00:00 2001 From: GeoEdge-r-and-d <72186958+GeoEdge-r-and-d@users.noreply.github.com> Date: Wed, 9 Aug 2023 18:56:38 +0300 Subject: [PATCH 34/88] Geoedge RTD module: support monitoring all GPT ad slots (#10291) * Added params.gpt config, to enable monitoring all GPT ad slots * Update tests * Update docs * Add geoedge to external js list * Fix test --------- Co-authored-by: daniel manan --- modules/geoedgeRtdProvider.js | 56 ++++++++++++---- modules/geoedgeRtdProvider.md | 3 +- src/adloader.js | 1 + test/spec/modules/geoedgeRtdProvider_spec.js | 68 +++++++++++++------- 4 files changed, 90 insertions(+), 38 deletions(-) diff --git a/modules/geoedgeRtdProvider.js b/modules/geoedgeRtdProvider.js index 6f910632fbc..fdd7aa2f5eb 100644 --- a/modules/geoedgeRtdProvider.js +++ b/modules/geoedgeRtdProvider.js @@ -20,6 +20,8 @@ import { ajax } from '../src/ajax.js'; import { generateUUID, insertElement, isEmpty, logError } from '../src/utils.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; +import { loadExternalScript } from '../src/adloader.js'; +import { auctionManager } from '../src/auctionManager.js'; /** @type {string} */ const SUBMODULE_NAME = 'geoedge'; @@ -33,9 +35,13 @@ const PV_ID = generateUUID(); /** @type {string} */ const HOST_NAME = 'https://rumcdn.geoedge.be'; /** @type {string} */ -const FILE_NAME = 'grumi.js'; +const FILE_NAME_CLIENT = 'grumi.js'; +/** @type {string} */ +const FILE_NAME_INPAGE = 'grumi-ip.js'; +/** @type {function} */ +export let getClientUrl = (key) => `${HOST_NAME}/${key}/${FILE_NAME_CLIENT}`; /** @type {function} */ -export let getClientUrl = (key) => `${HOST_NAME}/${key}/${FILE_NAME}`; +export let getInPageUrl = (key) => `${HOST_NAME}/${key}/${FILE_NAME_INPAGE}`; /** @type {string} */ export let wrapper /** @type {boolean} */; @@ -177,7 +183,8 @@ function isSupportedBidder(bidder, paramsBidders) { function shouldWrap(bid, params) { let supportedBidder = isSupportedBidder(bid.bidderCode, params.bidders); let donePreload = params.wap ? preloaded : true; - return wrapperReady && supportedBidder && donePreload; + let isGPT = params.gpt; + return wrapperReady && supportedBidder && donePreload && !isGPT; } function conditionallyWrap(bidResponse, config, userConsent) { @@ -187,31 +194,55 @@ function conditionallyWrap(bidResponse, config, userConsent) { } } +function isBillingMessage(data, params) { + return data.key === params.key && data.impression; +} + /** - * Fire billable events for applicable bids + * Fire billable events when our client sends a message + * Messages will be sent only when: + * a. applicable bids are wrapped + * b. our code laoded and executed sucesfully */ function fireBillableEventsForApplicableBids(params) { - events.on(CONSTANTS.EVENTS.BID_WON, function (winningBid) { - if (shouldWrap(winningBid, params)) { + window.addEventListener('message', function (message) { + let data = message.data; + if (isBillingMessage(data, params)) { + let winningBid = auctionManager.findBidByAdId(data.adId); events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { vendor: SUBMODULE_NAME, - billingId: generateUUID(), - type: 'impression', - transactionId: winningBid.transactionId, - auctionId: winningBid.auctionId, - bidId: winningBid.requestId + billingId: data.impressionId, + type: winningBid ? 'impression' : data.type, + transactionId: winningBid?.transactionId || data.transactionId, + auctionId: winningBid?.auctionId || data.auctionId, + bidId: winningBid?.requestId || data.requestId }); } }); } +/** + * Loads Geoedge in page script that monitors all ad slots created by GPT + * @param {Object} params + */ +function setupInPage(params) { + window.grumi = params; + window.grumi.fromPrebid = true; + loadExternalScript(getInPageUrl(params.key), SUBMODULE_NAME); +} + function init(config, userConsent) { let params = config.params; if (!params || !params.key) { logError('missing key for geoedge RTD module provider'); return false; } - preloadClient(params.key); + if (params.gpt) { + setupInPage(params); + } else { + fetchWrapper(setWrapper); + preloadClient(params.key); + } fireBillableEventsForApplicableBids(params); return true; } @@ -228,7 +259,6 @@ export const geoedgeSubmodule = { }; export function beforeInit() { - fetchWrapper(setWrapper); submodule('realTimeData', geoedgeSubmodule); } diff --git a/modules/geoedgeRtdProvider.md b/modules/geoedgeRtdProvider.md index 5414606612c..cdf913b8893 100644 --- a/modules/geoedgeRtdProvider.md +++ b/modules/geoedgeRtdProvider.md @@ -5,7 +5,7 @@ Module Type: Rtd Provider Maintainer: guy.books@geoedge.com The Geoedge Realtime module lets publishers block bad ads such as automatic redirects, malware, offensive creatives and landing pages. -To use this module, you'll need to work with [Geoedge](https://www.geoedge.com/publishers-real-time-protection/) to get an account and cutomer key. +To use this module, you'll need to work with [Geoedge](https://www.geoedge.com/publishers-real-time-protection/) to get an account and customer key. ## Integration @@ -49,6 +49,7 @@ Parameters details: |params.key | String | Customer key |Required, contact Geoedge to get your key | |params.bidders | Object | Bidders to monitor |Optional, list of bidder to include / exclude from monitoring. Omitting this will monitor bids from all bidders. | |params.wap |Boolean |Wrap after preload |Optional, defaults to `false`. Set to `true` if you want to monitor only after the module has preloaded the monitoring client. | +|params.gpt |Boolean |Wrap all GPT ad slots |Optional, defaults to `false`. Set to `true` if you want to monitor all Google Publisher Tag ad slots, regaedless if the winning bid comes from Prebid or Google Ad Manager (Direct, Adx, Adesnse, Open Bidding, etc). | ## Example diff --git a/src/adloader.js b/src/adloader.js index fb4aa44e872..a87b930b7df 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -26,6 +26,7 @@ const _approvedLoadExternalJSList = [ 'airgrid', 'clean.io', 'a1Media', + 'geoedge', ] /** diff --git a/test/spec/modules/geoedgeRtdProvider_spec.js b/test/spec/modules/geoedgeRtdProvider_spec.js index eec1feff87a..2f2fc8e2775 100644 --- a/test/spec/modules/geoedgeRtdProvider_spec.js +++ b/test/spec/modules/geoedgeRtdProvider_spec.js @@ -1,12 +1,13 @@ import * as utils from '../../../src/utils.js'; +import { loadExternalScript } from '../../../src/adloader.js'; import * as hook from '../../../src/hook.js' -import { beforeInit, geoedgeSubmodule, setWrapper, wrapper, htmlPlaceholder, WRAPPER_URL, getClientUrl } from '../../../modules/geoedgeRtdProvider.js'; +import { beforeInit, geoedgeSubmodule, setWrapper, wrapper, htmlPlaceholder, WRAPPER_URL, getClientUrl, getInPageUrl } from '../../../modules/geoedgeRtdProvider.js'; import { server } from '../../../test/mocks/xhr.js'; import * as events from '../../../src/events.js'; import CONSTANTS from '../../../src/constants.json'; let key = '123123123'; -function makeConfig() { +function makeConfig(gpt) { return { name: 'geoedge', params: { @@ -15,7 +16,8 @@ function makeConfig() { bidders: { bidderA: true, bidderB: false - } + }, + gpt: gpt } }; } @@ -23,6 +25,7 @@ function makeConfig() { function mockBid(bidderCode) { return { 'ad': '', + 'adId': '1234', 'cpm': '1.00', 'width': 300, 'height': 250, @@ -35,6 +38,15 @@ function mockBid(bidderCode) { }; } +function mockMessageFromClient(key) { + return { + key, + impression: true, + adId: 1234, + type: 'impression' + }; +} + let mockWrapper = `${htmlPlaceholder}`; describe('Geoedge RTD module', function () { @@ -47,22 +59,11 @@ describe('Geoedge RTD module', function () { after(function () { submoduleStub.restore(); }); - it('should fetch the wrapper', function () { - beforeInit(); - let request = server.requests[0]; - let isWrapperRequest = request && request.url && request.url && request.url === WRAPPER_URL; - expect(isWrapperRequest).to.equal(true); - }); it('should register RTD submodule provider', function () { + beforeInit(); expect(submoduleStub.calledWith('realTimeData', geoedgeSubmodule)).to.equal(true); }); }); - describe('setWrapper', function () { - it('should set the wrapper', function () { - setWrapper(mockWrapper); - expect(wrapper).to.equal(mockWrapper); - }); - }); describe('submodule', function () { describe('name', function () { it('should be geoedge', function () { @@ -84,35 +85,54 @@ describe('Geoedge RTD module', function () { expect(missingParams || missingKey).to.equal(false); }); it('should return true when params are ok', function () { - expect(geoedgeSubmodule.init(makeConfig())).to.equal(true); + expect(geoedgeSubmodule.init(makeConfig(false))).to.equal(true); + }); + it('should fetch the wrapper', function () { + geoedgeSubmodule.init(makeConfig(false)); + let request = server.requests[0]; + let isWrapperRequest = request && request.url && request.url && request.url === WRAPPER_URL; + expect(isWrapperRequest).to.equal(true); }); it('should preload the client', function () { let isLinkPreloadAsScript = arg => arg.tagName === 'LINK' && arg.rel === 'preload' && arg.as === 'script' && arg.href === getClientUrl(key); expect(insertElementStub.calledWith(sinon.match(isLinkPreloadAsScript))).to.equal(true); }); - it('should emit billable events with applicable winning bids', function () { - let applicableBid = mockBid('bidderA'); - let nonApplicableBid = mockBid('bidderB'); + it('should emit billable events with applicable winning bids', function (done) { let counter = 0; events.on(CONSTANTS.EVENTS.BILLABLE_EVENT, function (event) { if (event.vendor === 'geoedge' && event.type === 'impression') { counter += 1; } + expect(counter).to.equal(1); + done(); }); - events.emit(CONSTANTS.EVENTS.BID_WON, applicableBid); - events.emit(CONSTANTS.EVENTS.BID_WON, nonApplicableBid); - expect(counter).to.equal(1); + window.postMessage(mockMessageFromClient(key), '*'); + }); + it('should load the in page code when gpt params is true', function () { + geoedgeSubmodule.init(makeConfig(true)); + let isInPageUrl = arg => arg == getInPageUrl(key); + expect(loadExternalScript.calledWith(sinon.match(isInPageUrl))).to.equal(true); + }); + it('should set the window.grumi config object when gpt params is true', function () { + let hasGrumiObj = typeof window.grumi === 'object'; + expect(hasGrumiObj && window.grumi.key === key && window.grumi.fromPrebid).to.equal(true); + }); + }); + describe('setWrapper', function () { + it('should set the wrapper', function () { + setWrapper(mockWrapper); + expect(wrapper).to.equal(mockWrapper); }); }); describe('onBidResponseEvent', function () { let bidFromA = mockBid('bidderA'); it('should wrap bid html when bidder is configured', function () { - geoedgeSubmodule.onBidResponseEvent(bidFromA, makeConfig()); + geoedgeSubmodule.onBidResponseEvent(bidFromA, makeConfig(false)); expect(bidFromA.ad.indexOf('')).to.equal(0); }); it('should not wrap bid html when bidder is not configured', function () { let bidFromB = mockBid('bidderB'); - geoedgeSubmodule.onBidResponseEvent(bidFromB, makeConfig()); + geoedgeSubmodule.onBidResponseEvent(bidFromB, makeConfig(false)); expect(bidFromB.ad.indexOf('')).to.equal(-1); }); it('should only muatate the bid ad porperty', function () { From b84b6b6287d7a16f8da1140e1c7ba7ac1cd686a6 Mon Sep 17 00:00:00 2001 From: Fatih Kaya Date: Wed, 9 Aug 2023 18:57:47 +0300 Subject: [PATCH 35/88] AdMatic Bid Adapter: added ortb2Imp params (#10292) * Admatic Bidder Adaptor * Update admaticBidAdapter.md * Update admaticBidAdapter.md * remove floor parameter * Update admaticBidAdapter.js * Admatic Bid Adapter: alias and bid floor features activated * Admatic adapter: host param control changed * Alias name changed. * Revert "Admatic adapter: host param control changed" This reverts commit de7ac85981b1ba3ad8c5d1dc95c5dadbdf5b9895. * added alias feature and host param * Revert "added alias feature and host param" This reverts commit 6ec8f4539ea6be403a0d7e08dad5c7a5228f28a1. * Revert "Alias name changed." This reverts commit 661c54f9b2397e8f25c257144d73161e13466281. * Revert "Admatic Bid Adapter: alias and bid floor features activated" This reverts commit 7a2e0e29c49e2f876b68aafe886b336fe2fe6fcb. * Revert "Update admaticBidAdapter.js" This reverts commit 7a845b7151bbb08addfb58ea9bd5b44167cc8a4e. * Revert "remove floor parameter" This reverts commit 7a23b055ccd4ea23d23e73248e82b21bc6f69d90. * Admatic adapter: host param control && Add new Bidder * Revert "Admatic adapter: host param control && Add new Bidder" This reverts commit 3c797b120c8e0fe2b851381300ac5c4b1f92c6e2. * commit new features * Update admaticBidAdapter.js * updated for coverage * sync updated * Update adloader.js * AdMatic Bidder: development of user sync url * Update admaticBidAdapter.js * Set currency for AdserverCurrency: bug fix * Update admaticBidAdapter.js * update * admatic adapter video params update * Update admaticBidAdapter.js * update * Update admaticBidAdapter.js * update * update * Update admaticBidAdapter_spec.js * Update admaticBidAdapter.js * Update admaticBidAdapter.js * Revert "Update admaticBidAdapter.js" This reverts commit 1216892fe55e5ab24dda8e045ea007ee6bb40ff8. * Revert "Update admaticBidAdapter.js" This reverts commit b1929ece33bb4040a3bcd6b9332b50335356829c. * Revert "Update admaticBidAdapter_spec.js" This reverts commit 1ca659798b0c9b912634b1673e15e54e547b81e7. * Revert "update" This reverts commit 689ce9d21e08c27be49adb35c5fd5205aef5c35c. * Revert "update" This reverts commit f381a453f9389bebd58dcfa719e9ec17f939f338. * Revert "Update admaticBidAdapter.js" This reverts commit 38fd7abec701d8a4750f9e95eaeb40fb67e9f0e6. * Revert "update" This reverts commit a5316e74b612a5b2cd16cf42586334321fc87770. * Revert "Update admaticBidAdapter.js" This reverts commit 60a28cae302b711366dab0bff9f49b11862fb8ee. * Revert "admatic adapter video params update" This reverts commit 31e69e88fd9355e143f736754ac2e47fe49b65b6. * update * Update admaticBidAdapter.js * Update admaticBidAdapter_spec.js --- modules/admaticBidAdapter.js | 5 +++++ test/spec/modules/admaticBidAdapter_spec.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/modules/admaticBidAdapter.js b/modules/admaticBidAdapter.js index 027f924ac5d..436f918a0f6 100644 --- a/modules/admaticBidAdapter.js +++ b/modules/admaticBidAdapter.js @@ -195,6 +195,11 @@ function buildRequestObject(bid) { reqObj.type = 'video'; reqObj.mediatype = bid.mediaTypes.video; } + + if (deepAccess(bid, 'ortb2Imp.ext')) { + reqObj.ext = bid.ortb2Imp.ext; + } + reqObj.id = getBidIdParameter('bidId', bid); enrichSlotWithFloors(reqObj, bid); diff --git a/test/spec/modules/admaticBidAdapter_spec.js b/test/spec/modules/admaticBidAdapter_spec.js index 1d2fb1e79cb..8c9969e4d46 100644 --- a/test/spec/modules/admaticBidAdapter_spec.js +++ b/test/spec/modules/admaticBidAdapter_spec.js @@ -28,6 +28,7 @@ describe('admaticBidAdapter', () => { 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', 'creativeId': 'er2ee', + 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] } }; @@ -52,6 +53,7 @@ describe('admaticBidAdapter', () => { 'networkId': 10433394, 'host': 'layer.serve.admatic.com.tr' }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] }, 'mediaTypes': { 'banner': { @@ -162,6 +164,7 @@ describe('admaticBidAdapter', () => { 'networkId': 10433394, 'host': 'layer.serve.admatic.com.tr' }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] }, 'mediaTypes': { 'banner': { @@ -284,6 +287,7 @@ describe('admaticBidAdapter', () => { 'sizes': [[300, 250], [728, 90]] } }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] }, getFloor: inputParams => { if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { @@ -308,6 +312,7 @@ describe('admaticBidAdapter', () => { 'networkId': 10433394, 'host': 'layer.serve.admatic.com.tr' }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] }, 'adUnitCode': 'adunit-code', 'sizes': [[300, 250], [728, 90]], From f2fbcbe77379bbfab1fc05fffecb2fa074e85ed3 Mon Sep 17 00:00:00 2001 From: matthieularere-msq <63732822+matthieularere-msq@users.noreply.github.com> Date: Wed, 9 Aug 2023 18:04:59 +0200 Subject: [PATCH 36/88] Oxxion Analytics Adapter : support new attributes (#10304) * support meta.demandSource * add support for extra context information --- modules/oxxionAnalyticsAdapter.js | 27 +++++++++++++++---- .../modules/oxxionAnalyticsAdapter_spec.js | 7 ++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/modules/oxxionAnalyticsAdapter.js b/modules/oxxionAnalyticsAdapter.js index 737d130ac6c..cc69443d8bf 100644 --- a/modules/oxxionAnalyticsAdapter.js +++ b/modules/oxxionAnalyticsAdapter.js @@ -21,6 +21,7 @@ let saveEvents = {} let allEvents = {} let auctionEnd = {} let initOptions = {} +let mode = {}; let endpoint = 'https://default' let requestsAttributes = ['adUnitCode', 'auctionId', 'bidder', 'bidderCode', 'bidId', 'cpm', 'creativeId', 'currency', 'width', 'height', 'mediaType', 'netRevenue', 'originalCpm', 'originalCurrency', 'requestId', 'size', 'source', 'status', 'timeToRespond', 'transactionId', 'ttl', 'sizes', 'mediaTypes', 'src', 'params', 'userId', 'labelAny', 'bids', 'adId']; @@ -41,16 +42,27 @@ function filterAttributes(arg, removead) { } if (typeof arg['gdprConsent'] != 'undefined') { response['gdprConsent'] = {}; - if (typeof arg['gdprConsent']['consentString'] != 'undefined') { response['gdprConsent']['consentString'] = arg['gdprConsent']['consentString']; } + if (typeof arg['gdprConsent']['consentString'] != 'undefined') { + response['gdprConsent']['consentString'] = arg['gdprConsent']['consentString']; + } } - if (typeof arg['meta'] == 'object' && typeof arg['meta']['advertiserDomains'] != 'undefined') { - response['meta'] = {'advertiserDomains': arg['meta']['advertiserDomains']}; + if (typeof arg['meta'] == 'object') { + response['meta'] = {}; + if (typeof arg['meta']['advertiserDomains'] != 'undefined') { + response['meta']['advertiserDomains'] = arg['meta']['advertiserDomains']; + } + if (typeof arg['meta']['demandSource'] == 'string') { + response['meta']['demandSource'] = arg['meta']['demandSource']; + } } requestsAttributes.forEach((attr) => { if (typeof arg[attr] != 'undefined') { response[attr] = arg[attr]; } }); - if (typeof response['creativeId'] == 'number') { response['creativeId'] = response['creativeId'].toString(); } + if (typeof response['creativeId'] == 'number') { + response['creativeId'] = response['creativeId'].toString(); + } } + response['oxxionMode'] = mode; return response; } @@ -229,7 +241,12 @@ oxxionAnalytics.originEnableAnalytics = oxxionAnalytics.enableAnalytics; oxxionAnalytics.enableAnalytics = function (config) { oxxionAnalytics.originEnableAnalytics(config); // call the base class function initOptions = config.options; - if (initOptions.domain) { endpoint = 'https://' + initOptions.domain; } + if (initOptions.domain) { + endpoint = 'https://' + initOptions.domain; + } + if (window.OXXION_MODE) { + mode = window.OXXION_MODE; + } }; adapterManager.registerAnalyticsAdapter({ diff --git a/test/spec/modules/oxxionAnalyticsAdapter_spec.js b/test/spec/modules/oxxionAnalyticsAdapter_spec.js index 5516fb83320..13dc395968a 100644 --- a/test/spec/modules/oxxionAnalyticsAdapter_spec.js +++ b/test/spec/modules/oxxionAnalyticsAdapter_spec.js @@ -167,7 +167,8 @@ describe('Oxxion Analytics', function () { 'meta': { 'advertiserDomains': [ 'example.com' - ] + ], + 'demandSource': 'something' }, 'renderer': 'something', 'originalCpm': 25.02521, @@ -313,13 +314,16 @@ describe('Oxxion Analytics', function () { expect(message.auctionEnd[0].bidsReceived[0]).not.to.have.property('ad'); expect(message.auctionEnd[0].bidsReceived[0]).to.have.property('meta'); expect(message.auctionEnd[0].bidsReceived[0].meta).to.have.property('advertiserDomains'); + expect(message.auctionEnd[0].bidsReceived[0].meta).to.have.property('demandSource'); expect(message.auctionEnd[0].bidsReceived[0]).to.have.property('adId'); expect(message.auctionEnd[0]).to.have.property('bidderRequests').and.to.have.lengthOf(1); expect(message.auctionEnd[0].bidderRequests[0]).to.have.property('gdprConsent'); expect(message.auctionEnd[0].bidderRequests[0].gdprConsent).not.to.have.property('vendorData'); + expect(message.auctionEnd[0].bidderRequests[0]).to.have.property('oxxionMode'); }); it('test bidWon', function() { + window.OXXION_MODE = {'abtest': true}; adapterManager.registerAnalyticsAdapter({ code: 'oxxion', adapter: oxxionAnalytics @@ -337,6 +341,7 @@ describe('Oxxion Analytics', function () { expect(message).not.to.have.property('ad'); expect(message).to.have.property('adId') expect(message).to.have.property('cpmIncrement').and.to.equal(27.4276); + expect(message).to.have.property('oxxionMode').and.to.have.property('abtest').and.to.equal(true); // sinon.assert.callCount(oxxionAnalytics.track, 1); }); }); From 55df5c96694fc2aca4388373821995e12eb3c479 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 9 Aug 2023 18:18:28 +0200 Subject: [PATCH 37/88] Criteo Bid Adapter : Pass bid id through criteo bid adapter (#10336) Co-authored-by: v.raybaud --- modules/criteoBidAdapter.js | 1 + test/spec/modules/criteoBidAdapter_spec.js | 27 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 5e0cdbe0b70..9ff6b540467 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -463,6 +463,7 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { networkId = bidRequest.params.networkId || networkId; schain = bidRequest.schain || schain; const slot = { + slotid: bidRequest.bidId, impid: bidRequest.adUnitCode, transactionid: bidRequest.ortb2Imp?.ext?.tid }; diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index 5f93593fb32..7cba0e2fbdf 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -703,6 +703,33 @@ describe('The Criteo bidding adapter', function () { expect(ortbRequest.source.tid).to.equal('abc'); }); + it('should properly transmit bidId if available', function () { + const bidderRequest = { + ortb2: { + source: { + tid: 'abc' + } + } + }; + const bidRequests = [ + { + bidId: 'bidId', + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: {} + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + const ortbRequest = request.data; + expect(ortbRequest.slots[0].slotid).to.equal('bidId'); + }); + it('should properly build a request if refererInfo is not provided', function () { const bidderRequest = {}; const bidRequests = [ From 514286505fe4dd689ae3be95a2812f35d8309e26 Mon Sep 17 00:00:00 2001 From: Jason Quaccia Date: Wed, 9 Aug 2023 09:27:44 -0700 Subject: [PATCH 38/88] Bid Viewability Module: Support Core's Billing Deferral Logic (#10326) * wrote tests * reverted changes used for dev debugging * minor change * updated markdown file --- modules/bidViewability.js | 7 +++++ modules/bidViewability.md | 17 ++++++------ test/spec/modules/bidViewability_spec.js | 33 +++++++++++++++++++++++- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/modules/bidViewability.js b/modules/bidViewability.js index a5cab99b1a7..be18095e369 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -67,6 +67,8 @@ export let logWinningBidNotFound = (slot) => { export let impressionViewableHandler = (globalModuleConfig, slot, event) => { let respectiveBid = getMatchingWinningBidForGPTSlot(globalModuleConfig, slot); + let respectiveDeferredAdUnit = getGlobal().adUnits.find(adUnit => adUnit.deferBilling && respectiveBid.adUnitCode === adUnit.code); + if (respectiveBid === null) { logWinningBidNotFound(slot); } else { @@ -74,6 +76,11 @@ export let impressionViewableHandler = (globalModuleConfig, slot, event) => { fireViewabilityPixels(globalModuleConfig, respectiveBid); // trigger respective bidder's onBidViewable handler adapterManager.callBidViewableBidder(respectiveBid.adapterCode || respectiveBid.bidder, respectiveBid); + + if (respectiveDeferredAdUnit) { + adapterManager.callBidBillableBidder(respectiveBid); + } + // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels events.emit(CONSTANTS.EVENTS.BID_VIEWABLE, respectiveBid); } diff --git a/modules/bidViewability.md b/modules/bidViewability.md index 78a1539fb1a..922a4a9def4 100644 --- a/modules/bidViewability.md +++ b/modules/bidViewability.md @@ -2,19 +2,20 @@ Module Name: bidViewability -Purpose: Track when a bid is viewable +Purpose: Track when a bid is viewable (and also ready for billing) Maintainer: harshad.mane@pubmatic.com # Description -- This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Analytics adapters, bidders will need to implement `onBidViewable` method to capture this event -- Bidderes can check if this module is part of the final build and whether it is enabled or not by accessing ```pbjs.getConfig('bidViewability')``` +- This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Analytics adapters, bidders will need to implement the `onBidViewable` method to capture this event +- Bidders can check if this module is part of the final build and whether it is enabled or not by accessing ```pbjs.getConfig('bidViewability')``` - GPT API is used to find when a bid is viewable, https://developers.google.com/publisher-tag/reference#googletag.events.impressionviewableevent . This event is fired when an impression becomes viewable, according to the Active View criteria. Refer: https://support.google.com/admanager/answer/4524488 -- The module does not work with adserver other than GAM with GPT integration +- This module does not work with any adserver's other than GAM with GPT integration - Logic used to find a matching pbjs-bid for a GPT slot is ``` (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode) ``` this logic can be changed by using param ```customMatchFunction``` -- When a rendered PBJS bid is viewable the module will trigger BID_VIEWABLE event, which can be consumed by bidders and analytics adapters -- For the viewable bid if ```bid.vurls type array``` param is and module config ``` firePixels: true ``` is set then the URLs mentioned in bid.vurls will be executed. Please note that GDPR and USP related parameters will be added to the given URLs +- When a rendered PBJS bid is viewable the module will trigger a BID_VIEWABLE event, which can be consumed by bidders and analytics adapters +- If the viewable bid contains a ```vurls``` param containing URL's and the Bid Viewability module is configured with ``` firePixels: true ``` then the URLs mentioned in bid.vurls will be called. Please note that GDPR and USP related parameters will be added to the given URLs +- This module is also compatible with Prebid core's billing deferral logic, this means that bids linked to an ad unit marked with `deferBilling: true` will trigger a bid adapter's `onBidBillable` function (if present) indicating an ad slot was viewed and also billing ready (if it were deferred). # Params - enabled [required] [type: boolean, default: false], when set to true, the module will emit BID_VIEWABLE when applicable @@ -44,6 +45,6 @@ Refer: https://support.google.com/admanager/answer/4524488 ``` # Please Note: -- Doesn't seems to work with Instream Video, https://docs.prebid.org/dev-docs/examples/instream-banner-mix.html as GPT's impressionViewable event is not triggered for instream-video-creative -- Works with Banner, Outsteam, Native creatives +- This module doesn't seem to work with Instream Video, https://docs.prebid.org/dev-docs/examples/instream-banner-mix.html as GPT's impressionViewable event is not triggered for instream-video-creative +- Works with Banner, Outsteam and Native creatives diff --git a/test/spec/modules/bidViewability_spec.js b/test/spec/modules/bidViewability_spec.js index a822d86f852..2d2e51abbe1 100644 --- a/test/spec/modules/bidViewability_spec.js +++ b/test/spec/modules/bidViewability_spec.js @@ -245,18 +245,31 @@ describe('#bidViewability', function() { let logWinningBidNotFoundSpy; let callBidViewableBidderSpy; let winningBidsArray; + let callBidBillableBidderSpy; + let adUnits = [ + { + 'code': 'abc123', + 'bids': [ + { + 'bidder': 'pubmatic' + } + ] + } + ]; beforeEach(function() { sandbox = sinon.sandbox.create(); triggerPixelSpy = sandbox.spy(utils, ['triggerPixel']); eventsEmitSpy = sandbox.spy(events, ['emit']); callBidViewableBidderSpy = sandbox.spy(adapterManager, ['callBidViewableBidder']); + callBidBillableBidderSpy = sandbox.spy(adapterManager, ['callBidBillableBidder']); // mocking winningBidsArray winningBidsArray = []; sandbox.stub(prebidGlobal, 'getGlobal').returns({ getAllWinningBids: function (number) { return winningBidsArray; - } + }, + adUnits }); }); @@ -293,5 +306,23 @@ describe('#bidViewability', function() { // CONSTANTS.EVENTS.BID_VIEWABLE is NOT triggered expect(eventsEmitSpy.callCount).to.equal(0); }); + + it('should call the callBidBillableBidder function if the viewable bid is associated with an ad unit with deferBilling set to true', function() { + let moduleConfig = {}; + const deferredBillingAdUnit = { + 'code': '/harshad/Jan/2021/', + 'deferBilling': true, + 'bids': [ + { + 'bidder': 'pubmatic' + } + ] + }; + adUnits.push(deferredBillingAdUnit); + winningBidsArray.push(PBJS_WINNING_BID); + bidViewability.impressionViewableHandler(moduleConfig, GPT_SLOT, null); + expect(callBidBillableBidderSpy.callCount).to.equal(1); + sinon.assert.calledWith(callBidBillableBidderSpy, PBJS_WINNING_BID); + }); }); }); From df7e863623c5a2636a5e514cd8b86242db37a1d3 Mon Sep 17 00:00:00 2001 From: PeiZ <74068135+peixunzhang@users.noreply.github.com> Date: Wed, 9 Aug 2023 18:32:43 +0200 Subject: [PATCH 39/88] CM-896 (#10335) Update prebid dependency on LC --- package-lock.json | 70 +++++++++++++++++++++++++++++++++++------------ package.json | 2 +- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index fc17840b5c1..53cec8da74c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "prebid.js", - "version": "8.5.0-pre", + "version": "8.8.0-pre", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.16.7", @@ -22,7 +22,7 @@ "express": "^4.15.4", "fun-hooks": "^0.9.9", "just-clone": "^1.0.2", - "live-connect-js": "^5.0.0" + "live-connect-js": "^6.0.0" }, "devDependencies": { "@babel/eslint-parser": "^7.16.5", @@ -15469,6 +15469,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -16232,23 +16240,36 @@ "dev": true }, "node_modules/live-connect-common": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-1.0.0.tgz", - "integrity": "sha512-LBZsvykcGeVRYI1eqqXrrNZsoBdL2a8cpyrYPIiGAF/CpixbyRbvqGslaFw511lH294QB16J3fYYg21aYuaM2Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.0.0.tgz", + "integrity": "sha512-pa1SuzCg8ovsB6OziAQZpDid/OT8k37VgWFQkE8OUmG52Kf9PUtJM8wqaGdMXd/rNAe/NH8m+Kxx9MZuOvn5zg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/live-connect-handlers": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/live-connect-handlers/-/live-connect-handlers-2.1.0.tgz", + "integrity": "sha512-uABe9D6yRp7HRgO6vhdIM5j88l17/ROzYGIOHc2Rv1TacLFH6IJ8sbmunY5mIJ9L6ArOVmL4WHY+QgOIkabhxg==", + "dependencies": { + "js-cookie": "^3.0.5", + "live-connect-common": "^3.0.0" + }, "engines": { "node": ">=8" } }, "node_modules/live-connect-js": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-5.0.0.tgz", - "integrity": "sha512-Bv0wQQ+/1VU0/YczEpObbWtHbuXwaHGxwg1+Pe7ZlDgBLb334CrqSQvOL1uyZw3//zs+fSO94yYaQzjjkTd5OQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-6.0.0.tgz", + "integrity": "sha512-4iMSeDPpueYoGEK4U152yKg+hj7jTxkzyfJUAGxtVHxdgjsjh8QmP3kLlTLBBrR/g/L/WCX47PBQxtGW9z8Rlw==", "dependencies": { - "live-connect-common": "^1.0.0", + "live-connect-common": "^3.0.0", + "live-connect-handlers": "^2.0.0", "tiny-hashes": "1.0.1" }, "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/livereload-js": { @@ -37262,6 +37283,11 @@ } } }, + "js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -37883,16 +37909,26 @@ "dev": true }, "live-connect-common": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-1.0.0.tgz", - "integrity": "sha512-LBZsvykcGeVRYI1eqqXrrNZsoBdL2a8cpyrYPIiGAF/CpixbyRbvqGslaFw511lH294QB16J3fYYg21aYuaM2Q==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.0.0.tgz", + "integrity": "sha512-pa1SuzCg8ovsB6OziAQZpDid/OT8k37VgWFQkE8OUmG52Kf9PUtJM8wqaGdMXd/rNAe/NH8m+Kxx9MZuOvn5zg==" + }, + "live-connect-handlers": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/live-connect-handlers/-/live-connect-handlers-2.1.0.tgz", + "integrity": "sha512-uABe9D6yRp7HRgO6vhdIM5j88l17/ROzYGIOHc2Rv1TacLFH6IJ8sbmunY5mIJ9L6ArOVmL4WHY+QgOIkabhxg==", + "requires": { + "js-cookie": "^3.0.5", + "live-connect-common": "^3.0.0" + } }, "live-connect-js": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-5.0.0.tgz", - "integrity": "sha512-Bv0wQQ+/1VU0/YczEpObbWtHbuXwaHGxwg1+Pe7ZlDgBLb334CrqSQvOL1uyZw3//zs+fSO94yYaQzjjkTd5OQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-6.0.0.tgz", + "integrity": "sha512-4iMSeDPpueYoGEK4U152yKg+hj7jTxkzyfJUAGxtVHxdgjsjh8QmP3kLlTLBBrR/g/L/WCX47PBQxtGW9z8Rlw==", "requires": { - "live-connect-common": "^1.0.0", + "live-connect-common": "^3.0.0", + "live-connect-handlers": "^2.0.0", "tiny-hashes": "1.0.1" } }, diff --git a/package.json b/package.json index 4a0e2b1129f..d97dcf6fc20 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "express": "^4.15.4", "fun-hooks": "^0.9.9", "just-clone": "^1.0.2", - "live-connect-js": "^5.0.0" + "live-connect-js": "^6.0.0" }, "optionalDependencies": { "fsevents": "^2.3.2" From 5dd512f08e4648b4615e6ac9ee6698c4f949d95e Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Wed, 9 Aug 2023 15:15:32 -0400 Subject: [PATCH 40/88] liveIntent Id System: fix ix eid mismatch with ix adapter (#10341) * Update liveIntentIdSystem.js * updated expected source for index --------- Co-authored-by: Love Sharma --- modules/liveIntentIdSystem.js | 2 +- test/spec/modules/eids_spec.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index 70355e9dd09..2d9e6b63a35 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -299,7 +299,7 @@ export const liveIntentIdSubmodule = { } }, 'index': { - source: 'indexexchange.com', + source: 'liveintent.indexexchange.com', atype: 3, getValue: function(data) { return data.id; diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index de5ad025f69..1597790e652 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -278,7 +278,7 @@ describe('eids array generation for known sub-modules', function() { const newEids = createEidsArray(userId); expect(newEids.length).to.equal(1); expect(newEids[0]).to.deep.equal({ - source: 'indexexchange.com', + source: 'liveintent.indexexchange.com', uids: [{ id: 'sample_id', atype: 3 @@ -293,7 +293,7 @@ describe('eids array generation for known sub-modules', function() { const newEids = createEidsArray(userId); expect(newEids.length).to.equal(1); expect(newEids[0]).to.deep.equal({ - source: 'indexexchange.com', + source: 'liveintent.indexexchange.com', uids: [{ id: 'sample_id', atype: 3, From b3f0ccb4a6728bbd44e329babafb369a1a568ba6 Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Wed, 9 Aug 2023 19:52:12 +0000 Subject: [PATCH 41/88] Prebid 8.8.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 53cec8da74c..31ebd0fac2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.8.0-pre", + "version": "8.8.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index d97dcf6fc20..90d396b5ccb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.8.0-pre", + "version": "8.8.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 7a8c6f4fb9ad2db48f6254283802e9058db1c761 Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Wed, 9 Aug 2023 19:52:13 +0000 Subject: [PATCH 42/88] Increment version to 8.9.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 31ebd0fac2f..5f36598a762 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.8.0", + "version": "8.9.0-pre", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 90d396b5ccb..17541005a4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.8.0", + "version": "8.9.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 80c7220d12a0d25dcc490d9eb66c8b0619c06609 Mon Sep 17 00:00:00 2001 From: Gabriel Chicoye Date: Thu, 10 Aug 2023 13:29:36 +0200 Subject: [PATCH 43/88] ad added (#10344) --- modules/nexx360BidAdapter.js | 8 +++- test/spec/modules/nexx360BidAdapter_spec.js | 49 ++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/modules/nexx360BidAdapter.js b/modules/nexx360BidAdapter.js index 671cc800980..273f2290c68 100644 --- a/modules/nexx360BidAdapter.js +++ b/modules/nexx360BidAdapter.js @@ -192,7 +192,13 @@ function interpretResponse(serverResponse) { }; if (allowAlternateBidderCodes) response.bidderCode = `n360-${bid.ext.ssp}`; - if (bid.ext.mediaType === BANNER) response.adUrl = bid.ext.adUrl; + if (bid.ext.mediaType === BANNER) { + if (bid.adm) { + response.ad = bid.adm; + } else { + response.adUrl = bid.ext.adUrl; + } + } if ([INSTREAM, OUTSTREAM].includes(bid.ext.mediaType)) response.vastXml = bid.ext.vastXml; if (bid.ext.mediaType === OUTSTREAM) { diff --git a/test/spec/modules/nexx360BidAdapter_spec.js b/test/spec/modules/nexx360BidAdapter_spec.js index 7c2cea99a46..da4e6b414b0 100644 --- a/test/spec/modules/nexx360BidAdapter_spec.js +++ b/test/spec/modules/nexx360BidAdapter_spec.js @@ -434,7 +434,7 @@ describe('Nexx360 bid adapter tests', function () { const output = spec.interpretResponse(response); expect(output.length).to.be.eql(0); }); - it('banner responses', function() { + it('banner responses with adUrl only', function() { const response = { body: { 'id': 'a8d3a675-a4ba-4d26-807f-c8f2fad821e0', @@ -479,6 +479,53 @@ describe('Nexx360 bid adapter tests', function () { expect(output[0].currency).to.be.eql(response.body.cur); expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); }); + it('banner responses with adm', function() { + const response = { + body: { + 'id': 'a8d3a675-a4ba-4d26-807f-c8f2fad821e0', + 'cur': 'USD', + 'seatbid': [ + { + 'bid': [ + { + 'id': '4427551302944024629', + 'impid': '226175918ebeda', + 'price': 1.5, + 'adomain': [ + 'http://prebid.org' + ], + 'crid': '98493581', + 'ssp': 'appnexus', + 'h': 600, + 'w': 300, + 'adm': '
TestAd
', + 'cat': [ + 'IAB3-1' + ], + 'ext': { + 'adUnitCode': 'div-1', + 'mediaType': 'banner', + 'adUrl': 'https://fast.nexx360.io/cache?uuid=fdddcebc-1edf-489d-880d-1418d8bdc493', + 'ssp': 'appnexus', + } + } + ], + 'seat': 'appnexus' + } + ], + 'ext': { + 'id': 'de3de7c7-e1cf-4712-80a9-94eb26bfc718', + 'cookies': [] + }, + } + }; + const output = spec.interpretResponse(response); + expect(output[0].ad).to.be.eql(response.body.seatbid[0].bid[0].adm); + expect(output[0].adUrl).to.be.eql(undefined); + expect(output[0].mediaType).to.be.eql(response.body.seatbid[0].bid[0].ext.mediaType); + expect(output[0].currency).to.be.eql(response.body.cur); + expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); + }); it('instream responses', function() { const response = { body: { From 3bbe63b5b3ff2bf169c6f4c55b07a828f6ad12e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Rotta?= Date: Thu, 10 Aug 2023 16:58:21 +0200 Subject: [PATCH 44/88] enrich adagio bid params (#10346) --- modules/adagioBidAdapter.js | 14 +++++++++++--- test/spec/modules/adagioBidAdapter_spec.js | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index 2e1b7453a7a..c642dff5a8f 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -996,7 +996,7 @@ export const spec = { const aucId = generateUUID() const adUnits = _map(validBidRequests, (rawBidRequest) => { - const bidRequest = {...rawBidRequest} + const bidRequest = deepClone(rawBidRequest); // Fix https://github.com/prebid/Prebid.js/issues/9781 bidRequest.auctionId = aucId @@ -1130,8 +1130,8 @@ export const spec = { // remove useless props delete adUnitCopy.floorData; delete adUnitCopy.params.siteId; - delete adUnitCopy.userId - delete adUnitCopy.userIdAsEids + delete adUnitCopy.userId; + delete adUnitCopy.userIdAsEids; groupedAdUnits[adUnitCopy.params.organizationId] = groupedAdUnits[adUnitCopy.params.organizationId] || []; groupedAdUnits[adUnitCopy.params.organizationId].push(adUnitCopy); @@ -1139,6 +1139,14 @@ export const spec = { return groupedAdUnits; }, {}); + // Adding more params on the original bid object. + // Those params are not sent to the server. + // They are used for further operations on analytics adapter. + validBidRequests.forEach(rawBidRequest => { + rawBidRequest.params.adagioAuctionId = aucId + rawBidRequest.params.pageviewId = pageviewId + }); + // Build one request per organizationId const requests = _map(Object.keys(groupedAdUnits), organizationId => { return { diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index 7fa35456e3b..1f734a6a7fc 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -322,6 +322,21 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.adUnits[0].transactionId).to.not.exist; }); + it('should enrich prebid bid requests params', function() { + const expectedAuctionId = '373bcda7-9794-4f1c-be2c-0d223d11d579' + const expectedPageviewId = '56befc26-8cf0-472d-b105-73896df8eb89'; + sandbox.stub(utils, 'generateUUID').returns(expectedAuctionId); + sandbox.stub(adagio, 'getPageviewId').returns(expectedPageviewId); + + const bid01 = new BidRequestBuilder().withParams().build(); + const bidderRequest = new BidderRequestBuilder().build(); + + spec.buildRequests([bid01], bidderRequest); + + expect(bid01.params.adagioAuctionId).eq(expectedAuctionId); + expect(bid01.params.pageviewId).eq(expectedPageviewId); + }); + it('should enqueue computed features for collect usage', function() { sandbox.stub(Date, 'now').returns(12345); From 04c757032048c1fe7ec5e5d1cf9e821a8ca2a7bb Mon Sep 17 00:00:00 2001 From: kapil-tuptewar <91458408+kapil-tuptewar@users.noreply.github.com> Date: Thu, 10 Aug 2023 23:36:22 +0530 Subject: [PATCH 45/88] PubMatic Analytics Adapter : log floor values only when floor file is fetched successfully (#10328) * Log floor values only when floor data is present * Added test cases when floor location is other than fetch * Added comment --- modules/pubmaticAnalyticsAdapter.js | 19 +++- src/constants.json | 9 +- .../modules/pubmaticAnalyticsAdapter_spec.js | 104 ++++++++++++++++-- 3 files changed, 120 insertions(+), 12 deletions(-) diff --git a/modules/pubmaticAnalyticsAdapter.js b/modules/pubmaticAnalyticsAdapter.js index acae93c57be..8969b6e4f93 100755 --- a/modules/pubmaticAnalyticsAdapter.js +++ b/modules/pubmaticAnalyticsAdapter.js @@ -283,7 +283,7 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { 'ocpm': bid.bidResponse ? (bid.bidResponse.originalCpm || 0) : 0, 'ocry': bid.bidResponse ? (bid.bidResponse.originalCurrency || CURRENCY_USD) : CURRENCY_USD, 'piid': bid.bidResponse ? (bid.bidResponse.partnerImpId || EMPTY_STRING) : EMPTY_STRING, - 'frv': (s2sBidders.indexOf(bid.bidder) > -1) ? undefined : (bid.bidResponse ? (bid.bidResponse.floorData ? bid.bidResponse.floorData.floorRuleValue : undefined) : undefined), + 'frv': (bid.bidResponse ? (bid.bidResponse.floorData ? bid.bidResponse.floorData.floorRuleValue : undefined) : undefined), 'md': bid.bidResponse ? getMetadata(bid.bidResponse.meta) : undefined }); }); @@ -319,6 +319,17 @@ function getTgId() { return 0; } +function getFloorFetchStatus(floorData) { + if (!floorData?.floorRequestData) { + return false; + } + const { location, fetchStatus } = floorData?.floorRequestData; + const isDataValid = location !== CONSTANTS.FLOOR_VALUES.NO_DATA; + const isFetchSuccessful = location === CONSTANTS.FLOOR_VALUES.FETCH && fetchStatus === CONSTANTS.FLOOR_VALUES.SUCCESS; + const isAdUnitOrSetConfig = location === CONSTANTS.FLOOR_VALUES.AD_UNIT || location === CONSTANTS.FLOOR_VALUES.SET_CONFIG; + return isDataValid && (isAdUnitOrSetConfig || isFetchSuccessful); +} + function executeBidsLoggerCall(e, highestCpmBids) { let auctionId = e.auctionId; let referrer = config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''; @@ -326,6 +337,8 @@ function executeBidsLoggerCall(e, highestCpmBids) { let floorData = auctionCache.floorData; let outputObj = { s: [] }; let pixelURL = END_POINT_BID_LOGGER; + // will return true if floor data is present. + let fetchStatus = getFloorFetchStatus(auctionCache.floorData); if (!auctionCache) { return; @@ -347,7 +360,7 @@ function executeBidsLoggerCall(e, highestCpmBids) { outputObj['dvc'] = {'plt': getDevicePlatform()}; outputObj['tgid'] = getTgId(); - if (floorData) { + if (floorData && fetchStatus) { outputObj['fmv'] = floorData.floorRequestData ? floorData.floorRequestData.modelVersion || undefined : undefined; outputObj['ft'] = floorData.floorResponseData ? (floorData.floorResponseData.enforcements.enforceJS == false ? 0 : 1) : undefined; } @@ -362,7 +375,7 @@ function executeBidsLoggerCall(e, highestCpmBids) { 'mt': getAdUnitAdFormats(origAdUnit), 'sz': getSizesForAdUnit(adUnit, adUnitId), 'ps': gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestCpmBids.filter(bid => bid.adUnitCode === adUnitId)), - 'fskp': floorData ? (floorData.floorRequestData ? (floorData.floorRequestData.skipped == false ? 0 : 1) : undefined) : undefined, + 'fskp': (floorData && fetchStatus) ? (floorData.floorRequestData ? (floorData.floorRequestData.skipped == false ? 0 : 1) : undefined) : undefined, }; slotsArray.push(slotObject); return slotsArray; diff --git a/src/constants.json b/src/constants.json index f198c8fcac4..b7593b4867e 100644 --- a/src/constants.json +++ b/src/constants.json @@ -169,5 +169,12 @@ "adTemplate", "rendererUrl", "type" - ] + ], + "FLOOR_VALUES": { + "NO_DATA": "noData", + "AD_UNIT": "adUnit", + "SET_CONFIG": "setConfig", + "FETCH": "fetch", + "SUCCESS": "success" + } } diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index c56ed565c43..8e9580839d8 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -393,7 +393,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].af).to.equal('video'); expect(data.s[0].ps[0].ocpm).to.equal(1.23); expect(data.s[0].ps[0].ocry).to.equal('USD'); - expect(data.s[0].ps[0].frv).to.equal(undefined); + expect(data.s[0].ps[0].frv).to.equal(1.1); // slot 2 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); @@ -422,7 +422,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); // tracker slot1 let firstTracker = requests[0].url; @@ -452,6 +452,94 @@ describe('pubmatic analytics adapter', function () { expect(data.af).to.equal('video'); }); + it('Logger: do not log floor fields when prebids floor shows noData in location property', function() { + const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); + BID_REQUESTED_COPY['bids'][1]['floorData']['location'] = 'noData'; + + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] + }); + + config.setConfig({ + testGroupId: 15 + }); + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, BID_REQUESTED_COPY); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + + let data = getLoggerJsonFromRequest(request.requestBody); + + expect(data.pubid).to.equal('9999'); + expect(data.fmv).to.equal(undefined); + + // slot 1 + expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + + // slot 2 + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].au).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].fskp).to.equal(undefined); + }); + + it('Logger: log floor fields when prebids floor shows setConfig in location property', function() { + const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); + BID_REQUESTED_COPY['bids'][1]['floorData']['location'] = 'setConfig'; + + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] + }); + + config.setConfig({ + testGroupId: 15 + }); + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, BID_REQUESTED_COPY); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + + let data = getLoggerJsonFromRequest(request.requestBody); + + expect(data.pubid).to.equal('9999'); + expect(data.fmv).to.equal('floorModelTest'); + + // slot 1 + expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + + // slot 2 + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].au).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].fskp).to.equal(0); + }); + it('bidCpmAdjustment: USD: Logger: best case + win tracker', function() { const bidCopy = utils.deepClone(BID); bidCopy.cpm = bidCopy.originalCpm * 2; // bidCpmAdjustment => bidCpm * 2 @@ -501,7 +589,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].af).to.equal('video'); expect(data.s[0].ps[0].ocpm).to.equal(1.23); expect(data.s[0].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); // tracker slot1 let firstTracker = requests[0].url; expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); @@ -711,7 +799,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); }); it('Logger: currency conversion check', function() { @@ -818,7 +906,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); expect(data.dvc).to.deep.equal({'plt': 2}); // respective tracker slot let firstTracker = requests[1].url; @@ -876,7 +964,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); expect(data.dvc).to.deep.equal({'plt': 1}); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); // respective tracker slot let firstTracker = requests[1].url; expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); @@ -929,7 +1017,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); // respective tracker slot let firstTracker = requests[1].url; expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); @@ -1091,7 +1179,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); // tracker slot1 let firstTracker = requests[0].url; From 34846c77a1b48241d117ff7112324352fa5a5336 Mon Sep 17 00:00:00 2001 From: Michele Nasti Date: Fri, 11 Aug 2023 00:14:15 +0200 Subject: [PATCH 46/88] Native: privacyLink is now converted to ortb.privacy (#10271) * fix for privacyLink in native #10249 * handle privacy link in response too * privacy link should be returned in response --------- Co-authored-by: Michele Nasti --- src/constants.json | 1 - src/native.js | 13 ++++++++++++- test/spec/native_spec.js | 17 +++++++++++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/constants.json b/src/constants.json index b7593b4867e..e8cb64e575d 100644 --- a/src/constants.json +++ b/src/constants.json @@ -163,7 +163,6 @@ "MAIN": 3 }, "NATIVE_KEYS_THAT_ARE_NOT_ASSETS": [ - "privacyLink", "clickUrl", "sendTargetingKeys", "adTemplate", diff --git a/src/native.js b/src/native.js index 66d1fd4becd..1414c7a2ee7 100644 --- a/src/native.js +++ b/src/native.js @@ -480,6 +480,11 @@ export function toOrtbNativeRequest(legacyNativeAssets) { continue; } + if (key === 'privacyLink') { + ortb.privacy = 1; + continue; + } + const asset = legacyNativeAssets[key]; let required = 0; if (asset.required && isBoolean(asset.required)) { @@ -623,6 +628,9 @@ export function fromOrtbNativeRequest(openRTBRequest) { oldNativeObject[prebidAssetName].len = asset.data.len; } } + if (openRTBRequest.privacy) { + oldNativeObject.privacyLink = { required: false }; + } // video was not supported by old prebid assets } return oldNativeObject; @@ -696,8 +704,11 @@ export function legacyPropertiesToOrtbNative(legacyNative) { // in general, native trackers seem to be neglected and/or broken response.jstracker = Array.isArray(value) ? value.join('') : value; break; + case 'privacyLink': + response.privacy = value; + break; } - }) + }); return response; } diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js index a1b546e00f4..9cfee6f5cd8 100644 --- a/test/spec/native_spec.js +++ b/test/spec/native_spec.js @@ -862,6 +862,9 @@ describe('validate native', function () { }] }, address: {}, + privacyLink: { + required: true + } }, }, }; @@ -917,6 +920,7 @@ describe('validate native', function () { type: 9, } }); + expect(ortb.privacy).to.equal(1); }); ['bogusKey', 'clickUrl', 'privacyLink'].forEach(nativeKey => { @@ -1024,11 +1028,14 @@ describe('validate native', function () { expect(oldNativeRequest.sponsoredBy).to.include({ required: true, len: 25 - }) + }); expect(oldNativeRequest.body).to.include({ required: true, len: 140 - }) + }); + expect(oldNativeRequest.privacyLink).to.include({ + required: false + }); }); if (FEATURES.NATIVE) { @@ -1199,6 +1206,12 @@ describe('legacyPropertiesToOrtbNative', () => { expect(native.jstracker).to.eql('some-markupsome-other-markup'); }) }); + describe('privacylink', () => { + it('should convert privacyLink to privacy', () => { + const native = legacyPropertiesToOrtbNative({privacyLink: 'https:/my-privacy-link.com'}); + expect(native.privacy).to.eql('https:/my-privacy-link.com'); + }) + }) }); describe('fireImpressionTrackers', () => { From 52996a6c7bdfa4b1c40d8631cc73dde7e5e3c0d8 Mon Sep 17 00:00:00 2001 From: Laurentiu Badea Date: Thu, 10 Aug 2023 15:14:32 -0700 Subject: [PATCH 47/88] fledgeForGpt: consolidate publisher configuration (#10136) * FLEDGE: option to configure all adUnits for specific bidders Resolves #10105 * Update doc * More tests and consistent values for fledgeEnabled --- modules/fledgeForGpt.js | 18 +++- modules/fledgeForGpt.md | 27 ++++-- test/spec/modules/fledge_spec.js | 156 ++++++++++++++++++++++--------- 3 files changed, 144 insertions(+), 57 deletions(-) diff --git a/modules/fledgeForGpt.js b/modules/fledgeForGpt.js index f29ce7508d5..1ee5b0b4a9e 100644 --- a/modules/fledgeForGpt.js +++ b/modules/fledgeForGpt.js @@ -56,18 +56,26 @@ function isFledgeSupported() { export function markForFledge(next, bidderRequests) { if (isFledgeSupported()) { + const globalFledgeConfig = config.getConfig('fledgeForGpt'); + const bidders = globalFledgeConfig?.bidders ?? []; bidderRequests.forEach((req) => { - req.fledgeEnabled = config.runWithBidder(req.bidderCode, () => config.getConfig('fledgeEnabled')) - }) + const useGlobalConfig = globalFledgeConfig?.enabled && (bidders.length == 0 || bidders.includes(req.bidderCode)); + Object.assign(req, config.runWithBidder(req.bidderCode, () => { + return { + fledgeEnabled: config.getConfig('fledgeEnabled') ?? (useGlobalConfig ? globalFledgeConfig.enabled : undefined), + defaultForSlots: config.getConfig('defaultForSlots') ?? (useGlobalConfig ? globalFledgeConfig?.defaultForSlots : undefined) + } + })); + }); } next(bidderRequests); } getHook('makeBidRequests').after(markForFledge); export function setImpExtAe(imp, bidRequest, context) { - if (!context.bidderRequest.fledgeEnabled) { - delete imp.ext?.ae; - } + const impExt = imp.ext ?? {}; + impExt.ae = context.bidderRequest.fledgeEnabled ? (impExt.ae ?? context.bidderRequest.defaultForSlots) : undefined; + imp.ext = impExt; } registerOrtbProcessor({type: IMP, name: 'impExtAe', fn: setImpExtAe}); diff --git a/modules/fledgeForGpt.md b/modules/fledgeForGpt.md index 3bb86cd5946..28f44da6459 100644 --- a/modules/fledgeForGpt.md +++ b/modules/fledgeForGpt.md @@ -15,8 +15,8 @@ This is accomplished by adding the `fledgeForGpt` module to the list of modules gulp build --modules=fledgeForGpt,... ``` -Second, they must enable FLEDGE in their Prebid.js configuration. To provide a high degree of flexiblity for testing, FLEDGE -settings exist at the module level, the bidder level, and the slot level. +Second, they must enable FLEDGE in their Prebid.js configuration. +This is done through module level configuration, but to provide a high degree of flexiblity for testing, FLEDGE settings also exist at the bidder level and slot level. ### Module Configuration This module exposes the following settings: @@ -24,15 +24,20 @@ This module exposes the following settings: |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | |enabled | Boolean |Enable/disable the module |Defaults to `false` | +|bidders | Array[String] |Optional list of bidders |Defaults to all bidders | +|defaultForSlots | Number |Default value for `imp.ext.ae` in requests for specified bidders |Should be 1 | -As noted above, FLEDGE support is disabled by default. To enable it, set the `enabled` value to `true` for this module -using the `setConfig` method of Prebid.js: +As noted above, FLEDGE support is disabled by default. To enable it, set the `enabled` value to `true` for this module and configure `defaultForSlots` to be `1` (meaning _Client-side auction_). +using the `setConfig` method of Prebid.js. Optionally, a list of +bidders to apply these settings to may be provided: ```js pbjs.que.push(function() { pbjs.setConfig({ fledgeForGpt: { - enabled: true + enabled: true, + bidders: ['openx', 'rtbhouse'], + defaultForSlots: 1 } }); }); @@ -44,23 +49,25 @@ This module adds the following setting for bidders: |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | | fledgeEnabled | Boolean | Enable/disable a bidder to participate in FLEDGE | Defaults to `false` | +|defaultForSlots | Number |Default value for `imp.ext.ae` in requests for specified bidders |Should be 1| -In addition to enabling FLEDGE at the module level, individual bidders must also be enabled. This allows publishers to -selectively test with one or more bidders as they desire. To enable one or more bidders, use the `setBidderConfig` method +Individual bidders may be further included or excluded here using the `setBidderConfig` method of Prebid.js: ```js pbjs.setBidderConfig({ bidders: ["openx"], config: { - fledgeEnabled: true + fledgeEnabled: true, + defaultForSlots: 1 } }); ``` ### AdUnit Configuration -Enabling an adunit for FLEDGE eligibility is accomplished by setting an attribute of the `ortb2Imp` object for that -adunit. +All adunits can be opted-in to FLEDGE in the global config via the `defaultForSlots` parameter. +If needed, adunits can be configured individually by setting an attribute of the `ortb2Imp` object for that +adunit. This attribute will take precedence over `defaultForSlots` setting. |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | diff --git a/test/spec/modules/fledge_spec.js b/test/spec/modules/fledge_spec.js index a81ff05596e..62f5962fe17 100644 --- a/test/spec/modules/fledge_spec.js +++ b/test/spec/modules/fledge_spec.js @@ -61,59 +61,131 @@ describe('fledgeEnabled', function () { config.resetConfig(); }); - it('should set fledgeEnabled correctly per bidder', function () { - config.setConfig({bidderSequence: 'fixed'}) - config.setBidderConfig({ - bidders: ['appnexus'], - config: { - fledgeEnabled: true, - } + const adUnits = [{ + 'code': '/19968336/header-bid-tag1', + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + }, + }, + 'bids': [ + { + 'bidder': 'appnexus', + }, + { + 'bidder': 'rubicon', + }, + ] + }]; + + describe('with setBidderConfig()', () => { + it('should set fledgeEnabled correctly per bidder', function () { + config.setConfig({bidderSequence: 'fixed'}) + config.setBidderConfig({ + bidders: ['appnexus'], + config: { + fledgeEnabled: true, + defaultForSlots: 1, + } + }); + + const bidRequests = adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + + expect(bidRequests[0].bids[0].bidder).equals('appnexus'); + expect(bidRequests[0].fledgeEnabled).to.be.true; + expect(bidRequests[0].defaultForSlots).to.equal(1); + + expect(bidRequests[1].bids[0].bidder).equals('rubicon'); + expect(bidRequests[1].fledgeEnabled).to.be.undefined; + expect(bidRequests[1].defaultForSlots).to.be.undefined; }); + }); - const adUnits = [{ - 'code': '/19968336/header-bid-tag1', - 'mediaTypes': { - 'banner': { - 'sizes': [[728, 90]] - }, - }, - 'bids': [ - { - 'bidder': 'appnexus', - }, - { - 'bidder': 'rubicon', - }, - ] - }]; - - const bidRequests = adapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); - - expect(bidRequests[0].bids[0].bidder).equals('appnexus'); - expect(bidRequests[0].fledgeEnabled).to.be.true; - - expect(bidRequests[1].bids[0].bidder).equals('rubicon'); - expect(bidRequests[1].fledgeEnabled).to.be.undefined; + describe('with setConfig()', () => { + it('should set fledgeEnabled correctly per bidder', function () { + config.setConfig({ + bidderSequence: 'fixed', + fledgeForGpt: { + enabled: true, + bidders: ['appnexus'], + defaultForSlots: 1, + } + }); + + const bidRequests = adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + + expect(bidRequests[0].bids[0].bidder).equals('appnexus'); + expect(bidRequests[0].fledgeEnabled).to.be.true; + expect(bidRequests[0].defaultForSlots).to.equal(1); + + expect(bidRequests[1].bids[0].bidder).equals('rubicon'); + expect(bidRequests[1].fledgeEnabled).to.be.undefined; + expect(bidRequests[1].defaultForSlots).to.be.undefined; + }); + + it('should set fledgeEnabled correctly for all bidders', function () { + config.setConfig({ + bidderSequence: 'fixed', + fledgeForGpt: { + enabled: true, + defaultForSlots: 1, + } + }); + + const bidRequests = adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + + expect(bidRequests[0].bids[0].bidder).equals('appnexus'); + expect(bidRequests[0].fledgeEnabled).to.be.true; + expect(bidRequests[0].defaultForSlots).to.equal(1); + + expect(bidRequests[1].bids[0].bidder).equals('rubicon'); + expect(bidRequests[0].fledgeEnabled).to.be.true; + expect(bidRequests[0].defaultForSlots).to.equal(1); + }); }); }); describe('ortb processors for fledge', () => { - describe('imp.ext.ae', () => { - it('should be removed if fledge is not enabled', () => { + describe('when defaultForSlots is set', () => { + it('imp.ext.ae should be set if fledge is enabled', () => { + const imp = {}; + setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true, defaultForSlots: 1}}); + expect(imp.ext.ae).to.equal(1); + }); + it('imp.ext.ae should be left intact if set on adunit and fledge is enabled', () => { + const imp = {ext: {ae: 2}}; + setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true, defaultForSlots: 1}}); + expect(imp.ext.ae).to.equal(2); + }); + }); + describe('when defaultForSlots is not defined', () => { + 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('should be left intact if fledge is enabled', () => { - const imp = {ext: {ae: false}}; + 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(false); + expect(imp.ext.ae).to.equal(2); }); }); describe('parseExtPrebidFledge', () => { From e1acefe45a251c49eae3b538b71eea973bf5d860 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Fri, 11 Aug 2023 01:39:42 -0700 Subject: [PATCH 48/88] Core: enable `Sec-Browsing-Topics` header on outgoing bidder requests (#10340) * Convert ajax to fetch * Enable browsing topics header for client bidders * Enable browsingTopics for PBS * Improve mockFetchServer requestHeaders property --- modules/geoedgeRtdProvider.js | 6 +- modules/prebidServerBidAdapter/index.js | 10 +- src/adapters/bidderFactory.js | 19 +- src/ajax.js | 213 +++++---- test/mocks/xhr.js | 265 +++++++++++- test/spec/modules/adlooxAdServerVideo_spec.js | 6 +- test/spec/modules/adlooxRtdProvider_spec.js | 6 +- test/spec/modules/categoryTranslation_spec.js | 16 +- .../conversantAnalyticsAdapter_spec.js | 8 +- test/spec/modules/currency_spec.js | 4 +- test/spec/modules/dmdIdSystem_spec.js | 4 +- test/spec/modules/euidIdSystem_spec.js | 19 +- test/spec/modules/feedadBidAdapter_spec.js | 2 +- test/spec/modules/geoedgeRtdProvider_spec.js | 29 +- .../spec/modules/greenbidsRtdProvider_spec.js | 15 +- test/spec/modules/hadronIdSystem_spec.js | 2 +- test/spec/modules/id5AnalyticsAdapter_spec.js | 8 +- test/spec/modules/id5IdSystem_spec.js | 50 +-- .../spec/modules/identityLinkIdSystem_spec.js | 2 - test/spec/modules/intentIqIdSystem_spec.js | 2 - .../modules/invisiblyAnalyticsAdapter_spec.js | 8 +- test/spec/modules/jwplayerRtdProvider_spec.js | 54 ++- .../modules/magniteAnalyticsAdapter_spec.js | 4 +- test/spec/modules/mgidRtdProvider_spec.js | 6 +- test/spec/modules/oguryBidAdapter_spec.js | 24 +- .../spec/modules/ooloAnalyticsAdapter_spec.js | 2 +- .../modules/prebidServerBidAdapter_spec.js | 35 +- test/spec/modules/priceFloors_spec.js | 70 ++- test/spec/modules/publinkIdSystem_spec.js | 2 +- .../modules/pubmaticAnalyticsAdapter_spec.js | 16 +- .../modules/pubwiseAnalyticsAdapter_spec.js | 10 +- test/spec/modules/teadsIdSystem_spec.js | 2 +- test/spec/modules/uid2IdSystem_helpers.js | 10 +- test/spec/modules/uid2IdSystem_spec.js | 48 +-- test/spec/modules/userId_spec.js | 2 +- .../zeta_global_sspAnalyticsAdapter_spec.js | 7 +- test/spec/unit/core/ajax_spec.js | 403 ++++++++++++++++++ test/spec/unit/core/bidderFactory_spec.js | 88 +++- test/spec/videoCache_spec.js | 8 +- 39 files changed, 1105 insertions(+), 380 deletions(-) create mode 100644 test/spec/unit/core/ajax_spec.js diff --git a/modules/geoedgeRtdProvider.js b/modules/geoedgeRtdProvider.js index fdd7aa2f5eb..646d2f4e786 100644 --- a/modules/geoedgeRtdProvider.js +++ b/modules/geoedgeRtdProvider.js @@ -258,8 +258,4 @@ export const geoedgeSubmodule = { onBidResponseEvent: conditionallyWrap }; -export function beforeInit() { - submodule('realTimeData', geoedgeSubmodule); -} - -beforeInit(); +submodule('realTimeData', geoedgeSubmodule); diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 3cc38923d57..3cae4497354 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -18,7 +18,7 @@ import { deepAccess, } from '../../src/utils.js'; import CONSTANTS from '../../src/constants.json'; -import adapterManager from '../../src/adapterManager.js'; +import adapterManager, {s2sActivityParams} from '../../src/adapterManager.js'; import {config} from '../../src/config.js'; import {addComponentAuction, isValid} from '../../src/adapters/bidderFactory.js'; import * as events from '../../src/events.js'; @@ -29,6 +29,8 @@ import {hook} from '../../src/hook.js'; import {hasPurpose1Consent} from '../../src/utils/gpdr.js'; import {buildPBSRequest, interpretPBSResponse} from './ortbConverter.js'; import {useMetrics} from '../../src/utils/perfMetrics.js'; +import {isActivityAllowed} from '../../src/activities/rules.js'; +import {ACTIVITY_TRANSMIT_UFPD} from '../../src/activities/activities.js'; const getConfig = config.getConfig; @@ -571,7 +573,11 @@ export const processPBSRequest = hook('sync', function (s2sBidRequest, bidReques } }, requestJson, - {contentType: 'text/plain', withCredentials: true} + { + contentType: 'text/plain', + withCredentials: true, + browsingTopics: isActivityAllowed(ACTIVITY_TRANSMIT_UFPD, s2sActivityParams(s2sBidRequest.s2sConfig)) + } ); } else { logError('PBS request not made. Check endpoints.'); diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index d316ad5924c..b31019a6d79 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -25,7 +25,7 @@ import {useMetrics} from '../utils/perfMetrics.js'; import {isActivityAllowed} from '../activities/rules.js'; import {activityParams} from '../activities/activityParams.js'; import {MODULE_TYPE_BIDDER} from '../activities/modules.js'; -import {ACTIVITY_TRANSMIT_TID} from '../activities/activities.js'; +import {ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD} from '../activities/activities.js'; /** * This file aims to support Adapters during the Prebid 0.x -> 1.x transition. @@ -454,6 +454,15 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe onRequest(request); const networkDone = requestMetrics.startTiming('net'); + + function getOptions(defaults) { + const ro = request.options; + return Object.assign(defaults, ro, { + browsingTopics: ro?.hasOwnProperty('browsingTopics') && !ro.browsingTopics + ? false + : isActivityAllowed(ACTIVITY_TRANSMIT_UFPD, activityParams(MODULE_TYPE_BIDDER, spec.code)) + }) + } switch (request.method) { case 'GET': ajax( @@ -463,10 +472,10 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe error: onFailure }, undefined, - Object.assign({ + getOptions({ method: 'GET', withCredentials: true - }, request.options) + }) ); break; case 'POST': @@ -477,11 +486,11 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe error: onFailure }, typeof request.data === 'string' ? request.data : JSON.stringify(request.data), - Object.assign({ + getOptions({ method: 'POST', contentType: 'text/plain', withCredentials: true - }, request.options) + }) ); break; default: diff --git a/src/ajax.js b/src/ajax.js index 5e926f3210d..0601cc0e22b 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -1,99 +1,138 @@ -import { config } from './config.js'; -import { logMessage, logError, parseUrl, buildUrl, _each } from './utils.js'; +import {config} from './config.js'; +import {buildUrl, logError, parseUrl} from './utils.js'; -const XHR_DONE = 4; +export const dep = { + fetch: window.fetch.bind(window), + makeRequest: (r, o) => new Request(r, o), + timeout(timeout, resource) { + const ctl = new AbortController(); + let cancelTimer = setTimeout(() => { + ctl.abort(); + logError(`Request timeout after ${timeout}ms`, resource); + cancelTimer = null; + }, timeout); + return { + signal: ctl.signal, + done() { + cancelTimer && clearTimeout(cancelTimer) + } + } + } +} + +const GET = 'GET'; +const POST = 'POST'; +const CTYPE = 'Content-Type'; /** - * Simple IE9+ and cross-browser ajax request function - * Note: x-domain requests in IE9 do not support the use of cookies - * - * @param url string url - * @param callback {object | function} callback - * @param data mixed data - * @param options object + * transform legacy `ajax` parameters into a fetch request. + * @returns {Request} */ -export const ajax = ajaxBuilder(); - -export function ajaxBuilder(timeout = 3000, {request, done} = {}) { - return function(url, callback, data, options = {}) { - try { - let x; - let method = options.method || (data ? 'POST' : 'GET'); - let parser = document.createElement('a'); - parser.href = url; - - let callbacks = typeof callback === 'object' && callback !== null ? callback : { - success: function() { - logMessage('xhr success'); - }, - error: function(e) { - logError('xhr error', null, e); - } - }; +export function toFetchRequest(url, data, options = {}) { + const method = options.method || (data ? POST : GET); + if (method === GET && data) { + const urlInfo = parseUrl(url, options); + Object.assign(urlInfo.search, data); + url = buildUrl(urlInfo); + } + const headers = new Headers(options.customHeaders); + headers.set(CTYPE, options.contentType || 'text/plain'); + const rqOpts = { + method, + headers + } + if (method !== GET && data) { + rqOpts.body = data; + } + if (options.withCredentials) { + rqOpts.credentials = 'include'; + } + if (options.browsingTopics && isSecureContext) { + // the Request constructor will throw an exception if the browser supports topics + // but we're not in a secure context + rqOpts.browsingTopics = true; + } + return dep.makeRequest(url, rqOpts); +} - if (typeof callback === 'function') { - callbacks.success = callback; - } +/** + * Return a version of `fetch` that automatically cancels requests after `timeout` milliseconds. + * + * If provided, `request` and `done` should be functions accepting a single argument. + * `request` is invoked at the beginning of each request, and `done` at the end; both are passed its origin. + * + * @returns {function(*, {}?): Promise} + */ +export function fetcherFactory(timeout = 3000, {request, done} = {}) { + let fetcher = (resource, options) => { + let to; + if (timeout != null && options?.signal == null && !config.getConfig('disableAjaxTimeout')) { + to = dep.timeout(timeout, resource); + options = Object.assign({signal: to.signal}, options); + } + let pm = dep.fetch(resource, options); + if (to?.done != null) pm = pm.finally(to.done); + return pm; + }; - x = new window.XMLHttpRequest(); + if (request != null || done != null) { + fetcher = ((fetch) => function (resource, options) { + const origin = new URL(resource?.url == null ? resource : resource.url, document.location).origin; + let req = fetch(resource, options); + request && request(origin); + if (done) req = req.finally(() => done(origin)); + return req; + })(fetcher); + } + return fetcher; +} - x.onreadystatechange = function () { - if (x.readyState === XHR_DONE) { - if (typeof done === 'function') { - done(parser.origin); - } - let status = x.status; - if ((status >= 200 && status < 300) || status === 304) { - callbacks.success(x.responseText, x); - } else { - callbacks.error(x.statusText, x); - } +function toXHR({status, statusText = '', headers, url}, responseText) { + let xml = 0; + return { + readyState: XMLHttpRequest.DONE, + status, + statusText, + responseText, + response: responseText, + responseType: '', + responseURL: url, + get responseXML() { + if (xml === 0) { + try { + xml = new DOMParser().parseFromString(responseText, headers?.get(CTYPE)?.split(';')?.[0]) + } catch (e) { + xml = null; + logError(e); } - }; - - // Disabled timeout temporarily to avoid xhr failed requests. https://github.com/prebid/Prebid.js/issues/2648 - if (!config.getConfig('disableAjaxTimeout')) { - x.ontimeout = function () { - logError(' xhr timeout after ', x.timeout, 'ms'); - }; } + return xml; + }, + getResponseHeader: (header) => headers?.has(header) ? headers.get(header) : null, + } +} - if (method === 'GET' && data) { - let urlInfo = parseUrl(url, options); - Object.assign(urlInfo.search, data); - url = buildUrl(urlInfo); - } - - x.open(method, url, true); - // IE needs timeout to be set after open - see #1410 - // Disabled timeout temporarily to avoid xhr failed requests. https://github.com/prebid/Prebid.js/issues/2648 - if (!config.getConfig('disableAjaxTimeout')) { - x.timeout = timeout; - } - - if (options.withCredentials) { - x.withCredentials = true; - } - _each(options.customHeaders, (value, header) => { - x.setRequestHeader(header, value); - }); - if (options.preflight) { - x.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - } - x.setRequestHeader('Content-Type', options.contentType || 'text/plain'); - - if (typeof request === 'function') { - request(parser.origin); - } +/** + * attach legacy `ajax` callbacks to a fetch promise. + */ +export function attachCallbacks(fetchPm, callback) { + const {success, error} = typeof callback === 'object' && callback != null ? callback : { + success: typeof callback === 'function' ? callback : () => null, + error: (e, x) => logError('Network error', e, x) + }; + fetchPm.then(response => response.text().then((responseText) => [response, responseText])) + .then(([response, responseText]) => { + const xhr = toXHR(response, responseText); + response.ok || response.status === 304 ? success(responseText, xhr) : error(response.statusText, xhr); + }, () => error('', toXHR({status: 0}, ''))); +} - if (method === 'POST' && data) { - x.send(data); - } else { - x.send(); - } - } catch (error) { - logError('xhr construction', error); - typeof callback === 'object' && callback !== null && callback.error(error); - } - } +export function ajaxBuilder(timeout = 3000, {request, done} = {}) { + const fetcher = fetcherFactory(timeout, {request, done}); + return function (url, callback, data, options = {}) { + attachCallbacks(fetcher(toFetchRequest(url, data, options)), callback); + }; } + +export const ajax = ajaxBuilder(); +export const fetch = fetcherFactory(); diff --git a/test/mocks/xhr.js b/test/mocks/xhr.js index 424100f870c..e7b1d96f0a4 100644 --- a/test/mocks/xhr.js +++ b/test/mocks/xhr.js @@ -1,12 +1,236 @@ import {getUniqueIdentifierStr} from '../../src/utils.js'; +import {GreedyPromise} from '../../src/utils/promise.js'; +import {fakeXhr} from 'nise'; +import {dep} from 'src/ajax.js'; -export let server = sinon.createFakeServer(); -export let xhr = global.XMLHttpRequest; +export const xhr = sinon.useFakeXMLHttpRequest(); +export const server = mockFetchServer(); -beforeEach(function() { - server.restore(); - server = sinon.createFakeServer(); - xhr = global.XMLHttpRequest; +/** + * An (incomplete) replica of nise's fakeServer, but backing fetch used in ajax.js (rather than XHR). + */ +function mockFetchServer() { + const sandbox = sinon.createSandbox(); + const bodies = new WeakMap(); + const requests = []; + const {DONE, UNSENT} = XMLHttpRequest; + + function makeRequest(resource, options) { + const requestBody = options?.body || bodies.get(resource); + const request = new Request(resource, options); + bodies.set(request, requestBody); + return request; + } + + function mockXHR(resource, options) { + let resolve, reject; + const promise = new GreedyPromise((res, rej) => { + resolve = res; + reject = rej; + }); + + function error(reason = new TypeError('Failed to fetch')) { + mockReq.status = 0; + reject(reason); + } + + const request = makeRequest(resource, options); + request.signal.onabort = () => error(new DOMException('The user aborted a request')); + let responseHeaders; + + const mockReq = { + fetch: { + request, + requestBody: bodies.get(request), + promise, + }, + readyState: UNSENT, + url: request.url, + method: request.method, + requestBody: bodies.get(request), + status: 0, + statusText: '', + requestHeaders: new Proxy(request.headers, { + get(target, prop) { + return typeof prop === 'string' && target.has(prop) ? target.get(prop) : {}[prop]; + }, + has(target, prop) { + return typeof prop === 'string' && target.has(prop); + }, + ownKeys(target) { + return Array.from(target.keys()); + }, + getOwnPropertyDescriptor(target, prop) { + if (typeof prop === 'string' && target.has(prop)) { + return { + enumerable: true, + configurable: true, + writable: false, + value: target.get(prop) + } + } + } + }), + withCredentials: request.credentials === 'include', + setStatus(status) { + // nise replaces invalid status with 200 + status = typeof status === 'number' ? status : 200; + mockReq.status = status; + mockReq.statusText = fakeXhr.FakeXMLHttpRequest.statusCodes[status] || ''; + }, + setResponseHeaders(headers) { + responseHeaders = headers; + }, + setResponseBody(body) { + if (mockReq.status === 0) { + error(); + return; + } + const resp = Object.defineProperties(new Response(body, { + status: mockReq.status, + statusText: mockReq.statusText, + headers: responseHeaders || {}, + }), { + url: { + get: () => mockReq.fetch.request.url, + } + }); + mockReq.readyState = DONE; + // tests expect respond() to run everything immediately, + // so make body available syncronously + resp.text = () => GreedyPromise.resolve(body || ''); + Object.assign(mockReq.fetch, { + response: resp, + responseBody: body || '' + }) + resolve(resp); + }, + respond(status = 200, headers, body) { + mockReq.setStatus(status); + mockReq.setResponseHeaders(headers); + mockReq.setResponseBody(body); + }, + error + }; + return mockReq; + } + + let enabled = false; + let timeoutsEnabled = false; + + function enable() { + if (!enabled) { + sandbox.stub(dep, 'fetch').callsFake((resource, options) => { + const req = mockXHR(resource, options); + requests.push(req); + return req.fetch.promise; + }); + sandbox.stub(dep, 'makeRequest').callsFake(makeRequest); + const timeout = dep.timeout; + sandbox.stub(dep, 'timeout').callsFake(function () { + if (timeoutsEnabled) { + return timeout.apply(null, arguments); + } else { + return {}; + } + }); + enabled = true; + } + } + + enable(); + + const responders = []; + + function respondWith() { + let response, urlMatcher, methodMatcher; + urlMatcher = methodMatcher = () => true; + switch (arguments.length) { + case 1: + ([response] = arguments); + break; + case 2: + ([urlMatcher, response] = arguments); + break; + case 3: + ([methodMatcher, urlMatcher, response] = arguments); + methodMatcher = ((toMatch) => (method) => method === toMatch)(methodMatcher); + break; + default: + throw new Error('Invalid respondWith invocation'); + } + if (typeof urlMatcher.exec === 'function') { + urlMatcher = ((rx) => (url) => rx.exec(url)?.slice(1))(urlMatcher); + } else if (typeof urlMatcher === 'string') { + urlMatcher = ((toMatch) => (url) => url === toMatch)(urlMatcher); + } + responders.push((req) => { + if (req.readyState !== DONE && methodMatcher(req.method)) { + const arg = urlMatcher(req.url); + if (arg) { + if (typeof response === 'function') { + response(req, ...(Array.isArray(arg) ? arg : [])); + } else if (typeof response === 'string') { + req.respond(200, null, response); + } else { + req.respond.apply(req, response); + } + } + } + }); + } + + function resetState() { + requests.length = 0; + responders.length = 0; + timeoutsEnabled = false; + } + + return { + requests, + enable, + restore() { + resetState(); + sandbox.restore(); + enabled = false; + }, + reset() { + sandbox.resetHistory(); + resetState(); + }, + respondWith, + respond() { + if (arguments.length > 0) { + respondWith.apply(null, arguments); + } + requests.forEach(req => { + for (let i = responders.length - 1; i >= 0; i--) { + responders[i](req); + if (req.readyState === DONE) break; + } + if (req.readyState !== DONE) { + req.respond(404, {}, ''); + } + }); + }, + /** + * the timeout mechanism is quite different between XHR and fetch + * by default, mocked fetch does not time out - to reflect fakeServer XHRs + * note that many tests will fire requests without caring or waiting for their response - + * if they are timed out later, during unrelated tests, the log messages might interfere with their + * assertions + */ + get autoTimeout() { + return timeoutsEnabled; + }, + set autoTimeout(val) { + timeoutsEnabled = !!val; + } + }; +} + +beforeEach(function () { + server.reset(); }); const bid = getUniqueIdentifierStr().substring(4); @@ -20,12 +244,35 @@ afterEach(function () { return (s) => s.split('\n').map(s => `${preamble} ${s}`).join('\n'); })(); + function format(obj, body = null) { + if (obj == null) return obj; + const fmt = {}; + let node = obj; + while (node != null) { + Object.keys(node).forEach((k) => { + const val = obj[k]; + if (typeof val !== 'function' && !fmt.hasOwnProperty(k)) { + fmt[k] = val; + } + }); + node = Object.getPrototypeOf(node); + } + if (obj.headers != null) { + fmt.headers = Object.fromEntries(obj.headers.entries()) + } + fmt.body = body; + return fmt; + } + - console.log(prepend(`XHR mock state after failure (for test '${this.currentTest.fullTitle()}'): ${server.requests.length} requests`)) + console.log(prepend(`XHR mock state after failure (for test '${this.currentTest.fullTitle()}'): ${server.requests.length} requests`)); server.requests.forEach((req, i) => { console.log(prepend(`Request #${i}:`)); - console.log(prepend(JSON.stringify(req, null, 2))); - }) + console.log(prepend(JSON.stringify({ + request: format(req.fetch.request, req.fetch.requestBody), + response: format(req.fetch.response, req.fetch.responseBody) + }, null, 2))); + }); } }); /* eslint-enable */ diff --git a/test/spec/modules/adlooxAdServerVideo_spec.js b/test/spec/modules/adlooxAdServerVideo_spec.js index a071c6bbe3f..58277bc830d 100644 --- a/test/spec/modules/adlooxAdServerVideo_spec.js +++ b/test/spec/modules/adlooxAdServerVideo_spec.js @@ -1,11 +1,11 @@ import adapterManager from 'src/adapterManager.js'; import analyticsAdapter from 'modules/adlooxAnalyticsAdapter.js'; -import { ajax } from 'src/ajax.js'; import { buildVideoUrl } from 'modules/adlooxAdServerVideo.js'; import { expect } from 'chai'; import * as events from 'src/events.js'; import { targeting } from 'src/targeting.js'; import * as utils from 'src/utils.js'; +import {server} from '../../mocks/xhr.js'; const analyticsAdapterName = 'adloox'; @@ -199,11 +199,9 @@ describe('Adloox Ad Server Video', function () { }); describe('process VAST', function () { - let server = null; let BID = null; let getWinningBidsStub; beforeEach(function () { - server = sinon.createFakeServer(); BID = utils.deepClone(bid); getWinningBidsStub = sinon.stub(targeting, 'getWinningBids') getWinningBidsStub.withArgs(adUnit.code).returns([ BID ]); @@ -212,8 +210,6 @@ describe('Adloox Ad Server Video', function () { getWinningBidsStub.restore(); getWinningBidsStub = undefined; BID = null; - server.restore(); - server = null; }); it('should return URL unchanged for non-VAST', function (done) { diff --git a/test/spec/modules/adlooxRtdProvider_spec.js b/test/spec/modules/adlooxRtdProvider_spec.js index 5b99789981f..0e26ef1afdb 100644 --- a/test/spec/modules/adlooxRtdProvider_spec.js +++ b/test/spec/modules/adlooxRtdProvider_spec.js @@ -1,12 +1,12 @@ import adapterManager from 'src/adapterManager.js'; import analyticsAdapter from 'modules/adlooxAnalyticsAdapter.js'; import {auctionManager} from 'src/auctionManager.js'; -import { config as _config } from 'src/config.js'; import { expect } from 'chai'; import * as events from 'src/events.js'; import * as prebidGlobal from 'src/prebidGlobal.js'; import { subModuleObj as rtdProvider } from 'modules/adlooxRtdProvider.js'; import * as utils from 'src/utils.js'; +import {server} from '../../mocks/xhr.js'; const analyticsAdapterName = 'adloox'; @@ -139,16 +139,12 @@ describe('Adloox RTD Provider', function () { expect(analyticsAdapter.context).is.null; }); - let server = null; let CONFIG = null; beforeEach(function () { - server = sinon.createFakeServer(); CONFIG = utils.deepClone(config); }); afterEach(function () { CONFIG = null; - server.restore(); - server = null; }); it('should fetch segments', function (done) { diff --git a/test/spec/modules/categoryTranslation_spec.js b/test/spec/modules/categoryTranslation_spec.js index 2301d6aab1b..d4f6aa66c7d 100644 --- a/test/spec/modules/categoryTranslation_spec.js +++ b/test/spec/modules/categoryTranslation_spec.js @@ -2,18 +2,16 @@ import { getAdserverCategoryHook, initTranslation, storage } from 'modules/categ import { config } from 'src/config.js'; import * as utils from 'src/utils.js'; import { expect } from 'chai'; +import {server} from '../../mocks/xhr.js'; describe('category translation', function () { - let fakeTranslationServer; let getLocalStorageStub; beforeEach(function () { - fakeTranslationServer = sinon.fakeServer.create(); getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); }); afterEach(function() { - fakeTranslationServer.reset(); getLocalStorageStub.restore(); config.resetConfig(); }); @@ -73,7 +71,7 @@ describe('category translation', function () { } })); initTranslation(); - expect(fakeTranslationServer.requests.length).to.equal(0); + expect(server.requests.length).to.equal(0); clock.restore(); }); @@ -86,15 +84,15 @@ describe('category translation', function () { } })); initTranslation(); - expect(fakeTranslationServer.requests.length).to.equal(1); + expect(server.requests.length).to.equal(1); clock.restore(); }); it('should use default mapping file if publisher has not defined in config', function () { getLocalStorageStub.returns(null); initTranslation('http://sample.com', 'somekey'); - expect(fakeTranslationServer.requests.length).to.equal(1); - expect(fakeTranslationServer.requests[0].url).to.equal('http://sample.com'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://sample.com/'); }); it('should use publisher defined mapping file', function () { @@ -105,7 +103,7 @@ describe('category translation', function () { }); getLocalStorageStub.returns(null); initTranslation('http://sample.com', 'somekey'); - expect(fakeTranslationServer.requests.length).to.equal(2); - expect(fakeTranslationServer.requests[0].url).to.equal('http://sample.com'); + expect(server.requests.length).to.equal(2); + expect(server.requests[0].url).to.equal('http://sample.com/'); }); }); diff --git a/test/spec/modules/conversantAnalyticsAdapter_spec.js b/test/spec/modules/conversantAnalyticsAdapter_spec.js index ce134f7f6af..f425535ce73 100644 --- a/test/spec/modules/conversantAnalyticsAdapter_spec.js +++ b/test/spec/modules/conversantAnalyticsAdapter_spec.js @@ -3,6 +3,7 @@ import {expect} from 'chai'; import {default as conversantAnalytics, CNVR_CONSTANTS, cnvrHelper} from 'modules/conversantAnalyticsAdapter'; import * as utils from 'src/utils.js'; import * as prebidGlobal from 'src/prebidGlobal'; +import {server} from '../../mocks/xhr.js'; import constants from 'src/constants.json' @@ -10,14 +11,13 @@ let events = require('src/events'); describe('Conversant analytics adapter tests', function() { let sandbox; // sinon sandbox to make restoring all stubbed objects easier - let xhr; // xhr stub from sinon for capturing data sent via ajax let clock; // clock stub from sinon to mock our cache cleanup interval let logInfoStub; const PREBID_VERSION = '1.2'; const SITE_ID = 108060; - let requests = []; + let requests; const DATESTAMP = Date.now(); const VALID_CONFIGURATION = { @@ -36,10 +36,9 @@ describe('Conversant analytics adapter tests', function() { }; beforeEach(function () { + requests = server.requests; sandbox = sinon.sandbox.create(); sandbox.stub(events, 'getEvents').returns([]); // need to stub this otherwise unwanted events seem to get fired during testing - xhr = sandbox.useFakeXMLHttpRequest(); // allows us to capture ajax requests - xhr.onCreate = function (req) { requests.push(req); }; // save ajax requests in a private array for testing purposes let getGlobalStub = { version: PREBID_VERSION, getUserIds: function() { // userIdTargeting.js init() gets called on AUCTION_END so we need to mock this function. @@ -60,7 +59,6 @@ describe('Conversant analytics adapter tests', function() { afterEach(function () { sandbox.restore(); - requests = []; // clean up any requests in our ajax request capture array. conversantAnalytics.disableAnalytics(); }); diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index 88c640e38cc..f7c2580f3f3 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -14,6 +14,7 @@ import { } from 'modules/currency.js'; import {createBid} from '../../../src/bidfactory.js'; import CONSTANTS from '../../../src/constants.json'; +import {server} from '../../mocks/xhr.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -30,12 +31,11 @@ describe('currency', function () { } beforeEach(function () { - fakeCurrencyFileServer = sinon.fakeServer.create(); + fakeCurrencyFileServer = server; ready.reset(); }); afterEach(function () { - fakeCurrencyFileServer.restore(); setConfig({}); }); diff --git a/test/spec/modules/dmdIdSystem_spec.js b/test/spec/modules/dmdIdSystem_spec.js index 3096a8e55f5..16c32f184a3 100644 --- a/test/spec/modules/dmdIdSystem_spec.js +++ b/test/spec/modules/dmdIdSystem_spec.js @@ -60,7 +60,7 @@ describe('Dmd ID System', function () { it('Should invoke callback with response from API call', function () { const callbackSpy = sinon.spy(); - const domain = utils.getWindowLocation() + const domain = utils.getWindowLocation().href; const callback = dmdIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; @@ -73,7 +73,7 @@ describe('Dmd ID System', function () { it('Should log error if API response is not valid', function () { const callbackSpy = sinon.spy(); - const domain = utils.getWindowLocation() + const domain = utils.getWindowLocation().href; const callback = dmdIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; diff --git a/test/spec/modules/euidIdSystem_spec.js b/test/spec/modules/euidIdSystem_spec.js index 9a016b0facd..4f6bacebe6a 100644 --- a/test/spec/modules/euidIdSystem_spec.js +++ b/test/spec/modules/euidIdSystem_spec.js @@ -1,13 +1,9 @@ -import {coreStorage, init, setSubmoduleRegistry, requestBidsHook} from 'modules/userId/index.js'; +import {coreStorage, init, setSubmoduleRegistry} from 'modules/userId/index.js'; import {config} from 'src/config.js'; -import * as utils from 'src/utils.js'; -import { euidIdSubmodule } from 'modules/euidIdSystem.js'; +import {euidIdSubmodule} from 'modules/euidIdSystem.js'; import 'modules/consentManagement.js'; import 'src/prebid.js'; -import { getGlobal } from 'src/prebidGlobal.js'; -import { server } from 'test/mocks/xhr.js'; -import { configureTimerInterceptors } from 'test/mocks/timers.js'; -import { cookieHelpers, runAuction, apiHelpers, setGdprApplies } from './uid2IdSystem_helpers.js'; +import {apiHelpers, cookieHelpers, runAuction, setGdprApplies} from './uid2IdSystem_helpers.js'; import {hook} from 'src/hook.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; @@ -32,12 +28,15 @@ const makePrebidConfig = (params = null, extraSettings = {}, debug = false) => ( const apiUrl = 'https://prod.euid.eu/v2/token/refresh'; const headers = { 'Content-Type': 'application/json' }; const makeSuccessResponseBody = () => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: refreshedToken } })); -const configureEuidResponse = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); const expectToken = (bid, token) => expect(bid?.userId ?? {}).to.deep.include(makeEuidIdentityContainer(token)); const expectNoIdentity = (bid) => expect(bid).to.not.haveOwnProperty('userId'); describe('EUID module', function() { let suiteSandbox, testSandbox, timerSpy, fullTestTitle, restoreSubtleToUndefined = false; + let server; + + const configureEuidResponse = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); + before(function() { uninstallGdprEnforcement(); hook.ready(); @@ -54,10 +53,12 @@ describe('EUID module', function() { if (restoreSubtleToUndefined) window.crypto.subtle = undefined; }); beforeEach(function() { + server = sinon.createFakeServer(); init(config); setSubmoduleRegistry([euidIdSubmodule]); }); afterEach(function() { + server.restore(); $$PREBID_GLOBAL$$.requestBids.removeAll(); config.resetConfig(); cookieHelpers.clearCookies(moduleCookieName, publisherCookieName); @@ -116,7 +117,7 @@ describe('EUID module', function() { const euidToken = apiHelpers.makeTokenResponse(initialToken, true, true); configureEuidResponse(200, makeSuccessResponseBody()); config.setConfig(makePrebidConfig({euidToken})); - apiHelpers.respondAfterDelay(1); + apiHelpers.respondAfterDelay(1, server); const bid = await runAuction(); expectToken(bid, refreshedToken); }); diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 152adba9d00..cb81c6f06de 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -621,7 +621,7 @@ describe('FeedAdAdapter', function () { expect(call.url).to.equal('https://api.feedad.com/1/prebid/web/events'); expect(JSON.parse(call.requestBody)).to.deep.equal(expectedData); expect(call.method).to.equal('POST'); - expect(call.requestHeaders).to.include({'Content-Type': 'application/json;charset=utf-8'}); + expect(call.requestHeaders).to.include({'Content-Type': 'application/json'}); }) }); }); diff --git a/test/spec/modules/geoedgeRtdProvider_spec.js b/test/spec/modules/geoedgeRtdProvider_spec.js index 2f2fc8e2775..96da2e3dbd7 100644 --- a/test/spec/modules/geoedgeRtdProvider_spec.js +++ b/test/spec/modules/geoedgeRtdProvider_spec.js @@ -1,8 +1,15 @@ import * as utils from '../../../src/utils.js'; -import { loadExternalScript } from '../../../src/adloader.js'; -import * as hook from '../../../src/hook.js' -import { beforeInit, geoedgeSubmodule, setWrapper, wrapper, htmlPlaceholder, WRAPPER_URL, getClientUrl, getInPageUrl } from '../../../modules/geoedgeRtdProvider.js'; -import { server } from '../../../test/mocks/xhr.js'; +import {loadExternalScript} from '../../../src/adloader.js'; +import { + geoedgeSubmodule, + getClientUrl, + getInPageUrl, + htmlPlaceholder, + setWrapper, + wrapper, + WRAPPER_URL +} from '../../../modules/geoedgeRtdProvider.js'; +import {server} from '../../../test/mocks/xhr.js'; import * as events from '../../../src/events.js'; import CONSTANTS from '../../../src/constants.json'; @@ -50,20 +57,6 @@ function mockMessageFromClient(key) { let mockWrapper = `${htmlPlaceholder}`; describe('Geoedge RTD module', function () { - describe('beforeInit', function () { - let submoduleStub; - - before(function () { - submoduleStub = sinon.stub(hook, 'submodule'); - }); - after(function () { - submoduleStub.restore(); - }); - it('should register RTD submodule provider', function () { - beforeInit(); - expect(submoduleStub.calledWith('realTimeData', geoedgeSubmodule)).to.equal(true); - }); - }); describe('submodule', function () { describe('name', function () { it('should be geoedge', function () { diff --git a/test/spec/modules/greenbidsRtdProvider_spec.js b/test/spec/modules/greenbidsRtdProvider_spec.js index 7cb6c10ce48..cd93e9013c0 100644 --- a/test/spec/modules/greenbidsRtdProvider_spec.js +++ b/test/spec/modules/greenbidsRtdProvider_spec.js @@ -6,18 +6,9 @@ import { import { greenbidsSubmodule } from 'modules/greenbidsRtdProvider.js'; +import {server} from '../../mocks/xhr.js'; describe('greenbidsRtdProvider', () => { - let server; - - beforeEach(() => { - server = sinon.createFakeServer(); - }); - - afterEach(() => { - server.restore(); - }); - const endPoint = 't.greenbids.ai'; const SAMPLE_MODULE_CONFIG = { @@ -137,7 +128,6 @@ describe('greenbidsRtdProvider', () => { {'Content-Type': 'application/json'}, JSON.stringify(SAMPLE_RESPONSE_ADUNITS) ); - done(); }, 50); setTimeout(() => { @@ -152,6 +142,7 @@ describe('greenbidsRtdProvider', () => { expect(requestBids.adUnits[1].bids.map((bid) => bid.bidder)).to.include('rubicon'); expect(requestBids.adUnits[1].bids.map((bid) => bid.bidder)).to.include('openx'); expect(callback.calledOnce).to.be.true; + done(); }, 60); }); }); @@ -195,7 +186,6 @@ describe('greenbidsRtdProvider', () => { {'Content-Type': 'application/json'}, JSON.stringify({'failure': 'fail'}) ); - done(); }, 50); setTimeout(() => { @@ -204,6 +194,7 @@ describe('greenbidsRtdProvider', () => { expect(requestBids.adUnits[0].bids).to.have.length(3); expect(requestBids.adUnits[1].bids).to.have.length(3); expect(callback.calledOnce).to.be.true; + done(); }, 60); }); }); diff --git a/test/spec/modules/hadronIdSystem_spec.js b/test/spec/modules/hadronIdSystem_spec.js index c998ef2cf14..cc0118d4659 100644 --- a/test/spec/modules/hadronIdSystem_spec.js +++ b/test/spec/modules/hadronIdSystem_spec.js @@ -47,7 +47,7 @@ describe('HadronIdSystem', function () { const callback = hadronIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; - expect(request.url).to.eq('https://hadronid.publync.com?partner_id=0&_it=prebid'); + expect(request.url).to.eq('https://hadronid.publync.com/?partner_id=0&_it=prebid'); request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ hadronId: 'testHadronId1' })); expect(callbackSpy.lastCall.lastArg).to.deep.equal({ id: { hadronId: 'testHadronId1' } }); }); diff --git a/test/spec/modules/id5AnalyticsAdapter_spec.js b/test/spec/modules/id5AnalyticsAdapter_spec.js index 83951c3a6e9..9cb7233ce7c 100644 --- a/test/spec/modules/id5AnalyticsAdapter_spec.js +++ b/test/spec/modules/id5AnalyticsAdapter_spec.js @@ -1,20 +1,18 @@ import adapterManager from '../../../src/adapterManager.js'; import id5AnalyticsAdapter from '../../../modules/id5AnalyticsAdapter.js'; import { expect } from 'chai'; -import sinon from 'sinon'; import * as events from '../../../src/events.js'; import constants from '../../../src/constants.json'; import { generateUUID } from '../../../src/utils.js'; +import {server} from '../../mocks/xhr.js'; const CONFIG_URL = 'https://api.id5-sync.com/analytics/12349/pbjs'; const INGEST_URL = 'https://test.me/ingest'; describe('ID5 analytics adapter', () => { - let server; let config; beforeEach(() => { - server = sinon.createFakeServer(); config = { options: { partnerId: 12349, @@ -22,10 +20,6 @@ describe('ID5 analytics adapter', () => { }; }); - afterEach(() => { - server.restore(); - }); - it('registers itself with the adapter manager', () => { const adapter = adapterManager.getAnalyticsAdapter('id5Analytics'); expect(adapter).to.exist; diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index 2d81c9b7b8d..dd284357abe 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -19,8 +19,8 @@ import {uspDataHandler} from 'src/adapterManager.js'; import 'src/prebid.js'; import {hook} from '../../../src/hook.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; - -let expect = require('chai').expect; +import {server} from '../../mocks/xhr.js'; +import {expect} from 'chai'; describe('ID5 ID System', function () { const ID5_MODULE_NAME = 'id5Id'; @@ -268,7 +268,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server and handle a valid response', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let config = getId5FetchConfig(); let submoduleResponse = callSubmoduleGetId(config, undefined, undefined); @@ -297,7 +297,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with gdpr data ', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let consentData = { gdprApplies: true, consentString: 'consentString', @@ -322,7 +322,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server without gdpr data when gdpr not applies ', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let consentData = { gdprApplies: false, consentString: 'consentString' @@ -347,7 +347,7 @@ describe('ID5 ID System', function () { it('should call the ID5 server with us privacy consent', function () { let usPrivacyString = '1YN-'; uspDataHandler.setConsentData(usPrivacyString) - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let consentData = { gdprApplies: true, consentString: 'consentString', @@ -371,7 +371,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with no signature field when no stored object', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); return xhrServerMock.expectFetchRequest() @@ -384,7 +384,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server for config with submodule config object', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let id5FetchConfig = getId5FetchConfig(); id5FetchConfig.params.extraParam = { x: 'X', @@ -408,7 +408,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server for config with partner id being a string', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let id5FetchConfig = getId5FetchConfig(); id5FetchConfig.params.partner = '173'; let submoduleResponse = callSubmoduleGetId(id5FetchConfig, undefined, undefined); @@ -426,7 +426,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server for config with overridden url', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let id5FetchConfig = getId5FetchConfig(); id5FetchConfig.params.configUrl = 'http://localhost/x/y/z' @@ -444,7 +444,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with additional data when provided', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); return xhrServerMock.expectConfigRequest() @@ -478,7 +478,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with extensions', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); return xhrServerMock.expectConfigRequest() @@ -515,7 +515,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with extensions fetched with POST', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); return xhrServerMock.expectConfigRequest() @@ -561,7 +561,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with signature field from stored object', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); return xhrServerMock.expectFetchRequest() @@ -574,7 +574,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with pd field when pd config is set', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) const pubData = 'b50ca08271795a8e7e4012813f23d505193d75c0f2e2bb99baa63aa822f66ed3'; let id5Config = getId5FetchConfig(); @@ -592,7 +592,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with no pd field when pd config is not set', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let id5Config = getId5FetchConfig(); id5Config.params.pd = undefined; @@ -608,7 +608,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with nb=1 when no stored value exists and reset after', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); @@ -626,7 +626,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with incremented nb when stored value exists and reset after', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) storeNbInCache(ID5_TEST_PARTNER_ID, 1); let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); @@ -644,7 +644,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with ab_testing object when abTesting is turned on', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let id5Config = getId5FetchConfig(); id5Config.params.abTesting = {enabled: true, controlGroupPct: 0.234} @@ -661,7 +661,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server without ab_testing object when abTesting is turned off', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let id5Config = getId5FetchConfig(); id5Config.params.abTesting = {enabled: false, controlGroupPct: 0.55} @@ -677,7 +677,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server without ab_testing when when abTesting is not set', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let id5Config = getId5FetchConfig(); let submoduleResponse = callSubmoduleGetId(id5Config, undefined, ID5_STORED_OBJ); @@ -692,7 +692,7 @@ describe('ID5 ID System', function () { }); it('should store the privacy object from the ID5 server response', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); const privacy = { @@ -714,7 +714,7 @@ describe('ID5 ID System', function () { }); it('should not store a privacy object if not part of ID5 server response', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) coreStorage.removeDataFromLocalStorage(ID5_PRIVACY_STORAGE_NAME); let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); @@ -760,7 +760,7 @@ describe('ID5 ID System', function () { [false, 0] ].forEach(function ([isEnabled, expectedValue]) { it(`should check localStorage availability and log in request. Available=${isEnabled}`, () => { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let config = getId5FetchConfig(); let submoduleResponse = callSubmoduleGetId(config, undefined, undefined); storage.localStorageIsEnabled.callsFake(() => isEnabled) @@ -878,7 +878,7 @@ describe('ID5 ID System', function () { }); it('should call ID5 servers with signature and incremented nb post auction if refresh needed', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let initialLocalStorageValue = JSON.stringify(ID5_STORED_OBJ); storeInLocalStorage(ID5_STORAGE_NAME, initialLocalStorageValue, 1); storeInLocalStorage(`${ID5_STORAGE_NAME}_last`, expDaysStr(-1), 1); diff --git a/test/spec/modules/identityLinkIdSystem_spec.js b/test/spec/modules/identityLinkIdSystem_spec.js index 52e9f9171d6..9777ebe5501 100644 --- a/test/spec/modules/identityLinkIdSystem_spec.js +++ b/test/spec/modules/identityLinkIdSystem_spec.js @@ -111,10 +111,8 @@ describe('IdentityLinkId tests', function () { request.respond( 204, responseHeader, - '' ); expect(callBackSpy.calledOnce).to.be.true; - expect(request.response).to.equal(''); expect(logErrorStub.calledOnce).to.not.be.true; }); diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index d5ffbf92d68..ef174af416b 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -169,10 +169,8 @@ describe('IntentIQ tests', function () { request.respond( 204, responseHeader, - '' ); expect(callBackSpy.calledOnce).to.be.true; - expect(request.response).to.equal(''); }); it('should log an error and continue to callback if ajax request errors', function () { diff --git a/test/spec/modules/invisiblyAnalyticsAdapter_spec.js b/test/spec/modules/invisiblyAnalyticsAdapter_spec.js index 2c460156318..a8828515ffd 100644 --- a/test/spec/modules/invisiblyAnalyticsAdapter_spec.js +++ b/test/spec/modules/invisiblyAnalyticsAdapter_spec.js @@ -1,6 +1,7 @@ import invisiblyAdapter from 'modules/invisiblyAnalyticsAdapter.js'; import { expect } from 'chai'; import {expectEvents} from '../../helpers/analytics.js'; +import {server} from '../../mocks/xhr.js'; let events = require('src/events'); let constants = require('src/constants.json'); @@ -169,11 +170,7 @@ describe('Invisibly Analytics Adapter test suite', function () { describe('Invisibly Analytic tests specs', function () { beforeEach(function () { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = (xhr) => { - requests.push(xhr); - }; + requests = server.requests; sinon.stub(events, 'getEvents').returns([]); sinon.spy(invisiblyAdapter, 'track'); }); @@ -182,7 +179,6 @@ describe('Invisibly Analytics Adapter test suite', function () { invisiblyAdapter.disableAnalytics(); events.getEvents.restore(); invisiblyAdapter.track.restore(); - xhr.restore(); }); describe('Send all events as & when they are captured', function () { diff --git a/test/spec/modules/jwplayerRtdProvider_spec.js b/test/spec/modules/jwplayerRtdProvider_spec.js index 5a38a971e09..4638595e0d6 100644 --- a/test/spec/modules/jwplayerRtdProvider_spec.js +++ b/test/spec/modules/jwplayerRtdProvider_spec.js @@ -1,8 +1,19 @@ -import { fetchTargetingForMediaId, getVatFromCache, extractPublisherParams, - formatTargetingResponse, getVatFromPlayer, enrichAdUnits, addTargetingToBid, - fetchTargetingInformation, jwplayerSubmodule, getContentId, getContentSegments, getContentData, addOrtbSiteContent } from 'modules/jwplayerRtdProvider.js'; -import { server } from 'test/mocks/xhr.js'; -import { config as prebidConfig } from 'src/config.js'; +import { + addOrtbSiteContent, + addTargetingToBid, + enrichAdUnits, + extractPublisherParams, + fetchTargetingForMediaId, + fetchTargetingInformation, + formatTargetingResponse, + getContentData, + getContentId, + getContentSegments, + getVatFromCache, + getVatFromPlayer, + jwplayerSubmodule +} from 'modules/jwplayerRtdProvider.js'; +import {server} from 'test/mocks/xhr.js'; import {deepClone} from '../../../src/utils.js'; describe('jwplayerRtdProvider', function() { @@ -223,9 +234,6 @@ describe('jwplayerRtdProvider', function() { describe('Get Bid Request Data', function () { it('executes immediately while request is active if player has item', function () { const bidRequestSpy = sinon.spy(); - const fakeServer = sinon.createFakeServer(); - fakeServer.respondImmediately = false; - fakeServer.autoRespond = false; fetchTargetingForMediaId(mediaIdWithSegment); @@ -255,7 +263,7 @@ describe('jwplayerRtdProvider', function() { jwplayerSubmodule.getBidRequestData({ adUnits: [adUnit] }, bidRequestSpy); expect(bidRequestSpy.calledOnce).to.be.true; expect(bid.rtd.jwplayer).to.have.deep.property('targeting', expectedTargeting); - fakeServer.respond(); + server.respond(); expect(bidRequestSpy.calledOnce).to.be.true; }); }); @@ -271,22 +279,17 @@ describe('jwplayerRtdProvider', function() { } }; let bidRequestSpy; - let fakeServer; let clock; beforeEach(function () { bidRequestSpy = sinon.spy(); - fakeServer = sinon.createFakeServer(); - fakeServer.respondImmediately = false; - fakeServer.autoRespond = false; - clock = sinon.useFakeTimers(); }); afterEach(function () { clock.restore(); - fakeServer.respond(); + server.respond(); }); it('adds targeting when pending request succeeds', function () { @@ -318,7 +321,7 @@ describe('jwplayerRtdProvider', function() { expect(bid1).to.not.have.property('rtd'); expect(bid2).to.not.have.property('rtd'); - const request = fakeServer.requests[0]; + const request = server.requests[0]; request.respond( 200, responseHeader, @@ -358,7 +361,7 @@ describe('jwplayerRtdProvider', function() { }, bids }; - const request = fakeServer.requests[0]; + const request = server.requests[0]; request.respond( 200, responseHeader, @@ -443,7 +446,7 @@ describe('jwplayerRtdProvider', function() { expect(bid1).to.not.have.property('rtd'); expect(bid2).to.not.have.property('rtd'); - const request = fakeServer.requests[0]; + const request = server.requests[0]; request.respond( 200, responseHeader, @@ -518,7 +521,7 @@ describe('jwplayerRtdProvider', function() { const bid1 = bids[0]; expect(bid1).to.not.have.property('rtd'); - const request = fakeServer.requests[0]; + const request = server.requests[0]; request.respond( 200, responseHeader, @@ -931,7 +934,6 @@ describe('jwplayerRtdProvider', function() { describe('Get Bid Request Data', function () { const validMediaIDs = ['media_ID_1', 'media_ID_2', 'media_ID_3']; let bidRequestSpy; - let fakeServer; let clock; let bidReqConfig; @@ -971,16 +973,12 @@ describe('jwplayerRtdProvider', function() { bidRequestSpy = sinon.spy(); - fakeServer = sinon.createFakeServer(); - fakeServer.respondImmediately = false; - fakeServer.autoRespond = false; - clock = sinon.useFakeTimers(); }); afterEach(function () { clock.restore(); - fakeServer.respond(); + server.respond(); }); it('executes callback immediately when ad units are missing', function () { @@ -1003,9 +1001,9 @@ describe('jwplayerRtdProvider', function() { jwplayerSubmodule.getBidRequestData(bidReqConfig, bidRequestSpy); expect(bidRequestSpy.notCalled).to.be.true; - const req1 = fakeServer.requests[0]; - const req2 = fakeServer.requests[1]; - const req3 = fakeServer.requests[2]; + const req1 = server.requests[0]; + const req2 = server.requests[1]; + const req3 = server.requests[2]; req1.respond(); expect(bidRequestSpy.notCalled).to.be.true; diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index ae63f19f46b..304ce2ed7a5 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -550,7 +550,7 @@ describe('magnite analytics adapter', function () { expect(server.requests.length).to.equal(1); let request = server.requests[0]; - expect(request.url).to.equal('//localhost:9999/event'); + expect(request.url).to.match(/\/\/localhost:9999\/event/); let message = JSON.parse(request.requestBody); @@ -724,7 +724,7 @@ describe('magnite analytics adapter', function () { expect(server.requests.length).to.equal(1); let request = server.requests[0]; - expect(request.url).to.equal('//localhost:9999/event'); + expect(request.url).to.match(/\/\/localhost:9999\/event/); let message = JSON.parse(request.requestBody); diff --git a/test/spec/modules/mgidRtdProvider_spec.js b/test/spec/modules/mgidRtdProvider_spec.js index 4f70b4d8b7c..996875649b6 100644 --- a/test/spec/modules/mgidRtdProvider_spec.js +++ b/test/spec/modules/mgidRtdProvider_spec.js @@ -1,16 +1,14 @@ import { mgidSubmodule, storage } from '../../../modules/mgidRtdProvider.js'; import {expect} from 'chai'; import * as refererDetection from '../../../src/refererDetection'; +import {server} from '../../mocks/xhr.js'; describe('Mgid RTD submodule', () => { - let server; let clock; let getRefererInfoStub; let getDataFromLocalStorageStub; beforeEach(() => { - server = sinon.fakeServer.create(); - clock = sinon.useFakeTimers(); getRefererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); @@ -22,7 +20,6 @@ describe('Mgid RTD submodule', () => { }); afterEach(() => { - server.restore(); clock.restore(); getRefererInfoStub.restore(); getDataFromLocalStorageStub.restore(); @@ -309,7 +306,6 @@ describe('Mgid RTD submodule', () => { server.requests[0].respond( 204, {'Content-Type': 'application/json'}, - '{}' ); assert.deepEqual(reqBidsConfigObj.ortb2Fragments.global, {}); diff --git a/test/spec/modules/oguryBidAdapter_spec.js b/test/spec/modules/oguryBidAdapter_spec.js index bbe53855094..ed358af19b6 100644 --- a/test/spec/modules/oguryBidAdapter_spec.js +++ b/test/spec/modules/oguryBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec } from 'modules/oguryBidAdapter'; import * as utils from 'src/utils.js'; +import {server} from '../../mocks/xhr.js'; const BID_URL = 'https://mweb-hb.presage.io/api/header-bidding-request'; const TIMEOUT_URL = 'https://ms-ads-monitoring-events.presage.io/bid_timeout' @@ -851,20 +852,11 @@ describe('OguryBidAdapter', function () { }); describe('onBidWon', function() { - const nurl = 'https://fakewinurl.test'; - let xhr; + const nurl = 'https://fakewinurl.test/'; let requests; beforeEach(function() { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = (xhr) => { - requests.push(xhr); - }; - }) - - afterEach(function() { - xhr.restore() + requests = server.requests; }) it('Should not create nurl request if bid is undefined', function() { @@ -932,21 +924,15 @@ describe('OguryBidAdapter', function () { }) describe('onTimeout', function () { - let xhr; let requests; beforeEach(function() { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = (xhr) => { + requests = server.requests; + server.onCreate = (xhr) => { requests.push(xhr); }; }) - afterEach(function() { - xhr.restore() - }) - it('should send on bid timeout notification', function() { const bid = { ad: 'cookies', diff --git a/test/spec/modules/ooloAnalyticsAdapter_spec.js b/test/spec/modules/ooloAnalyticsAdapter_spec.js index 2515c713b14..1224c3f0740 100644 --- a/test/spec/modules/ooloAnalyticsAdapter_spec.js +++ b/test/spec/modules/ooloAnalyticsAdapter_spec.js @@ -663,7 +663,7 @@ describe('oolo Prebid Analytic', () => { events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit }); - expect(server.requests[3].url).to.equal('https://pbjs.com') + expect(server.requests[3].url).to.equal('https://pbjs.com/') }) it('should send raw events based on server configuration', () => { diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index e4f06c8835f..1626d6f2c9d 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -6,7 +6,7 @@ import { resetWurlMap, s2sDefaultConfig } from 'modules/prebidServerBidAdapter/index.js'; -import adapterManager from 'src/adapterManager.js'; +import adapterManager, {PBS_ADAPTER_NAME} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import {deepAccess, deepClone, mergeDeep} from 'src/utils.js'; import {ajax} from 'src/ajax.js'; @@ -27,6 +27,7 @@ import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; import 'modules/fledgeForGpt.js'; import * as redactor from 'src/activities/redactor.js'; +import * as activityRules from 'src/activities/rules.js'; import {hook} from '../../../src/hook.js'; import {decorateAdUnitsWithNativeParams} from '../../../src/native.js'; import {auctionManager} from '../../../src/auctionManager.js'; @@ -35,6 +36,10 @@ import {addComponentAuction, registerBidder} from 'src/adapters/bidderFactory.js import {getGlobal} from '../../../src/prebidGlobal.js'; import {syncAddFPDEnrichments, syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; import {deepSetValue} from '../../../src/utils.js'; +import {sandbox} from 'sinon'; +import {ACTIVITY_TRANSMIT_UFPD} from '../../../src/activities/activities.js'; +import {activityParams} from '../../../src/activities/activityParams.js'; +import {MODULE_TYPE_PREBID} from '../../../src/activities/modules.js'; let CONFIG = { accountId: '1', @@ -734,13 +739,39 @@ describe('S2S Adapter', function () { }) }) + describe('browsingTopics', () => { + const sandbox = sinon.createSandbox(); + afterEach(() => { + sandbox.restore() + }); + Object.entries({ + 'allowed': true, + 'not allowed': false, + }).forEach(([t, allow]) => { + it(`should be set to ${allow} when transmitUfpd is ${t}`, () => { + sandbox.stub(activityRules, 'isActivityAllowed').callsFake((activity, params) => { + if (activity === ACTIVITY_TRANSMIT_UFPD && params.component === `${MODULE_TYPE_PREBID}.${PBS_ADAPTER_NAME}`) { + return allow; + } + return false; + }); + config.setConfig({s2sConfig: CONFIG}); + const ajax = sinon.stub(); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + sinon.assert.calledWith(ajax, sinon.match.any, sinon.match.any, sinon.match.any, sinon.match({ + browsingTopics: allow + })); + }); + }); + }) + it('should set tmax to s2sConfig.timeout', () => { const cfg = {...CONFIG, timeout: 123}; config.setConfig({s2sConfig: cfg}); adapter.callBids({...REQUEST, s2sConfig: cfg}, BID_REQUESTS, addBidResponse, done, ajax); const req = JSON.parse(server.requests[0].requestBody); expect(req.tmax).to.eql(123); - }) + }); it('should block request if config did not define p1Consent URL in endpoint object config', function () { let badConfig = utils.deepClone(CONFIG); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index 64c871308a9..e2b8ca38792 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -22,6 +22,7 @@ import {auctionManager} from '../../../src/auctionManager.js'; import {stubAuctionIndex} from '../../helpers/indexStub.js'; import {guardTids} from '../../../src/adapters/bidderFactory.js'; import * as activities from '../../../src/activities/rules.js'; +import {server} from '../../mocks/xhr.js'; describe('the price floors module', function () { let logErrorSpy; @@ -593,16 +594,11 @@ describe('the price floors module', function () { adUnits, }); }; - let fakeFloorProvider; let actualAllowedFields = allowedFields; let actualFieldMatchingFunctions = fieldMatchingFunctions; const defaultAllowedFields = [...allowedFields]; const defaultMatchingFunctions = {...fieldMatchingFunctions}; - beforeEach(function() { - fakeFloorProvider = sinon.fakeServer.create(); - }); afterEach(function() { - fakeFloorProvider.restore(); exposedAdUnits = undefined; actualAllowedFields = [...defaultAllowedFields]; actualFieldMatchingFunctions = {...defaultMatchingFunctions}; @@ -986,7 +982,7 @@ describe('the price floors module', function () { }); }); it('Should continue auction of delay is hit without a response from floor provider', function () { - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json//'}}); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1013,7 +1009,7 @@ describe('the price floors module', function () { fetchStatus: 'timeout', floorProvider: undefined }); - fakeFloorProvider.respond(); + server.respond(); }); it('It should fetch if config has url and bidRequests have fetch level flooring meta data', function () { // init the fake server with response stuff @@ -1021,14 +1017,14 @@ describe('the price floors module', function () { ...basicFloorData, modelVersion: 'fetch model name', // change the model name }; - fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + server.respondWith(JSON.stringify(fetchFloorData)); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // floor provider should be called - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1037,7 +1033,7 @@ describe('the price floors module', function () { expect(exposedAdUnits).to.be.undefined; // make the fetch respond - fakeFloorProvider.respond(); + server.respond(); expect(exposedAdUnits).to.not.be.undefined; // the exposedAdUnits should be from the fetch not setConfig level data @@ -1061,14 +1057,14 @@ describe('the price floors module', function () { floorProvider: 'floorProviderD', // change the floor provider modelVersion: 'fetch model name', // change the model name }; - fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + server.respondWith(JSON.stringify(fetchFloorData)); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorproviderC', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorproviderC', auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // floor provider should be called - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1077,7 +1073,7 @@ describe('the price floors module', function () { expect(exposedAdUnits).to.be.undefined; // make the fetch respond - fakeFloorProvider.respond(); + server.respond(); // the exposedAdUnits should be from the fetch not setConfig level data // and fetchStatus is success since fetch worked @@ -1102,14 +1098,14 @@ describe('the price floors module', function () { modelVersion: 'fetch model name', // change the model name }; fetchFloorData.skipRate = 95; - fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + server.respondWith(JSON.stringify(fetchFloorData)); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // floor provider should be called - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1118,7 +1114,7 @@ describe('the price floors module', function () { expect(exposedAdUnits).to.be.undefined; // make the fetch respond - fakeFloorProvider.respond(); + server.respond(); expect(exposedAdUnits).to.not.be.undefined; // the exposedAdUnits should be from the fetch not setConfig level data @@ -1137,10 +1133,10 @@ describe('the price floors module', function () { }); it('Should not break if floor provider returns 404', function () { // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // run the auction and make server respond with 404 - fakeFloorProvider.respond(); + server.respond(); runStandardAuction(); // error should have been called for fetch error @@ -1160,13 +1156,13 @@ describe('the price floors module', function () { }); }); it('Should not break if floor provider returns non json', function () { - fakeFloorProvider.respondWith('Not valid response'); + server.respondWith('Not valid response'); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // run the auction and make server respond - fakeFloorProvider.respond(); + server.respond(); runStandardAuction(); // error should have been called for response floor data not being valid @@ -1187,27 +1183,27 @@ describe('the price floors module', function () { }); it('should handle not using fetch correctly', function () { // run setConfig twice indicating fetch - fakeFloorProvider.respondWith(JSON.stringify(basicFloorData)); - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + server.respondWith(JSON.stringify(basicFloorData)); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // log warn should be called and server only should have one request expect(logWarnSpy.calledOnce).to.equal(true); - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // now we respond and then run again it should work and make another request - fakeFloorProvider.respond(); - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); - fakeFloorProvider.respond(); + server.respond(); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); + server.respond(); // now warn still only called once and server called twice expect(logWarnSpy.calledOnce).to.equal(true); - expect(fakeFloorProvider.requests.length).to.equal(2); + expect(server.requests.length).to.equal(2); // should log error if method is not GET for now expect(logErrorSpy.calledOnce).to.equal(false); - handleSetFloorsConfig({...basicFloorConfig, endpoint: {url: 'http://www.fakeFloorProvider.json', method: 'POST'}}); + handleSetFloorsConfig({...basicFloorConfig, endpoint: {url: 'http://www.fakefloorprovider.json/', method: 'POST'}}); expect(logErrorSpy.calledOnce).to.equal(true); }); describe('isFloorsDataValid', function () { diff --git a/test/spec/modules/publinkIdSystem_spec.js b/test/spec/modules/publinkIdSystem_spec.js index 7d98b724bd8..f35a7453403 100644 --- a/test/spec/modules/publinkIdSystem_spec.js +++ b/test/spec/modules/publinkIdSystem_spec.js @@ -120,7 +120,7 @@ describe('PublinkIdSystem', () => { expect(parsed.search.mpn).to.equal('Prebid.js'); expect(parsed.search.mpv).to.equal('$prebid.version$'); - request.respond(204, {}, JSON.stringify(serverResponse)); + request.respond(204); expect(callbackSpy.called).to.be.false; }); diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index 8e9580839d8..3a79efbfc1c 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -1,11 +1,10 @@ -import pubmaticAnalyticsAdapter, { getMetadata } from 'modules/pubmaticAnalyticsAdapter.js'; +import pubmaticAnalyticsAdapter, {getMetadata} from 'modules/pubmaticAnalyticsAdapter.js'; import adapterManager from 'src/adapterManager.js'; import CONSTANTS from 'src/constants.json'; -import { config } from 'src/config.js'; -import { - setConfig, - addBidResponseHook, -} from 'modules/currency.js'; +import {config} from 'src/config.js'; +import {setConfig} from 'modules/currency.js'; +import {server} from '../../mocks/xhr.js'; +import 'src/prebid.js'; let events = require('src/events'); let ajax = require('src/ajax'); @@ -273,7 +272,6 @@ function getLoggerJsonFromRequest(requestBody) { describe('pubmatic analytics adapter', function () { let sandbox; - let xhr; let requests; let oldScreen; let clock; @@ -282,9 +280,7 @@ describe('pubmatic analytics adapter', function () { setUADefault(); sandbox = sinon.sandbox.create(); - xhr = sandbox.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); + requests = server.requests; sandbox.stub(events, 'getEvents').returns([]); diff --git a/test/spec/modules/pubwiseAnalyticsAdapter_spec.js b/test/spec/modules/pubwiseAnalyticsAdapter_spec.js index e14582edc39..92d5972cc13 100644 --- a/test/spec/modules/pubwiseAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubwiseAnalyticsAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import pubwiseAnalytics from 'modules/pubwiseAnalyticsAdapter.js'; import {expectEvents} from '../../helpers/analytics.js'; +import {server} from '../../mocks/xhr.js'; let events = require('src/events'); let adapterManager = require('src/adapterManager').default; @@ -9,7 +10,6 @@ let constants = require('src/constants.json'); describe('PubWise Prebid Analytics', function () { let requests; let sandbox; - let xhr; let clock; let mock = {}; @@ -38,9 +38,7 @@ describe('PubWise Prebid Analytics', function () { clock = sandbox.useFakeTimers(); sandbox.stub(events, 'getEvents').returns([]); - xhr = sandbox.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); + requests = server.requests; }); afterEach(function () { @@ -50,10 +48,6 @@ describe('PubWise Prebid Analytics', function () { }); describe('enableAnalytics', function () { - beforeEach(function () { - requests = []; - }); - it('should catch all events', function () { pubwiseAnalytics.enableAnalytics(mock.DEFAULT_PW_CONFIG); diff --git a/test/spec/modules/teadsIdSystem_spec.js b/test/spec/modules/teadsIdSystem_spec.js index 7b977e2fb2b..1959b990957 100644 --- a/test/spec/modules/teadsIdSystem_spec.js +++ b/test/spec/modules/teadsIdSystem_spec.js @@ -218,7 +218,7 @@ describe('TeadsIdSystem', function () { callback(callbackSpy); const request = server.requests[0]; expect(request.url).to.include(teadsUrl); - request.respond(204, null, 'Unavailable'); + request.respond(204); expect(logInfoStub.calledOnce).to.be.true; }); diff --git a/test/spec/modules/uid2IdSystem_helpers.js b/test/spec/modules/uid2IdSystem_helpers.js index 65d52c1d7c3..5006a50dedd 100644 --- a/test/spec/modules/uid2IdSystem_helpers.js +++ b/test/spec/modules/uid2IdSystem_helpers.js @@ -1,6 +1,6 @@ -import { setConsentConfig } from 'modules/consentManagement.js'; -import { server } from 'test/mocks/xhr.js'; -import {coreStorage, init, setSubmoduleRegistry, requestBidsHook} from 'modules/userId/index.js'; +import {setConsentConfig} from 'modules/consentManagement.js'; +import {server} from 'test/mocks/xhr.js'; +import {coreStorage, requestBidsHook} from 'modules/userId/index.js'; const msIn12Hours = 60 * 60 * 12 * 1000; const expireCookieDate = 'Thu, 01 Jan 1970 00:00:01 GMT'; @@ -34,8 +34,8 @@ export const apiHelpers = { refresh_expires: Date.now() + 24 * 60 * 60 * 1000, // 24 hours refresh_response_key: 'wR5t6HKMfJ2r4J7fEGX9Gw==', // Fake data }), - respondAfterDelay: (delay) => new Promise((resolve) => setTimeout(() => { - server.respond(); + respondAfterDelay: (delay, srv = server) => new Promise((resolve) => setTimeout(() => { + srv.respond(); setTimeout(() => resolve()); }, delay)), } diff --git a/test/spec/modules/uid2IdSystem_spec.js b/test/spec/modules/uid2IdSystem_spec.js index 20a38a292bb..f33060869df 100644 --- a/test/spec/modules/uid2IdSystem_spec.js +++ b/test/spec/modules/uid2IdSystem_spec.js @@ -7,7 +7,6 @@ import { uid2IdSubmodule } from 'modules/uid2IdSystem.js'; import 'src/prebid.js'; import 'modules/consentManagement.js'; import { getGlobal } from 'src/prebidGlobal.js'; -import { server } from 'test/mocks/xhr.js'; import { configureTimerInterceptors } from 'test/mocks/timers.js'; import { cookieHelpers, runAuction, apiHelpers, setGdprApplies } from './uid2IdSystem_helpers.js'; import {hook} from 'src/hook.js'; @@ -56,22 +55,6 @@ const expectModuleStorageToContain = (initialIdentity, latestIdentity) => { const apiUrl = 'https://prod.uidapi.com/v2/token/refresh'; const headers = { 'Content-Type': 'application/json' }; const makeSuccessResponseBody = () => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: refreshedToken } })); -const configureUid2Response = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); -const configureUid2ApiSuccessResponse = () => configureUid2Response(200, makeSuccessResponseBody()); -const configureUid2ApiFailResponse = () => configureUid2Response(500, 'Error'); - -// Runs the provided test twice - once with a successful API mock, once with one which returns a server error -const testApiSuccessAndFailure = (act, testDescription, failTestDescription, only = false) => { - const testFn = only ? it.only : it; - testFn(`API responds successfully: ${testDescription}`, async function() { - configureUid2ApiSuccessResponse(); - await act(true); - }); - testFn(`API responds with an error: ${failTestDescription ?? testDescription}`, async function() { - configureUid2ApiFailResponse(); - await act(false); - }); -} const testCookieAndLocalStorage = (description, test, only = false) => { const describeFn = only ? describe.only : describe; @@ -93,7 +76,7 @@ const testCookieAndLocalStorage = (description, test, only = false) => { }; describe(`UID2 module`, function () { - let suiteSandbox, testSandbox, timerSpy, fullTestTitle, restoreSubtleToUndefined = false; + let server, suiteSandbox, testSandbox, timerSpy, fullTestTitle, restoreSubtleToUndefined = false; before(function () { timerSpy = configureTimerInterceptors(debugOutput); hook.ready(); @@ -116,13 +99,31 @@ describe(`UID2 module`, function () { if (restoreSubtleToUndefined) window.crypto.subtle = undefined; }); + const configureUid2Response = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); + const configureUid2ApiSuccessResponse = () => configureUid2Response(200, makeSuccessResponseBody()); + const configureUid2ApiFailResponse = () => configureUid2Response(500, 'Error'); + // Runs the provided test twice - once with a successful API mock, once with one which returns a server error + const testApiSuccessAndFailure = (act, testDescription, failTestDescription, only = false) => { + const testFn = only ? it.only : it; + testFn(`API responds successfully: ${testDescription}`, async function() { + configureUid2ApiSuccessResponse(); + await act(true); + }); + testFn(`API responds with an error: ${failTestDescription ?? testDescription}`, async function() { + configureUid2ApiFailResponse(); + await act(false); + }); + } + const getFullTestTitle = (test) => `${test.parent.title ? getFullTestTitle(test.parent) + ' | ' : ''}${test.title}`; + beforeEach(function () { debugOutput(`----------------- START TEST ------------------`); fullTestTitle = getFullTestTitle(this.test.ctx.currentTest); debugOutput(fullTestTitle); testSandbox = sinon.sandbox.create(); testSandbox.stub(utils, 'logWarn'); + server = sinon.createFakeServer(); init(config); setSubmoduleRegistry([uid2IdSubmodule]); @@ -143,7 +144,6 @@ describe(`UID2 module`, function () { } cookieHelpers.clearCookies(moduleCookieName, publisherCookieName); coreStorage.removeDataFromLocalStorage(moduleCookieName); - debugOutput('----------------- END TEST ------------------'); }); @@ -247,7 +247,7 @@ describe(`UID2 module`, function () { describe('When the refresh is available in time', function() { testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); - apiHelpers.respondAfterDelay(auctionDelayMs / 10); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); const bid = await runAuction(); if (apiSucceeds) expectToken(bid, refreshedToken); @@ -256,7 +256,7 @@ describe(`UID2 module`, function () { testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); - apiHelpers.respondAfterDelay(auctionDelayMs / 10); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); await runAuction(); if (apiSucceeds) { @@ -275,7 +275,7 @@ describe(`UID2 module`, function () { testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); - const promise = apiHelpers.respondAfterDelay(auctionDelayMs * 2); + const promise = apiHelpers.respondAfterDelay(auctionDelayMs * 2, server); const bid = await runAuction(); expectNoIdentity(bid); @@ -319,13 +319,13 @@ describe(`UID2 module`, function () { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true), {auctionDelay: 0, syncDelay: 1}); }); testApiSuccessAndFailure(async function() { - apiHelpers.respondAfterDelay(10); + apiHelpers.respondAfterDelay(10, server); const bid = await runAuction(); expectToken(bid, initialToken); }, 'it should not be refreshed before the auction runs'); testApiSuccessAndFailure(async function(success) { - const promise = apiHelpers.respondAfterDelay(1); + const promise = apiHelpers.respondAfterDelay(1, server); await runAuction(); await promise; if (success) { diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index acc016a903d..68e42310c33 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -3005,7 +3005,7 @@ describe('User ID', function () { expect(server.requests).to.be.empty; return endAuction(); }).then(() => { - expect(server.requests[0].url).to.equal('/any/unifiedid/url'); + expect(server.requests[0].url).to.match(/\/any\/unifiedid\/url/); }); }); diff --git a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js index 15a1155f378..cbba815cfc1 100644 --- a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js @@ -1,7 +1,7 @@ import zetaAnalyticsAdapter from 'modules/zeta_global_sspAnalyticsAdapter.js'; import {config} from 'src/config'; import CONSTANTS from 'src/constants.json'; -import {logError} from '../../../src/utils'; +import {server} from '../../mocks/xhr.js'; let utils = require('src/utils'); let events = require('src/events'); @@ -358,14 +358,11 @@ const MOCK = { describe('Zeta Global SSP Analytics Adapter', function() { let sandbox; - let xhr; let requests; beforeEach(function() { sandbox = sinon.sandbox.create(); - requests = []; - xhr = sandbox.useFakeXMLHttpRequest(); - xhr.onCreate = request => requests.push(request); + requests = server.requests; sandbox.stub(events, 'getEvents').returns([]); }); diff --git a/test/spec/unit/core/ajax_spec.js b/test/spec/unit/core/ajax_spec.js new file mode 100644 index 00000000000..df0ce02c15c --- /dev/null +++ b/test/spec/unit/core/ajax_spec.js @@ -0,0 +1,403 @@ +import {dep, attachCallbacks, fetcherFactory, toFetchRequest} from '../../../../src/ajax.js'; +import {config} from 'src/config.js'; +import {server} from '../../../mocks/xhr.js'; +import {sandbox} from 'sinon'; + +const EXAMPLE_URL = 'https://www.example.com'; + +describe('fetcherFactory', () => { + let clock; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + server.autoTimeout = true; + }); + + afterEach(() => { + clock.runAll(); + clock.restore(); + config.resetConfig(); + }); + + Object.entries({ + 'URL': EXAMPLE_URL, + 'request object': new Request(EXAMPLE_URL) + }).forEach(([t, resource]) => { + it(`times out after timeout when fetching ${t}`, (done) => { + const fetch = fetcherFactory(1000); + const resp = fetch(resource); + clock.tick(900); + expect(server.requests[0].fetch.request.signal.aborted).to.be.false; + clock.tick(100); + expect(server.requests[0].fetch.request.signal.aborted).to.be.true; + resp.catch(() => done()); + }); + }); + + it('does not timeout after it completes', () => { + const fetch = fetcherFactory(1000); + const resp = fetch(EXAMPLE_URL); + server.requests[0].respond(); + return resp.then(() => { + clock.tick(2000); + expect(server.requests[0].fetch.request.signal.aborted).to.be.false; + }); + }); + + Object.entries({ + 'disableAjaxTimeout is set'() { + const fetcher = fetcherFactory(1000); + config.setConfig({disableAjaxTimeout: true}); + return fetcher; + }, + 'timeout is null'() { + return fetcherFactory(null); + }, + }).forEach(([t, mkFetcher]) => { + it(`does not timeout if ${t}`, (done) => { + const fetch = mkFetcher(); + const pm = fetch(EXAMPLE_URL); + clock.tick(2000); + server.requests[0].respond(); + pm.then(() => done()); + }); + }); + + Object.entries({ + 'local URL': ['/local.html', window.origin], + 'remote URL': [EXAMPLE_URL + '/remote.html', EXAMPLE_URL], + 'request with local URL': [new Request('/local.html'), window.origin], + 'request with remote URL': [new Request(EXAMPLE_URL + '/remote.html'), EXAMPLE_URL] + }).forEach(([t, [resource, expectedOrigin]]) => { + describe(`using ${t}`, () => { + it('calls request, passing origin', () => { + const request = sinon.stub(); + const fetch = fetcherFactory(1000, {request}); + fetch(resource); + sinon.assert.calledWith(request, expectedOrigin); + }); + + Object.entries({ + success: 'respond', + error: 'error' + }).forEach(([t, method]) => { + it(`calls done on ${t}, passing origin`, () => { + const done = sinon.stub(); + const fetch = fetcherFactory(1000, {done}); + const req = fetch(resource).catch(() => null).then(() => { + sinon.assert.calledWith(done, expectedOrigin); + }); + server.requests[0][method](); + return req; + }); + }); + }); + }); +}); + +describe('toFetchRequest', () => { + Object.entries({ + 'simple POST': { + url: EXAMPLE_URL, + data: 'data', + expect: { + request: { + url: EXAMPLE_URL + '/', + method: 'POST', + }, + text: 'data', + headers: { + 'content-type': 'text/plain' + } + } + }, + 'POST with headers': { + url: EXAMPLE_URL, + data: '{"json": "body"}', + options: { + contentType: 'application/json', + customHeaders: { + 'x-custom': 'value' + } + }, + expect: { + request: { + url: EXAMPLE_URL + '/', + method: 'POST', + }, + text: '{"json": "body"}', + headers: { + 'content-type': 'application/json', + 'X-Custom': 'value' + } + } + }, + 'simple GET': { + url: EXAMPLE_URL, + data: {p1: 'v1', p2: 'v2'}, + options: { + method: 'GET', + }, + expect: { + request: { + url: EXAMPLE_URL + '/?p1=v1&p2=v2', + method: 'GET' + }, + text: '', + headers: { + 'content-type': 'text/plain' + } + } + }, + 'GET with credentials': { + url: EXAMPLE_URL, + data: null, + options: { + method: 'GET', + withCredentials: true, + }, + expect: { + request: { + url: EXAMPLE_URL + '/', + method: 'GET', + credentials: 'include' + }, + text: '', + headers: { + 'content-type': 'text/plain' + } + } + } + }).forEach(([t, {url, data, options, expect: {request, text, headers}}]) => { + it(`can build ${t}`, () => { + const req = toFetchRequest(url, data, options); + return req.text().then(body => { + Object.entries(request).forEach(([prop, val]) => { + expect(req[prop]).to.eql(val); + }); + const hdr = new Headers(headers); + Array.from(req.headers.entries()).forEach(([name, val]) => { + expect(hdr.get(name)).to.eql(val); + }); + expect(body).to.eql(text); + }); + }); + }); + + describe('browsingTopics', () => { + Object.entries({ + 'browsingTopics = true': [{browsingTopics: true}, true], + 'browsingTopics = false': [{browsingTopics: false}, false], + 'browsingTopics is undef': [{}, false] + }).forEach(([t, [opts, shouldBeSet]]) => { + describe(`when options has ${t}`, () => { + const sandbox = sinon.createSandbox(); + afterEach(() => { + sandbox.restore(); + }); + + it(`should ${!shouldBeSet ? 'not ' : ''}be set when in a secure context`, () => { + sandbox.stub(window, 'isSecureContext').get(() => true); + toFetchRequest(EXAMPLE_URL, null, opts); + sinon.assert.calledWithMatch(dep.makeRequest, sinon.match.any, {browsingTopics: shouldBeSet ? true : undefined}); + }); + it(`should not be set when not in a secure context`, () => { + sandbox.stub(window, 'isSecureContext').get(() => false); + toFetchRequest(EXAMPLE_URL, null, opts); + sinon.assert.calledWithMatch(dep.makeRequest, sinon.match.any, {browsingTopics: undefined}); + }); + }) + }) + }) +}); + +describe('attachCallbacks', () => { + const sampleHeaders = new Headers({ + 'x-1': 'v1', + 'x-2': 'v2' + }); + + function responseFactory(body, props) { + props = Object.assign({headers: sampleHeaders, url: EXAMPLE_URL}, props); + return function () { + return { + response: Object.defineProperties(new Response(body, props), { + url: { + get: () => props.url + } + }), + body: body || '' + }; + }; + } + + function expectNullXHR(response) { + return new Promise((resolve, reject) => { + attachCallbacks(Promise.resolve(response), { + success: () => { + reject(new Error('should not succeed')); + }, + error(statusText, xhr) { + expect(statusText).to.eql(''); + sinon.assert.match(xhr, { + readyState: XMLHttpRequest.DONE, + status: 0, + statusText: '', + responseText: '', + response: '', + responseXML: null + }); + expect(xhr.getResponseHeader('any')).to.be.null; + resolve(); + } + }); + }); + } + + it('runs error callback on rejections', () => { + return expectNullXHR(Promise.reject(new Error())); + }); + + Object.entries({ + '2xx response': { + success: true, + makeResponse: responseFactory('body', {status: 200, statusText: 'OK'}) + }, + '2xx response with no body': { + success: true, + makeResponse: responseFactory(null, {status: 204, statusText: 'No content'}) + }, + '2xx response with XML': { + success: true, + xml: true, + makeResponse: responseFactory('', { + status: 200, + statusText: 'OK', + headers: {'content-type': 'application/xml;charset=UTF8'} + }) + }, + '2xx response with HTML': { + success: true, + xml: true, + makeResponse: responseFactory('

', { + status: 200, + statusText: 'OK', + headers: {'content-type': 'text/html;charset=UTF-8'} + }) + }, + '304 response': { + success: true, + makeResponse: responseFactory(null, {status: 304, statusText: 'Moved permanently'}) + }, + '4xx response': { + success: false, + makeResponse: responseFactory('body', {status: 400, statusText: 'Invalid request'}) + }, + '5xx response': { + success: false, + makeResponse: responseFactory('body', {status: 503, statusText: 'Gateway error'}) + }, + '4xx response with XML': { + success: false, + xml: true, + makeResponse: responseFactory('', { + status: 404, + statusText: 'Not found', + headers: { + 'content-type': 'application/xml' + } + }) + } + }).forEach(([t, {success, makeResponse, xml}]) => { + const cbType = success ? 'success' : 'error'; + + describe(`for ${t}`, () => { + let response, body; + beforeEach(() => { + ({response, body} = makeResponse()); + }); + + function checkXHR(xhr) { + sinon.assert.match(xhr, { + readyState: XMLHttpRequest.DONE, + status: response.status, + statusText: response.statusText, + responseType: '', + responseURL: response.url, + response: body, + responseText: body, + }); + if (xml) { + expect(xhr.responseXML.querySelectorAll('*').length > 0).to.be.true; + } else { + expect(xhr.responseXML).to.not.exist; + } + Array.from(response.headers.entries()).forEach(([name, value]) => { + expect(xhr.getResponseHeader(name)).to.eql(value); + }); + expect(xhr.getResponseHeader('$$missing-header')).to.be.null; + } + + it(`runs ${cbType} callback`, (done) => { + attachCallbacks(Promise.resolve(response), { + success(payload, xhr) { + expect(success).to.be.true; + expect(payload).to.eql(body); + checkXHR(xhr); + done(); + }, + error(statusText, xhr) { + expect(success).to.be.false; + expect(statusText).to.eql(response.statusText); + checkXHR(xhr); + done(); + } + }); + }); + + it(`runs error callback if body cannot be retrieved`, () => { + response.text = () => Promise.reject(new Error()); + return expectNullXHR(response); + }); + + if (success) { + it('accepts a single function as success callback', (done) => { + attachCallbacks(Promise.resolve(response), function (payload, xhr) { + expect(payload).to.eql(body); + checkXHR(xhr); + done(); + }) + }) + } + }); + }); + + describe('callback exceptions', () => { + Object.entries({ + success: responseFactory(null, {status: 204}), + error: responseFactory('', {status: 400}), + }).forEach(([cbType, makeResponse]) => { + it(`do not choke ${cbType} callbacks`, () => { + const {response} = makeResponse(); + return new Promise((resolve) => { + const result = {success: false, error: false}; + attachCallbacks(Promise.resolve(response), { + success() { + result.success = true; + throw new Error(); + }, + error() { + result.error = true; + throw new Error(); + } + }); + setTimeout(() => resolve(result), 20); + }).then(result => { + Object.entries(result).forEach(([typ, ran]) => { + expect(ran).to.be[typ === cbType ? 'true' : 'false'] + }) + }); + }); + }); + }); +}); diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 138ffcb608d..0360679ca52 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -14,7 +14,7 @@ import {bidderSettings} from '../../../../src/bidderSettings.js'; import {decorateAdUnitsWithNativeParams} from '../../../../src/native.js'; import * as activityRules from 'src/activities/rules.js'; import {MODULE_TYPE_BIDDER} from '../../../../src/activities/modules.js'; -import {ACTIVITY_TRANSMIT_TID} from '../../../../src/activities/activities.js'; +import {ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD} from '../../../../src/activities/activities.js'; const CODE = 'sampleBidder'; const MOCK_BIDS_REQUEST = { @@ -319,7 +319,7 @@ describe('bidders created by newBidder', function () { expect(ajaxStub.calledOnce).to.equal(true); expect(ajaxStub.firstCall.args[0]).to.equal(url); expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ + sinon.assert.match(ajaxStub.firstCall.args[3], { method: 'POST', contentType: 'text/plain', withCredentials: true @@ -344,11 +344,11 @@ describe('bidders created by newBidder', function () { expect(ajaxStub.calledOnce).to.equal(true); expect(ajaxStub.firstCall.args[0]).to.equal(url); expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ + sinon.assert.match(ajaxStub.firstCall.args[3], { method: 'POST', contentType: 'application/json', withCredentials: true - }); + }) }); it('should make the appropriate GET request', function () { @@ -367,10 +367,10 @@ describe('bidders created by newBidder', function () { expect(ajaxStub.calledOnce).to.equal(true); expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); expect(ajaxStub.firstCall.args[2]).to.be.undefined; - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ + sinon.assert.match(ajaxStub.firstCall.args[3], { method: 'GET', withCredentials: true - }); + }) }); it('should make the appropriate GET request when options are passed', function () { @@ -391,10 +391,10 @@ describe('bidders created by newBidder', function () { expect(ajaxStub.calledOnce).to.equal(true); expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); expect(ajaxStub.firstCall.args[2]).to.be.undefined; - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ + sinon.assert.match(ajaxStub.firstCall.args[3], { method: 'GET', withCredentials: false - }); + }) }); it('should make multiple calls if the spec returns them', function () { @@ -420,6 +420,78 @@ describe('bidders created by newBidder', function () { expect(ajaxStub.calledTwice).to.equal(true); }); + describe('browsingTopics ajax option', () => { + let transmitUfpdAllowed, bidder; + beforeEach(() => { + activityRules.isActivityAllowed.reset(); + activityRules.isActivityAllowed.callsFake((activity) => activity === ACTIVITY_TRANSMIT_UFPD ? transmitUfpdAllowed : true); + bidder = newBidder(spec); + spec.isBidRequestValid.returns(true); + }); + + it(`should be set to false when adapter sets browsingTopics = false`, () => { + transmitUfpdAllowed = true; + spec.buildRequests.returns([ + { + method: 'GET', + url: 'url', + options: { + browsingTopics: false + } + } + ]); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + sinon.assert.calledWith(ajaxStub, 'url', sinon.match.any, sinon.match.any, sinon.match({ + browsingTopics: false + })); + }); + + Object.entries({ + 'allowed': true, + 'not allowed': false + }).forEach(([t, allow]) => { + it(`should be set to ${allow} when transmitUfpd is ${t}`, () => { + transmitUfpdAllowed = allow; + spec.buildRequests.returns([ + { + method: 'GET', + url: '1', + }, + { + method: 'POST', + url: '2', + data: {} + }, + { + method: 'GET', + url: '3', + options: { + browsingTopics: true + } + }, + { + method: 'POST', + url: '4', + data: {}, + options: { + browsingTopics: true + } + } + ]); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + ['1', '2', '3', '4'].forEach(url => { + sinon.assert.calledWith( + ajaxStub, + url, + sinon.match.any, + sinon.match.any, + sinon.match({browsingTopics: allow}) + ); + }); + }); + }); + }); + it('should not add bids for each placement code if no requests are given', function () { const bidder = newBidder(spec); diff --git a/test/spec/videoCache_spec.js b/test/spec/videoCache_spec.js index c7c0b2eb329..c746fdd2afd 100644 --- a/test/spec/videoCache_spec.js +++ b/test/spec/videoCache_spec.js @@ -174,7 +174,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); let payload = { puts: [{ type: 'xml', @@ -224,7 +224,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); let payload = { puts: [{ type: 'xml', @@ -295,7 +295,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); let payload = { puts: [{ type: 'xml', @@ -356,7 +356,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); JSON.parse(request.requestBody).should.deep.equal({ puts: [{ From 934f5a96c9cded3f312bf9c332727925ded7ad6f Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Fri, 11 Aug 2023 12:13:09 -0400 Subject: [PATCH 49/88] Revert "fledgeForGpt: consolidate publisher configuration (#10136)" (#10352) This reverts commit 52996a6c7bdfa4b1c40d8631cc73dde7e5e3c0d8. --- modules/fledgeForGpt.js | 18 +--- modules/fledgeForGpt.md | 27 ++---- test/spec/modules/fledge_spec.js | 156 +++++++++---------------------- 3 files changed, 57 insertions(+), 144 deletions(-) diff --git a/modules/fledgeForGpt.js b/modules/fledgeForGpt.js index 1ee5b0b4a9e..f29ce7508d5 100644 --- a/modules/fledgeForGpt.js +++ b/modules/fledgeForGpt.js @@ -56,26 +56,18 @@ function isFledgeSupported() { export function markForFledge(next, bidderRequests) { if (isFledgeSupported()) { - const globalFledgeConfig = config.getConfig('fledgeForGpt'); - const bidders = globalFledgeConfig?.bidders ?? []; bidderRequests.forEach((req) => { - const useGlobalConfig = globalFledgeConfig?.enabled && (bidders.length == 0 || bidders.includes(req.bidderCode)); - Object.assign(req, config.runWithBidder(req.bidderCode, () => { - return { - fledgeEnabled: config.getConfig('fledgeEnabled') ?? (useGlobalConfig ? globalFledgeConfig.enabled : undefined), - defaultForSlots: config.getConfig('defaultForSlots') ?? (useGlobalConfig ? globalFledgeConfig?.defaultForSlots : undefined) - } - })); - }); + req.fledgeEnabled = config.runWithBidder(req.bidderCode, () => config.getConfig('fledgeEnabled')) + }) } next(bidderRequests); } getHook('makeBidRequests').after(markForFledge); export function setImpExtAe(imp, bidRequest, context) { - const impExt = imp.ext ?? {}; - impExt.ae = context.bidderRequest.fledgeEnabled ? (impExt.ae ?? context.bidderRequest.defaultForSlots) : undefined; - imp.ext = impExt; + if (!context.bidderRequest.fledgeEnabled) { + delete imp.ext?.ae; + } } registerOrtbProcessor({type: IMP, name: 'impExtAe', fn: setImpExtAe}); diff --git a/modules/fledgeForGpt.md b/modules/fledgeForGpt.md index 28f44da6459..3bb86cd5946 100644 --- a/modules/fledgeForGpt.md +++ b/modules/fledgeForGpt.md @@ -15,8 +15,8 @@ This is accomplished by adding the `fledgeForGpt` module to the list of modules gulp build --modules=fledgeForGpt,... ``` -Second, they must enable FLEDGE in their Prebid.js configuration. -This is done through module level configuration, but to provide a high degree of flexiblity for testing, FLEDGE settings also exist at the bidder level and slot level. +Second, they must enable FLEDGE in their Prebid.js configuration. To provide a high degree of flexiblity for testing, FLEDGE +settings exist at the module level, the bidder level, and the slot level. ### Module Configuration This module exposes the following settings: @@ -24,20 +24,15 @@ This module exposes the following settings: |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | |enabled | Boolean |Enable/disable the module |Defaults to `false` | -|bidders | Array[String] |Optional list of bidders |Defaults to all bidders | -|defaultForSlots | Number |Default value for `imp.ext.ae` in requests for specified bidders |Should be 1 | -As noted above, FLEDGE support is disabled by default. To enable it, set the `enabled` value to `true` for this module and configure `defaultForSlots` to be `1` (meaning _Client-side auction_). -using the `setConfig` method of Prebid.js. Optionally, a list of -bidders to apply these settings to may be provided: +As noted above, FLEDGE support is disabled by default. To enable it, set the `enabled` value to `true` for this module +using the `setConfig` method of Prebid.js: ```js pbjs.que.push(function() { pbjs.setConfig({ fledgeForGpt: { - enabled: true, - bidders: ['openx', 'rtbhouse'], - defaultForSlots: 1 + enabled: true } }); }); @@ -49,25 +44,23 @@ This module adds the following setting for bidders: |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | | fledgeEnabled | Boolean | Enable/disable a bidder to participate in FLEDGE | Defaults to `false` | -|defaultForSlots | Number |Default value for `imp.ext.ae` in requests for specified bidders |Should be 1| -Individual bidders may be further included or excluded here using the `setBidderConfig` method +In addition to enabling FLEDGE at the module level, individual bidders must also be enabled. This allows publishers to +selectively test with one or more bidders as they desire. To enable one or more bidders, use the `setBidderConfig` method of Prebid.js: ```js pbjs.setBidderConfig({ bidders: ["openx"], config: { - fledgeEnabled: true, - defaultForSlots: 1 + fledgeEnabled: true } }); ``` ### AdUnit Configuration -All adunits can be opted-in to FLEDGE in the global config via the `defaultForSlots` parameter. -If needed, adunits can be configured individually by setting an attribute of the `ortb2Imp` object for that -adunit. This attribute will take precedence over `defaultForSlots` setting. +Enabling an adunit for FLEDGE eligibility is accomplished by setting an attribute of the `ortb2Imp` object for that +adunit. |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | diff --git a/test/spec/modules/fledge_spec.js b/test/spec/modules/fledge_spec.js index 62f5962fe17..a81ff05596e 100644 --- a/test/spec/modules/fledge_spec.js +++ b/test/spec/modules/fledge_spec.js @@ -61,131 +61,59 @@ describe('fledgeEnabled', function () { config.resetConfig(); }); - const adUnits = [{ - 'code': '/19968336/header-bid-tag1', - 'mediaTypes': { - 'banner': { - 'sizes': [[728, 90]] - }, - }, - 'bids': [ - { - 'bidder': 'appnexus', - }, - { - 'bidder': 'rubicon', - }, - ] - }]; - - describe('with setBidderConfig()', () => { - it('should set fledgeEnabled correctly per bidder', function () { - config.setConfig({bidderSequence: 'fixed'}) - config.setBidderConfig({ - bidders: ['appnexus'], - config: { - fledgeEnabled: true, - defaultForSlots: 1, - } - }); - - const bidRequests = adapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); - - expect(bidRequests[0].bids[0].bidder).equals('appnexus'); - expect(bidRequests[0].fledgeEnabled).to.be.true; - expect(bidRequests[0].defaultForSlots).to.equal(1); - - expect(bidRequests[1].bids[0].bidder).equals('rubicon'); - expect(bidRequests[1].fledgeEnabled).to.be.undefined; - expect(bidRequests[1].defaultForSlots).to.be.undefined; - }); - }); - - describe('with setConfig()', () => { - it('should set fledgeEnabled correctly per bidder', function () { - config.setConfig({ - bidderSequence: 'fixed', - fledgeForGpt: { - enabled: true, - bidders: ['appnexus'], - defaultForSlots: 1, - } - }); - - const bidRequests = adapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); - - expect(bidRequests[0].bids[0].bidder).equals('appnexus'); - expect(bidRequests[0].fledgeEnabled).to.be.true; - expect(bidRequests[0].defaultForSlots).to.equal(1); - - expect(bidRequests[1].bids[0].bidder).equals('rubicon'); - expect(bidRequests[1].fledgeEnabled).to.be.undefined; - expect(bidRequests[1].defaultForSlots).to.be.undefined; + it('should set fledgeEnabled correctly per bidder', function () { + config.setConfig({bidderSequence: 'fixed'}) + config.setBidderConfig({ + bidders: ['appnexus'], + config: { + fledgeEnabled: true, + } }); - it('should set fledgeEnabled correctly for all bidders', function () { - config.setConfig({ - bidderSequence: 'fixed', - fledgeForGpt: { - enabled: true, - defaultForSlots: 1, - } - }); - - const bidRequests = adapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); - - expect(bidRequests[0].bids[0].bidder).equals('appnexus'); - expect(bidRequests[0].fledgeEnabled).to.be.true; - expect(bidRequests[0].defaultForSlots).to.equal(1); - - expect(bidRequests[1].bids[0].bidder).equals('rubicon'); - expect(bidRequests[0].fledgeEnabled).to.be.true; - expect(bidRequests[0].defaultForSlots).to.equal(1); - }); + const adUnits = [{ + 'code': '/19968336/header-bid-tag1', + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + }, + }, + 'bids': [ + { + 'bidder': 'appnexus', + }, + { + 'bidder': 'rubicon', + }, + ] + }]; + + const bidRequests = adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + + expect(bidRequests[0].bids[0].bidder).equals('appnexus'); + expect(bidRequests[0].fledgeEnabled).to.be.true; + + expect(bidRequests[1].bids[0].bidder).equals('rubicon'); + expect(bidRequests[1].fledgeEnabled).to.be.undefined; }); }); describe('ortb processors for fledge', () => { - describe('when defaultForSlots is set', () => { - it('imp.ext.ae should be set if fledge is enabled', () => { - const imp = {}; - setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true, defaultForSlots: 1}}); - expect(imp.ext.ae).to.equal(1); - }); - it('imp.ext.ae should be left intact if set on adunit and fledge is enabled', () => { - const imp = {ext: {ae: 2}}; - setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true, defaultForSlots: 1}}); - expect(imp.ext.ae).to.equal(2); - }); - }); - describe('when defaultForSlots is not defined', () => { - it('imp.ext.ae should be removed if fledge is not enabled', () => { + describe('imp.ext.ae', () => { + it('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}}; + it('should be left intact if fledge is enabled', () => { + const imp = {ext: {ae: false}}; setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true}}); - expect(imp.ext.ae).to.equal(2); + expect(imp.ext.ae).to.equal(false); }); }); describe('parseExtPrebidFledge', () => { From 1678443bd1890b843f486a130c71f4fc54fa6ea7 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Fri, 11 Aug 2023 19:37:16 -0400 Subject: [PATCH 50/88] Activity Controls GPP: invalidate covered = 0 in mspa (#10354) * Update activityControls.js * Update activityControls_spec.js --- libraries/mspa/activityControls.js | 4 +++- test/spec/libraries/mspa/activityControls_spec.js | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/libraries/mspa/activityControls.js b/libraries/mspa/activityControls.js index 9c8e393f064..d18f0617bfe 100644 --- a/libraries/mspa/activityControls.js +++ b/libraries/mspa/activityControls.js @@ -24,7 +24,9 @@ export function isBasicConsentDenied(cd) { // minors 13+ who have not given consent cd.KnownChildSensitiveDataConsents[0] === 1 || // minors under 13 cannot consent - isApplicable(cd.KnownChildSensitiveDataConsents[1]); + isApplicable(cd.KnownChildSensitiveDataConsents[1]) || + // covered cannot be zero + cd.MspaCoveredTransaction === 0; } export function sensitiveNoticeIs(cd, value) { diff --git a/test/spec/libraries/mspa/activityControls_spec.js b/test/spec/libraries/mspa/activityControls_spec.js index eab99dc43ee..247e405683a 100644 --- a/test/spec/libraries/mspa/activityControls_spec.js +++ b/test/spec/libraries/mspa/activityControls_spec.js @@ -35,6 +35,12 @@ describe('Consent interpretation', () => { })); expect(result).to.equal(true); }); + it('should be true (basic consent conditions do not pass) with covered set to zero (invalid state)', () => { + const result = isBasicConsentDenied(mkConsent({ + MspaCoveredTransaction: 0 + })); + expect(result).to.equal(true); + }); it('should not deny when consent for under-13 is null', () => { expect(isBasicConsentDenied(mkConsent({ KnownChildSensitiveDataConsents: [0, null] From a4218f109565eef372d523e2b2973a3bb3922525 Mon Sep 17 00:00:00 2001 From: southern-growthcode <79725079+southern-growthcode@users.noreply.github.com> Date: Mon, 14 Aug 2023 10:01:18 -0400 Subject: [PATCH 51/88] GrowthCode Analytics: Updates/BugFixes (#10339) * Update Markdown for growthCodeAnalytics modules * GC-85 Update URL endpoint in growthCodeAnalytics module * GC-84 added an addition event to send to server * GC-83 Added the passage of the GCID in the POST * Update growthCodeAnalytics module tests * Fixed event bug * Removed package-lock.json --- integrationExamples/gpt/growthcode.html | 15 +++------------ modules/growthCodeAnalyticsAdapter.js | 11 +++++++---- modules/growthCodeAnalyticsAdapter.md | 8 +------- .../modules/growthCodeAnalyticsAdapter_spec.js | 1 - 4 files changed, 11 insertions(+), 24 deletions(-) diff --git a/integrationExamples/gpt/growthcode.html b/integrationExamples/gpt/growthcode.html index d8ad6c4a5af..35de2b710ad 100644 --- a/integrationExamples/gpt/growthcode.html +++ b/integrationExamples/gpt/growthcode.html @@ -56,20 +56,11 @@ provider: 'growthCodeAnalytics', options: { pid: 'TEST01', + //url: 'http://localhost:8080/v3/pb/analytics', trackEvents: [ - 'auctionInit', 'auctionEnd', - 'bidAdjustment', - 'bidTimeout', - 'bidTimeout', - 'bidRequested', - 'bidResponse', - 'setTargeting', - 'requestBids', - 'addAdUnits', - 'noBid', 'bidWon', - 'bidderDone'] + ] } }); pbjs.setConfig({ @@ -80,7 +71,7 @@ auctionDelay: 1000, dataProviders: [{ name: 'growthCodeRtd', - waitForIt: true, + waitForIt: false, params: { pid: 'TEST01', } diff --git a/modules/growthCodeAnalyticsAdapter.js b/modules/growthCodeAnalyticsAdapter.js index 655f8af53a3..e7c572ae48a 100644 --- a/modules/growthCodeAnalyticsAdapter.js +++ b/modules/growthCodeAnalyticsAdapter.js @@ -13,7 +13,7 @@ import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_NAME = 'growthCodeAnalytics'; const DEFAULT_PID = 'INVALID_PID' -const ENDPOINT_URL = 'https://p2.gcprivacy.com/v1/pb/analytics' +const ENDPOINT_URL = 'https://p2.gcprivacy.com/v3/pb/analytics' export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_NAME}); @@ -138,12 +138,15 @@ growthCodeAnalyticsAdapter.enableAnalytics = function(conf = {}) { function logToServer() { if (pid === DEFAULT_PID) return; - if (eventQueue.length > 1) { + if (eventQueue.length >= 1) { + // Get the correct GCID + let gcid = localStorage.getItem('gcid') + let data = { session: sessionId, pid: pid, + gcid: gcid, timestamp: Date.now(), - timezoneoffset: new Date().getTimezoneOffset(), url: getRefererInfo().page, referer: document.referrer, events: eventQueue @@ -167,7 +170,7 @@ function sendEvent(event) { eventQueue.push(event); logInfo(MODULE_NAME + 'Analytics Event: ' + event); - if (event.eventType === CONSTANTS.EVENTS.AUCTION_END) { + if ((event.eventType === CONSTANTS.EVENTS.AUCTION_END) || (event.eventType === CONSTANTS.EVENTS.BID_WON)) { logToServer(); } } diff --git a/modules/growthCodeAnalyticsAdapter.md b/modules/growthCodeAnalyticsAdapter.md index e45cb2e9c62..6625d492ee6 100644 --- a/modules/growthCodeAnalyticsAdapter.md +++ b/modules/growthCodeAnalyticsAdapter.md @@ -21,13 +21,7 @@ pbjs.enableAnalytics({ pid: '', trackEvents: [ 'auctionEnd', - 'bidAdjustment', - 'bidTimeout', - 'bidRequested', - 'bidResponse', - 'noBid', - 'bidWon', - 'bidderDone'] + 'bidWon'] } }); ``` diff --git a/test/spec/modules/growthCodeAnalyticsAdapter_spec.js b/test/spec/modules/growthCodeAnalyticsAdapter_spec.js index e542a2641e8..cd9c12a729c 100644 --- a/test/spec/modules/growthCodeAnalyticsAdapter_spec.js +++ b/test/spec/modules/growthCodeAnalyticsAdapter_spec.js @@ -64,7 +64,6 @@ describe('growthCode analytics adapter', () => { var eventTypes = []; body.events.forEach(e => eventTypes.push(e.eventType)); assert(eventTypes.length > 0) - assert(eventTypes.indexOf(constants.EVENTS.AUCTION_END) > -1); growthCodeAnalyticsAdapter.disableAnalytics(); }); }); From d63c6278795e534f95a5013f133c1cc19fcc7986 Mon Sep 17 00:00:00 2001 From: Sam Ghitelman Date: Mon, 14 Aug 2023 11:00:44 -0400 Subject: [PATCH 52/88] ConcertBidAdapter: Add `browserLanguage` to request `meta` object (#10356) * collect EIDs for bid request * add ad slot positioning to payload * RPO-2012: Update local storage name-spacing for c_uid (#8) * Updates c_uid namespacing to be more specific for concert * fixes unit tests * remove console.log * RPO-2012: Add check for shared id (#9) * Adds check for sharedId * Updates cookie name * remove trailing comma * [RPO-3152] Enable Support for GPP Consent (#12) * Adds gpp consent integration to concert bid adapter * Update tests to check for gpp consent string param * removes user sync endpoint and tests * updates comment * cleans up consentAllowsPpid function * comment fix * rename variables for clarity * fixes conditional logic for consent allows function (#13) * [RPO-3262] Update getUid function to check for pubcid and sharedid (#14) * Update getUid function to check for pubcid and sharedid * updates adapter version * [RPO-3405] Add browserLanguage to request meta object --------- Co-authored-by: antoin Co-authored-by: Antoin Co-authored-by: Brett Bloxom <38990705+BrettBlox@users.noreply.github.com> --- modules/concertBidAdapter.js | 1 + test/spec/modules/concertBidAdapter_spec.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/concertBidAdapter.js b/modules/concertBidAdapter.js index bf4079322ff..7042c895bfb 100644 --- a/modules/concertBidAdapter.js +++ b/modules/concertBidAdapter.js @@ -41,6 +41,7 @@ export const spec = { prebidVersion: '$prebid.version$', pageUrl: bidderRequest.refererInfo.page, screen: [window.screen.width, window.screen.height].join('x'), + browserLanguage: window.navigator.language, debug: debugTurnedOn(), uid: getUid(bidderRequest, validBidRequests), optedOut: hasOptedOutOfPersonalization(), diff --git a/test/spec/modules/concertBidAdapter_spec.js b/test/spec/modules/concertBidAdapter_spec.js index 96c98e5e5a2..4a6a4f2ba60 100644 --- a/test/spec/modules/concertBidAdapter_spec.js +++ b/test/spec/modules/concertBidAdapter_spec.js @@ -116,7 +116,7 @@ describe('ConcertAdapter', function () { expect(payload).to.have.property('meta'); expect(payload).to.have.property('slots'); - const metaRequiredFields = ['prebidVersion', 'pageUrl', 'screen', 'debug', 'uid', 'optedOut', 'adapterVersion', 'uspConsent', 'gdprConsent', 'gppConsent']; + const metaRequiredFields = ['prebidVersion', 'pageUrl', 'screen', 'debug', 'uid', 'optedOut', 'adapterVersion', 'uspConsent', 'gdprConsent', 'gppConsent', 'browserLanguage']; const slotsRequiredFields = ['name', 'bidId', 'transactionId', 'sizes', 'partnerId', 'slotType']; metaRequiredFields.forEach(function(field) { From 64615dc138f6ca82fbf1ec7257e40a030ce7a46e Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Mon, 14 Aug 2023 16:18:13 -0700 Subject: [PATCH 53/88] fledgeForGpt: consolidate publisher configuration (#10360) * FLEDGE: option to configure all adUnits for specific bidders Resolves #10105 * Update doc * More tests and consistent values for fledgeEnabled * Delete ae instead of setting it to undef when fledge is not enabled * fix lint --------- Co-authored-by: Laurentiu Badea --- modules/fledgeForGpt.js | 21 +++- modules/fledgeForGpt.md | 27 ++++-- test/spec/modules/fledge_spec.js | 160 ++++++++++++++++++++++--------- 3 files changed, 151 insertions(+), 57 deletions(-) diff --git a/modules/fledgeForGpt.js b/modules/fledgeForGpt.js index f29ce7508d5..e592cd38044 100644 --- a/modules/fledgeForGpt.js +++ b/modules/fledgeForGpt.js @@ -20,12 +20,14 @@ export function init(cfg) { if (cfg && cfg.enabled === true) { if (!isEnabled) { getHook('addComponentAuction').before(addComponentAuctionHook); + getHook('makeBidRequests').after(markForFledge); 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() isEnabled = false; } logInfo(`${MODULE} disabled`, cfg); @@ -56,16 +58,27 @@ function isFledgeSupported() { export function markForFledge(next, bidderRequests) { if (isFledgeSupported()) { + const globalFledgeConfig = config.getConfig('fledgeForGpt'); + const bidders = globalFledgeConfig?.bidders ?? []; bidderRequests.forEach((req) => { - req.fledgeEnabled = config.runWithBidder(req.bidderCode, () => config.getConfig('fledgeEnabled')) - }) + const useGlobalConfig = globalFledgeConfig?.enabled && (bidders.length == 0 || bidders.includes(req.bidderCode)); + Object.assign(req, config.runWithBidder(req.bidderCode, () => { + return { + fledgeEnabled: config.getConfig('fledgeEnabled') ?? (useGlobalConfig ? globalFledgeConfig.enabled : undefined), + defaultForSlots: config.getConfig('defaultForSlots') ?? (useGlobalConfig ? globalFledgeConfig?.defaultForSlots : undefined) + } + })); + }); } next(bidderRequests); } -getHook('makeBidRequests').after(markForFledge); export function setImpExtAe(imp, bidRequest, context) { - if (!context.bidderRequest.fledgeEnabled) { + if (context.bidderRequest.fledgeEnabled) { + imp.ext = Object.assign(imp.ext || {}, { + ae: imp.ext?.ae ?? context.bidderRequest.defaultForSlots + }) + } else { delete imp.ext?.ae; } } diff --git a/modules/fledgeForGpt.md b/modules/fledgeForGpt.md index 3bb86cd5946..28f44da6459 100644 --- a/modules/fledgeForGpt.md +++ b/modules/fledgeForGpt.md @@ -15,8 +15,8 @@ This is accomplished by adding the `fledgeForGpt` module to the list of modules gulp build --modules=fledgeForGpt,... ``` -Second, they must enable FLEDGE in their Prebid.js configuration. To provide a high degree of flexiblity for testing, FLEDGE -settings exist at the module level, the bidder level, and the slot level. +Second, they must enable FLEDGE in their Prebid.js configuration. +This is done through module level configuration, but to provide a high degree of flexiblity for testing, FLEDGE settings also exist at the bidder level and slot level. ### Module Configuration This module exposes the following settings: @@ -24,15 +24,20 @@ This module exposes the following settings: |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | |enabled | Boolean |Enable/disable the module |Defaults to `false` | +|bidders | Array[String] |Optional list of bidders |Defaults to all bidders | +|defaultForSlots | Number |Default value for `imp.ext.ae` in requests for specified bidders |Should be 1 | -As noted above, FLEDGE support is disabled by default. To enable it, set the `enabled` value to `true` for this module -using the `setConfig` method of Prebid.js: +As noted above, FLEDGE support is disabled by default. To enable it, set the `enabled` value to `true` for this module and configure `defaultForSlots` to be `1` (meaning _Client-side auction_). +using the `setConfig` method of Prebid.js. Optionally, a list of +bidders to apply these settings to may be provided: ```js pbjs.que.push(function() { pbjs.setConfig({ fledgeForGpt: { - enabled: true + enabled: true, + bidders: ['openx', 'rtbhouse'], + defaultForSlots: 1 } }); }); @@ -44,23 +49,25 @@ This module adds the following setting for bidders: |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | | fledgeEnabled | Boolean | Enable/disable a bidder to participate in FLEDGE | Defaults to `false` | +|defaultForSlots | Number |Default value for `imp.ext.ae` in requests for specified bidders |Should be 1| -In addition to enabling FLEDGE at the module level, individual bidders must also be enabled. This allows publishers to -selectively test with one or more bidders as they desire. To enable one or more bidders, use the `setBidderConfig` method +Individual bidders may be further included or excluded here using the `setBidderConfig` method of Prebid.js: ```js pbjs.setBidderConfig({ bidders: ["openx"], config: { - fledgeEnabled: true + fledgeEnabled: true, + defaultForSlots: 1 } }); ``` ### AdUnit Configuration -Enabling an adunit for FLEDGE eligibility is accomplished by setting an attribute of the `ortb2Imp` object for that -adunit. +All adunits can be opted-in to FLEDGE in the global config via the `defaultForSlots` parameter. +If needed, adunits can be configured individually by setting an attribute of the `ortb2Imp` object for that +adunit. This attribute will take precedence over `defaultForSlots` setting. |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | diff --git a/test/spec/modules/fledge_spec.js b/test/spec/modules/fledge_spec.js index a81ff05596e..7a670fa2381 100644 --- a/test/spec/modules/fledge_spec.js +++ b/test/spec/modules/fledge_spec.js @@ -15,7 +15,9 @@ const AD_UNIT_CODE = 'mock/placement'; describe('fledgeForGpt module', function() { let nextFnSpy; - fledge.init({enabled: true}) + before(() => { + fledge.init({enabled: true}) + }); const bidRequest = { adUnitCode: AD_UNIT_CODE, @@ -61,59 +63,131 @@ describe('fledgeEnabled', function () { config.resetConfig(); }); - it('should set fledgeEnabled correctly per bidder', function () { - config.setConfig({bidderSequence: 'fixed'}) - config.setBidderConfig({ - bidders: ['appnexus'], - config: { - fledgeEnabled: true, - } + const adUnits = [{ + 'code': '/19968336/header-bid-tag1', + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + }, + }, + 'bids': [ + { + 'bidder': 'appnexus', + }, + { + 'bidder': 'rubicon', + }, + ] + }]; + + describe('with setBidderConfig()', () => { + it('should set fledgeEnabled correctly per bidder', function () { + config.setConfig({bidderSequence: 'fixed'}) + config.setBidderConfig({ + bidders: ['appnexus'], + config: { + fledgeEnabled: true, + defaultForSlots: 1, + } + }); + + const bidRequests = adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + + expect(bidRequests[0].bids[0].bidder).equals('appnexus'); + expect(bidRequests[0].fledgeEnabled).to.be.true; + expect(bidRequests[0].defaultForSlots).to.equal(1); + + expect(bidRequests[1].bids[0].bidder).equals('rubicon'); + expect(bidRequests[1].fledgeEnabled).to.be.undefined; + expect(bidRequests[1].defaultForSlots).to.be.undefined; }); + }); - const adUnits = [{ - 'code': '/19968336/header-bid-tag1', - 'mediaTypes': { - 'banner': { - 'sizes': [[728, 90]] - }, - }, - 'bids': [ - { - 'bidder': 'appnexus', - }, - { - 'bidder': 'rubicon', - }, - ] - }]; - - const bidRequests = adapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); - - expect(bidRequests[0].bids[0].bidder).equals('appnexus'); - expect(bidRequests[0].fledgeEnabled).to.be.true; - - expect(bidRequests[1].bids[0].bidder).equals('rubicon'); - expect(bidRequests[1].fledgeEnabled).to.be.undefined; + describe('with setConfig()', () => { + it('should set fledgeEnabled correctly per bidder', function () { + config.setConfig({ + bidderSequence: 'fixed', + fledgeForGpt: { + enabled: true, + bidders: ['appnexus'], + defaultForSlots: 1, + } + }); + + const bidRequests = adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + + expect(bidRequests[0].bids[0].bidder).equals('appnexus'); + expect(bidRequests[0].fledgeEnabled).to.be.true; + expect(bidRequests[0].defaultForSlots).to.equal(1); + + expect(bidRequests[1].bids[0].bidder).equals('rubicon'); + expect(bidRequests[1].fledgeEnabled).to.be.undefined; + expect(bidRequests[1].defaultForSlots).to.be.undefined; + }); + + it('should set fledgeEnabled correctly for all bidders', function () { + config.setConfig({ + bidderSequence: 'fixed', + fledgeForGpt: { + enabled: true, + defaultForSlots: 1, + } + }); + + const bidRequests = adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + + expect(bidRequests[0].bids[0].bidder).equals('appnexus'); + expect(bidRequests[0].fledgeEnabled).to.be.true; + expect(bidRequests[0].defaultForSlots).to.equal(1); + + expect(bidRequests[1].bids[0].bidder).equals('rubicon'); + expect(bidRequests[0].fledgeEnabled).to.be.true; + expect(bidRequests[0].defaultForSlots).to.equal(1); + }); }); }); describe('ortb processors for fledge', () => { - describe('imp.ext.ae', () => { - it('should be removed if fledge is not enabled', () => { + describe('when defaultForSlots is set', () => { + it('imp.ext.ae should be set if fledge is enabled', () => { + const imp = {}; + setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true, defaultForSlots: 1}}); + expect(imp.ext.ae).to.equal(1); + }); + it('imp.ext.ae should be left intact if set on adunit and fledge is enabled', () => { + const imp = {ext: {ae: 2}}; + setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true, defaultForSlots: 1}}); + expect(imp.ext.ae).to.equal(2); + }); + }); + describe('when defaultForSlots is not defined', () => { + 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('should be left intact if fledge is enabled', () => { - const imp = {ext: {ae: false}}; + 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(false); + expect(imp.ext.ae).to.equal(2); }); }); describe('parseExtPrebidFledge', () => { From c3839db393f162b8707d6684c706182ce8d4ff4f Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Tue, 15 Aug 2023 00:33:34 +0000 Subject: [PATCH 54/88] Prebid 8.9.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 5f36598a762..8c4fd6f7f1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.9.0-pre", + "version": "8.9.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 17541005a4c..6908766c653 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.9.0-pre", + "version": "8.9.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 59eac50afdaf050c15be1e95dda7da9de11e6133 Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Tue, 15 Aug 2023 00:33:35 +0000 Subject: [PATCH 55/88] Increment version to 8.10.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 8c4fd6f7f1f..fc856529601 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.9.0", + "version": "8.10.0-pre", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 6908766c653..74d83b6f5de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.9.0", + "version": "8.10.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 3cc9175874edc0e04c86fd7a3d2f2d6cf1b39a6c Mon Sep 17 00:00:00 2001 From: Yoko OYAMA Date: Tue, 15 Aug 2023 22:14:06 +0900 Subject: [PATCH 56/88] fluct Bid Adapter: add gpid to bid requests (#10361) * accept gpid * fallback gpid * test transactionId --- modules/fluctBidAdapter.js | 6 +- test/spec/modules/fluctBidAdapter_spec.js | 73 +++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/modules/fluctBidAdapter.js b/modules/fluctBidAdapter.js index edb750a6b90..b7cabfa95a0 100644 --- a/modules/fluctBidAdapter.js +++ b/modules/fluctBidAdapter.js @@ -43,16 +43,20 @@ export const spec = { const page = bidderRequest.refererInfo.page; _each(validBidRequests, (request) => { + const impExt = request.ortb2Imp?.ext; const data = Object(); data.page = page; data.adUnitCode = request.adUnitCode; data.bidId = request.bidId; - data.transactionId = request.ortb2Imp?.ext?.tid; data.user = { eids: (request.userIdAsEids || []).filter((eid) => SUPPORTED_USER_ID_SOURCES.indexOf(eid.source) !== -1) }; + if (impExt) { + data.transactionId = impExt.tid; + data.gpid = impExt.gpid ?? impExt.data?.pbadslot ?? impExt.data?.adserver?.adslot; + } if (bidderRequest.gdprConsent) { deepSetValue(data, 'regs.gdpr', { consent: bidderRequest.gdprConsent.consentString, diff --git a/test/spec/modules/fluctBidAdapter_spec.js b/test/spec/modules/fluctBidAdapter_spec.js index d970f70ad85..ca0f89da10d 100644 --- a/test/spec/modules/fluctBidAdapter_spec.js +++ b/test/spec/modules/fluctBidAdapter_spec.js @@ -99,6 +99,79 @@ describe('fluctAdapter', function () { expect(request.data.page).to.eql('http://example.com'); }); + it('sends no transactionId by default', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.transactionId).to.eql(undefined); + }); + + it('sends ortb2Imp.ext.tid as transactionId', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + tid: 'tid', + } + }, + })), bidderRequest)[0]; + expect(request.data.transactionId).to.eql('tid'); + }); + + it('sends no gpid by default', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.gpid).to.eql(undefined); + }); + + it('sends ortb2Imp.ext.gpid as gpid', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + gpid: 'gpid', + data: { + pbadslot: 'data-pbadslot', + adserver: { + adslot: 'data-adserver-adslot', + }, + }, + }, + }, + })), bidderRequest)[0]; + expect(request.data.gpid).to.eql('gpid'); + }); + + it('sends ortb2Imp.ext.data.pbadslot as gpid', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + data: { + pbadslot: 'data-pbadslot', + adserver: { + adslot: 'data-adserver-adslot', + }, + }, + }, + }, + })), bidderRequest)[0]; + expect(request.data.gpid).to.eql('data-pbadslot'); + }); + + it('sends ortb2Imp.ext.data.adserver.adslot as gpid', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + data: { + adserver: { + adslot: 'data-adserver-adslot', + }, + }, + }, + }, + })), bidderRequest)[0]; + expect(request.data.gpid).to.eql('data-adserver-adslot'); + }); + it('includes data.user.eids = [] by default', function () { const request = spec.buildRequests(bidRequests, bidderRequest)[0]; expect(request.data.user.eids).to.eql([]); From 507b5a0195ea1dbcc6d434cd2812ebd593b54e31 Mon Sep 17 00:00:00 2001 From: mamatic <52153441+mamatic@users.noreply.github.com> Date: Tue, 15 Aug 2023 15:29:16 +0200 Subject: [PATCH 57/88] identityLinkSubmodule: add additional check on retrieving the envelope (#10355) * IdentityLinkIdSystem - liveramp - add additional logic to get envelope from storage if ats is not present on a page * IdentityLinkIdSystem - liveramp - fix unit test --- modules/identityLinkIdSystem.js | 16 +++++- .../spec/modules/identityLinkIdSystem_spec.js | 53 ++++++++++++++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index e8cef34f41e..ba794df1a9c 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -15,6 +15,8 @@ const MODULE_NAME = 'identityLink'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); +const liverampEnvelopeName = '_lr_env'; + /** @type {Submodule} */ export const identityLinkSubmodule = { /** @@ -74,7 +76,14 @@ export const identityLinkSubmodule = { } }); } else { - getEnvelope(url, callback, configParams); + // try to get envelope directly from storage if ats lib is not present on a page + let envelope = getEnvelopeFromStorage(); + if (envelope) { + utils.logInfo('identityLink: LiveRamp envelope successfully retrieved from storage!'); + callback(JSON.parse(envelope).envelope); + } else { + getEnvelope(url, callback, configParams); + } } }; @@ -127,4 +136,9 @@ function setEnvelopeSource(src) { storage.setCookie('_lr_env_src_ats', src, now.toUTCString()); } +export function getEnvelopeFromStorage() { + let rawEnvelope = storage.getCookie(liverampEnvelopeName) || storage.getDataFromLocalStorage(liverampEnvelopeName); + return rawEnvelope ? window.atob(rawEnvelope) : undefined; +} + submodule('userId', identityLinkSubmodule); diff --git a/test/spec/modules/identityLinkIdSystem_spec.js b/test/spec/modules/identityLinkIdSystem_spec.js index 9777ebe5501..a273f26b28b 100644 --- a/test/spec/modules/identityLinkIdSystem_spec.js +++ b/test/spec/modules/identityLinkIdSystem_spec.js @@ -1,13 +1,22 @@ -import {identityLinkSubmodule} from 'modules/identityLinkIdSystem.js'; +import {getEnvelopeFromStorage, identityLinkSubmodule} from 'modules/identityLinkIdSystem.js'; import * as utils from 'src/utils.js'; import {server} from 'test/mocks/xhr.js'; import {getCoreStorageManager} from '../../../src/storageManager.js'; +import {stub} from 'sinon'; const storage = getCoreStorageManager(); const pid = '14'; let defaultConfigParams; -const responseHeader = {'Content-Type': 'application/json'} +const responseHeader = {'Content-Type': 'application/json'}; +const testEnvelope = 'eyJ0aW1lc3RhbXAiOjE2OTEwNjU5MzQwMTcsInZlcnNpb24iOiIxLjIuMSIsImVudmVsb3BlIjoiQWhIenUyMFN3WHZ6T0hPd3c2bkxaODAtd2hoN2Nnd0FqWllNdkQ0UjBXT25xRVc1N21zR2Vral9QejU2b1FwcGdPOVB2aFJFa3VHc2lMdG56c3A2aG13eDRtTTRNLTctRy12NiJ9'; +const testEnvelopeValue = '{"timestamp":1691065934017,"version":"1.2.1","envelope":"AhHzu20SwXvzOHOww6nLZ80-whh7cgwAjZYMvD4R0WOnqEW57msGekj_Pz56oQppgO9PvhREkuGsiLtnzsp6hmwx4mM4M-7-G-v6"}'; + +function setTestEnvelopeCookie () { + let now = new Date(); + now.setTime(now.getTime() + 3000); + storage.setCookie('_lr_env', testEnvelope, now.toUTCString()); +} describe('IdentityLinkId tests', function () { let logErrorStub; @@ -17,6 +26,7 @@ describe('IdentityLinkId tests', function () { logErrorStub = sinon.stub(utils, 'logError'); // remove _lr_retry_request cookie before test storage.setCookie('_lr_retry_request', 'true', 'Thu, 01 Jan 1970 00:00:01 GMT'); + storage.setCookie('_lr_env', testEnvelope, 'Thu, 01 Jan 1970 00:00:01 GMT'); }); afterEach(function () { @@ -163,4 +173,43 @@ describe('IdentityLinkId tests', function () { let request = server.requests[0]; expect(request).to.be.eq(undefined); }); + + it('should get envelope from storage if ats is not present on a page and pass it to callback', function () { + setTestEnvelopeCookie(); + let envelopeValueFromStorage = getEnvelopeFromStorage(); + let callBackSpy = sinon.spy(); + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + expect(envelopeValueFromStorage).to.be.a('string'); + expect(callBackSpy.calledOnce).to.be.true; + }) + + it('if there is no envelope in storage and ats is not present on a page try to call 3p url', function () { + let envelopeValueFromStorage = getEnvelopeFromStorage(); + let callBackSpy = sinon.spy(); + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://api.rlcdn.com/api/identity/envelope?pid=14'); + request.respond( + 204, + responseHeader, + ); + expect(envelopeValueFromStorage).to.be.a('undefined'); + expect(callBackSpy.calledOnce).to.be.true; + }) + + it('if ats is present on a page, and envelope is generated and stored in storage, call a callback', function () { + setTestEnvelopeCookie(); + let envelopeValueFromStorage = getEnvelopeFromStorage(); + window.ats = {retrieveEnvelope: function() { + }} + // mock ats.retrieveEnvelope to return envelope + stub(window.ats, 'retrieveEnvelope').callsFake(function() { return envelopeValueFromStorage }) + let callBackSpy = sinon.spy(); + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + expect(envelopeValueFromStorage).to.be.a('string'); + expect(envelopeValueFromStorage).to.be.eq(testEnvelopeValue); + }) }); From 309f9799db75a07ac5860976e024c16f2742bb82 Mon Sep 17 00:00:00 2001 From: Cadent Aperture MX <43830380+EMXDigital@users.noreply.github.com> Date: Tue, 15 Aug 2023 09:48:48 -0400 Subject: [PATCH 58/88] Cadent Aperture MX Bid Adapter: support GPP and GPP Section Ids (#10342) * added gpp support and test cases * code review changes to tests and syntax * cosmetic change --------- Co-authored-by: Murtaza Haji Co-authored-by: Michael Denton --- modules/cadentApertureMXBidAdapter.js | 17 + .../cadentApertureMXBidAdapter_spec.js | 466 ++++++++++-------- 2 files changed, 285 insertions(+), 198 deletions(-) diff --git a/modules/cadentApertureMXBidAdapter.js b/modules/cadentApertureMXBidAdapter.js index 26e8639154c..22ed0590e05 100644 --- a/modules/cadentApertureMXBidAdapter.js +++ b/modules/cadentApertureMXBidAdapter.js @@ -170,6 +170,22 @@ export const cadentAdapter = { return cadentData; }, + + getGpp: (bidRequest, cadentData) => { + if (bidRequest.gppConsent) { + const {gppString: gpp, applicableSections: gppSid} = bidRequest.gppConsent; + if (cadentData.regs) { + cadentData.regs.gpp = gpp; + cadentData.regs.gpp_sid = gppSid; + } else { + cadentData.regs = { + gpp: gpp, + gpp_sid: gppSid + } + } + } + return cadentData; + }, getSupplyChain: (bidderRequest, cadentData) => { if (bidderRequest.bids[0] && bidderRequest.bids[0].schain) { cadentData.source = { @@ -290,6 +306,7 @@ export const spec = { }; cadentData = cadentAdapter.getGdpr(bidderRequest, Object.assign({}, cadentData)); + cadentData = cadentAdapter.getGpp(bidderRequest, Object.assign({}, cadentData)); cadentData = cadentAdapter.getSupplyChain(bidderRequest, Object.assign({}, cadentData)); if (bidderRequest && bidderRequest.uspConsent) { cadentData.us_privacy = bidderRequest.uspConsent; diff --git a/test/spec/modules/cadentApertureMXBidAdapter_spec.js b/test/spec/modules/cadentApertureMXBidAdapter_spec.js index eb127cfd9f3..64f3d047a3a 100644 --- a/test/spec/modules/cadentApertureMXBidAdapter_spec.js +++ b/test/spec/modules/cadentApertureMXBidAdapter_spec.js @@ -1,7 +1,8 @@ -import { expect } from 'chai'; -import { spec } from 'modules/cadentApertureMXBidAdapter.js'; import * as utils from 'src/utils.js'; + +import { expect } from 'chai'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import { spec } from 'modules/cadentApertureMXBidAdapter.js'; describe('cadent_aperture_mx Adapter', function () { describe('callBids', function () { @@ -240,224 +241,293 @@ describe('cadent_aperture_mx Adapter', function () { }; let request = spec.buildRequests(bidderRequest.bids, bidderRequest); - it('sends bid request to ENDPOINT via POST', function () { - expect(request.method).to.equal('POST'); - }); + describe('non-gpp tests', function() { + it('sends bid request to ENDPOINT via POST', function () { + expect(request.method).to.equal('POST'); + }); - it('contains the correct options', function () { - expect(request.options.withCredentials).to.equal(true); - }); + it('contains the correct options', function () { + expect(request.options.withCredentials).to.equal(true); + }); - it('contains a properly formatted endpoint url', function () { - const url = request.url.split('?'); - const queryParams = url[1].split('&'); - expect(queryParams[0]).to.match(new RegExp('^t=\d*', 'g')); - expect(queryParams[1]).to.match(new RegExp('^ts=\d*', 'g')); - }); + it('contains a properly formatted endpoint url', function () { + const url = request.url.split('?'); + const queryParams = url[1].split('&'); + expect(queryParams[0]).to.match(new RegExp('^t=\d*', 'g')); + expect(queryParams[1]).to.match(new RegExp('^ts=\d*', 'g')); + }); - it('builds bidfloor value from bid param when getFloor function does not exist', function () { - const bidRequestWithFloor = utils.deepClone(bidderRequest.bids); - bidRequestWithFloor[0].params.bidfloor = 1; - const requestWithFloor = spec.buildRequests(bidRequestWithFloor, bidderRequest); - const data = JSON.parse(requestWithFloor.data); - expect(data.imp[0].bidfloor).to.equal(bidRequestWithFloor[0].params.bidfloor); - }); + it('builds bidfloor value from bid param when getFloor function does not exist', function () { + const bidRequestWithFloor = utils.deepClone(bidderRequest.bids); + bidRequestWithFloor[0].params.bidfloor = 1; + const requestWithFloor = spec.buildRequests(bidRequestWithFloor, bidderRequest); + const data = JSON.parse(requestWithFloor.data); + expect(data.imp[0].bidfloor).to.equal(bidRequestWithFloor[0].params.bidfloor); + }); - it('builds bidfloor value from getFloor function when it exists', function () { - const floorResponse = { currency: 'USD', floor: 3 }; - const bidRequestWithGetFloor = utils.deepClone(bidderRequest.bids); - bidRequestWithGetFloor[0].getFloor = () => floorResponse; - const requestWithGetFloor = spec.buildRequests(bidRequestWithGetFloor, bidderRequest); - const data = JSON.parse(requestWithGetFloor.data); - expect(data.imp[0].bidfloor).to.equal(3); - }); + it('builds bidfloor value from getFloor function when it exists', function () { + const floorResponse = { currency: 'USD', floor: 3 }; + const bidRequestWithGetFloor = utils.deepClone(bidderRequest.bids); + bidRequestWithGetFloor[0].getFloor = () => floorResponse; + const requestWithGetFloor = spec.buildRequests(bidRequestWithGetFloor, bidderRequest); + const data = JSON.parse(requestWithGetFloor.data); + expect(data.imp[0].bidfloor).to.equal(3); + }); - it('builds bidfloor value from getFloor when both floor and getFloor function exists', function () { - const floorResponse = { currency: 'USD', floor: 3 }; - const bidRequestWithBothFloors = utils.deepClone(bidderRequest.bids); - bidRequestWithBothFloors[0].params.bidfloor = 1; - bidRequestWithBothFloors[0].getFloor = () => floorResponse; - const requestWithBothFloors = spec.buildRequests(bidRequestWithBothFloors, bidderRequest); - const data = JSON.parse(requestWithBothFloors.data); - expect(data.imp[0].bidfloor).to.equal(3); - }); + it('builds bidfloor value from getFloor when both floor and getFloor function exists', function () { + const floorResponse = { currency: 'USD', floor: 3 }; + const bidRequestWithBothFloors = utils.deepClone(bidderRequest.bids); + bidRequestWithBothFloors[0].params.bidfloor = 1; + bidRequestWithBothFloors[0].getFloor = () => floorResponse; + const requestWithBothFloors = spec.buildRequests(bidRequestWithBothFloors, bidderRequest); + const data = JSON.parse(requestWithBothFloors.data); + expect(data.imp[0].bidfloor).to.equal(3); + }); - it('empty bidfloor value when floor and getFloor is not defined', function () { - const bidRequestWithoutFloor = utils.deepClone(bidderRequest.bids); - const requestWithoutFloor = spec.buildRequests(bidRequestWithoutFloor, bidderRequest); - const data = JSON.parse(requestWithoutFloor.data); - expect(data.imp[0].bidfloor).to.not.exist; - }); + it('empty bidfloor value when floor and getFloor is not defined', function () { + const bidRequestWithoutFloor = utils.deepClone(bidderRequest.bids); + const requestWithoutFloor = spec.buildRequests(bidRequestWithoutFloor, bidderRequest); + const data = JSON.parse(requestWithoutFloor.data); + expect(data.imp[0].bidfloor).to.not.exist; + }); - it('builds request properly', function () { - const data = JSON.parse(request.data); - expect(Array.isArray(data.imp)).to.equal(true); - expect(data.id).to.equal(bidderRequest.auctionId); - expect(data.imp.length).to.equal(1); - expect(data.imp[0].id).to.equal('30b31c2501de1e'); - expect(data.imp[0].tid).to.equal('d7b773de-ceaa-484d-89ca-d9f51b8d61ec'); - expect(data.imp[0].tagid).to.equal('25251'); - expect(data.imp[0].secure).to.equal(0); - expect(data.imp[0].vastXml).to.equal(undefined); - }); + it('builds request properly', function () { + const data = JSON.parse(request.data); + expect(Array.isArray(data.imp)).to.equal(true); + expect(data.id).to.equal(bidderRequest.auctionId); + expect(data.imp.length).to.equal(1); + expect(data.imp[0].id).to.equal('30b31c2501de1e'); + expect(data.imp[0].tid).to.equal('d7b773de-ceaa-484d-89ca-d9f51b8d61ec'); + expect(data.imp[0].tagid).to.equal('25251'); + expect(data.imp[0].secure).to.equal(0); + expect(data.imp[0].vastXml).to.equal(undefined); + }); - it('properly sends site information and protocol', function () { - request = spec.buildRequests(bidderRequest.bids, bidderRequest); - request = JSON.parse(request.data); - expect(request.site).to.have.property('domain', 'example.com'); - expect(request.site).to.have.property('page', 'https://example.com/index.html?pbjs_debug=true'); - expect(request.site).to.have.property('ref', 'https://referrer.com'); - }); + it('properly sends site information and protocol', function () { + request = spec.buildRequests(bidderRequest.bids, bidderRequest); + request = JSON.parse(request.data); + expect(request.site).to.have.property('domain', 'example.com'); + expect(request.site).to.have.property('page', 'https://example.com/index.html?pbjs_debug=true'); + expect(request.site).to.have.property('ref', 'https://referrer.com'); + }); - it('builds correctly formatted request banner object', function () { - let bidRequestWithBanner = utils.deepClone(bidderRequest.bids); - let request = spec.buildRequests(bidRequestWithBanner, bidderRequest); - const data = JSON.parse(request.data); - expect(data.imp[0].video).to.equal(undefined); - expect(data.imp[0].banner).to.exist.and.to.be.a('object'); - expect(data.imp[0].banner.w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); - expect(data.imp[0].banner.h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); - expect(data.imp[0].banner.format[0].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); - expect(data.imp[0].banner.format[0].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); - expect(data.imp[0].banner.format[1].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][0]); - expect(data.imp[0].banner.format[1].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][1]); - }); + it('builds correctly formatted request banner object', function () { + let bidRequestWithBanner = utils.deepClone(bidderRequest.bids); + let request = spec.buildRequests(bidRequestWithBanner, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].video).to.equal(undefined); + expect(data.imp[0].banner).to.exist.and.to.be.a('object'); + expect(data.imp[0].banner.w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); + expect(data.imp[0].banner.h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); + expect(data.imp[0].banner.format[0].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); + expect(data.imp[0].banner.format[0].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); + expect(data.imp[0].banner.format[1].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][0]); + expect(data.imp[0].banner.format[1].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][1]); + }); - it('builds correctly formatted request video object for instream', function () { - let bidRequestWithVideo = utils.deepClone(bidderRequest.bids); - bidRequestWithVideo[0].mediaTypes = { - video: { - context: 'instream', - playerSize: [[640, 480]] - }, - }; - bidRequestWithVideo[0].params.video = {}; - let request = spec.buildRequests(bidRequestWithVideo, bidderRequest); - const data = JSON.parse(request.data); - expect(data.imp[0].video).to.exist.and.to.be.a('object'); - expect(data.imp[0].video.w).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][0]); - expect(data.imp[0].video.h).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][1]); - }); + it('builds correctly formatted request video object for instream', function () { + let bidRequestWithVideo = utils.deepClone(bidderRequest.bids); + bidRequestWithVideo[0].mediaTypes = { + video: { + context: 'instream', + playerSize: [[640, 480]] + }, + }; + bidRequestWithVideo[0].params.video = {}; + let request = spec.buildRequests(bidRequestWithVideo, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].video).to.exist.and.to.be.a('object'); + expect(data.imp[0].video.w).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][0]); + expect(data.imp[0].video.h).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][1]); + }); - it('builds correctly formatted request video object for outstream', function () { - let bidRequestWithOutstreamVideo = utils.deepClone(bidderRequest.bids); - bidRequestWithOutstreamVideo[0].mediaTypes = { - video: { - context: 'outstream', - playerSize: [[640, 480]] - }, - }; - bidRequestWithOutstreamVideo[0].params.video = {}; - let request = spec.buildRequests(bidRequestWithOutstreamVideo, bidderRequest); - const data = JSON.parse(request.data); - expect(data.imp[0].video).to.exist.and.to.be.a('object'); - expect(data.imp[0].video.w).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][0]); - expect(data.imp[0].video.h).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][1]); - }); + it('builds correctly formatted request video object for outstream', function () { + let bidRequestWithOutstreamVideo = utils.deepClone(bidderRequest.bids); + bidRequestWithOutstreamVideo[0].mediaTypes = { + video: { + context: 'outstream', + playerSize: [[640, 480]] + }, + }; + bidRequestWithOutstreamVideo[0].params.video = {}; + let request = spec.buildRequests(bidRequestWithOutstreamVideo, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].video).to.exist.and.to.be.a('object'); + expect(data.imp[0].video.w).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][0]); + expect(data.imp[0].video.h).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][1]); + }); - it('shouldn\'t contain a user obj without GDPR information', function () { - let request = spec.buildRequests(bidderRequest.bids, bidderRequest) - request = JSON.parse(request.data) - expect(request).to.not.have.property('user'); - }); + it('shouldn\'t contain a user obj without GDPR information', function () { + let request = spec.buildRequests(bidderRequest.bids, bidderRequest) + request = JSON.parse(request.data) + expect(request).to.not.have.property('user'); + }); - it('should have the right gdpr info when enabled', function () { - let consentString = 'OIJSZsOAFsABAB8EMXZZZZZ+A=='; - const gdprBidderRequest = utils.deepClone(bidderRequest); - gdprBidderRequest.gdprConsent = { - 'consentString': consentString, - 'gdprApplies': true - }; - let request = spec.buildRequests(gdprBidderRequest.bids, gdprBidderRequest); + it('should have the right gdpr info when enabled', function () { + let consentString = 'OIJSZsOAFsABAB8EMXZZZZZ+A=='; + const gdprBidderRequest = utils.deepClone(bidderRequest); + gdprBidderRequest.gdprConsent = { + 'consentString': consentString, + 'gdprApplies': true + }; + let request = spec.buildRequests(gdprBidderRequest.bids, gdprBidderRequest); + + request = JSON.parse(request.data) + expect(request.regs.ext).to.have.property('gdpr', 1); + expect(request.user.ext).to.have.property('consent', consentString); + }); - request = JSON.parse(request.data) - expect(request.regs.ext).to.have.property('gdpr', 1); - expect(request.user.ext).to.have.property('consent', consentString); - }); + it('should\'t contain consent string if gdpr isn\'t applied', function () { + const nonGdprBidderRequest = utils.deepClone(bidderRequest); + nonGdprBidderRequest.gdprConsent = { + 'gdprApplies': false + }; + let request = spec.buildRequests(nonGdprBidderRequest.bids, nonGdprBidderRequest); + request = JSON.parse(request.data) + expect(request.regs.ext).to.have.property('gdpr', 0); + expect(request).to.not.have.property('user'); + }); - it('should\'t contain consent string if gdpr isn\'t applied', function () { - const nonGdprBidderRequest = utils.deepClone(bidderRequest); - nonGdprBidderRequest.gdprConsent = { - 'gdprApplies': false - }; - let request = spec.buildRequests(nonGdprBidderRequest.bids, nonGdprBidderRequest); - request = JSON.parse(request.data) - expect(request.regs.ext).to.have.property('gdpr', 0); - expect(request).to.not.have.property('user'); - }); + it('should add us privacy info to request', function() { + const uspBidderRequest = utils.deepClone(bidderRequest); + let consentString = '1YNN'; + uspBidderRequest.uspConsent = consentString; + let request = spec.buildRequests(uspBidderRequest.bids, uspBidderRequest); + request = JSON.parse(request.data); + expect(request.us_privacy).to.exist; + expect(request.us_privacy).to.exist.and.to.equal(consentString); + }); - it('should add us privacy info to request', function() { - const uspBidderRequest = utils.deepClone(bidderRequest); - let consentString = '1YNN'; - uspBidderRequest.uspConsent = consentString; - let request = spec.buildRequests(uspBidderRequest.bids, uspBidderRequest); - request = JSON.parse(request.data); - expect(request.us_privacy).to.exist; - expect(request.us_privacy).to.exist.and.to.equal(consentString); - }); + it('should add schain object to request', function() { + const schainBidderRequest = utils.deepClone(bidderRequest); + schainBidderRequest.bids[0].schain = { + 'complete': 1, + 'ver': '1.0', + 'nodes': [ + { + 'asi': 'testing.com', + 'sid': 'abc', + 'hp': 1 + } + ] + }; + let request = spec.buildRequests(schainBidderRequest.bids, schainBidderRequest); + request = JSON.parse(request.data); + expect(request.source.ext.schain).to.exist; + expect(request.source.ext.schain).to.have.property('complete', 1); + expect(request.source.ext.schain).to.have.property('ver', '1.0'); + expect(request.source.ext.schain.nodes[0].asi).to.equal(schainBidderRequest.bids[0].schain.nodes[0].asi); + }); - it('should add schain object to request', function() { - const schainBidderRequest = utils.deepClone(bidderRequest); - schainBidderRequest.bids[0].schain = { - 'complete': 1, - 'ver': '1.0', - 'nodes': [ - { - 'asi': 'testing.com', - 'sid': 'abc', - 'hp': 1 - } - ] - }; - let request = spec.buildRequests(schainBidderRequest.bids, schainBidderRequest); - request = JSON.parse(request.data); - expect(request.source.ext.schain).to.exist; - expect(request.source.ext.schain).to.have.property('complete', 1); - expect(request.source.ext.schain).to.have.property('ver', '1.0'); - expect(request.source.ext.schain.nodes[0].asi).to.equal(schainBidderRequest.bids[0].schain.nodes[0].asi); - }); + it('should add liveramp identitylink id to request', () => { + const idl_env = '123'; + const bidRequestWithID = utils.deepClone(bidderRequest); + bidRequestWithID.userId = { idl_env }; + let requestWithID = spec.buildRequests(bidRequestWithID.bids, bidRequestWithID); + requestWithID = JSON.parse(requestWithID.data); + expect(requestWithID.user.ext.eids[0]).to.deep.equal({ + source: 'liveramp.com', + uids: [{ + id: idl_env, + ext: { + rtiPartner: 'idl' + } + }] + }); + }); - it('should add liveramp identitylink id to request', () => { - const idl_env = '123'; - const bidRequestWithID = utils.deepClone(bidderRequest); - bidRequestWithID.userId = { idl_env }; - let requestWithID = spec.buildRequests(bidRequestWithID.bids, bidRequestWithID); - requestWithID = JSON.parse(requestWithID.data); - expect(requestWithID.user.ext.eids[0]).to.deep.equal({ - source: 'liveramp.com', - uids: [{ - id: idl_env, - ext: { - rtiPartner: 'idl' - } - }] + it('should add gpid to request if present', () => { + const gpid = '/12345/my-gpt-tag-0'; + let bid = utils.deepClone(bidderRequest.bids[0]); + bid.ortb2Imp = { ext: { data: { adserver: { adslot: gpid } } } }; + bid.ortb2Imp = { ext: { data: { pbadslot: gpid } } }; + let requestWithGPID = spec.buildRequests([bid], bidderRequest); + requestWithGPID = JSON.parse(requestWithGPID.data); + expect(requestWithGPID.imp[0].ext.gpid).to.exist.and.equal(gpid); }); - }); - it('should add gpid to request if present', () => { - const gpid = '/12345/my-gpt-tag-0'; - let bid = utils.deepClone(bidderRequest.bids[0]); - bid.ortb2Imp = { ext: { data: { adserver: { adslot: gpid } } } }; - bid.ortb2Imp = { ext: { data: { pbadslot: gpid } } }; - let requestWithGPID = spec.buildRequests([bid], bidderRequest); - requestWithGPID = JSON.parse(requestWithGPID.data); - expect(requestWithGPID.imp[0].ext.gpid).to.exist.and.equal(gpid); + it('should add UID 2.0 to request', () => { + const uid2 = { id: '456' }; + const bidRequestWithUID = utils.deepClone(bidderRequest); + bidRequestWithUID.userId = { uid2 }; + let requestWithUID = spec.buildRequests(bidRequestWithUID.bids, bidRequestWithUID); + requestWithUID = JSON.parse(requestWithUID.data); + expect(requestWithUID.user.ext.eids[0]).to.deep.equal({ + source: 'uidapi.com', + uids: [{ + id: uid2.id, + ext: { + rtiPartner: 'UID2' + } + }] + }); + }); }); - it('should add UID 2.0 to request', () => { - const uid2 = { id: '456' }; - const bidRequestWithUID = utils.deepClone(bidderRequest); - bidRequestWithUID.userId = { uid2 }; - let requestWithUID = spec.buildRequests(bidRequestWithUID.bids, bidRequestWithUID); - requestWithUID = JSON.parse(requestWithUID.data); - expect(requestWithUID.user.ext.eids[0]).to.deep.equal({ - source: 'uidapi.com', - uids: [{ - id: uid2.id, - ext: { - rtiPartner: 'UID2' - } - }] + describe('gpp tests', function() { + describe('when gppConsent is not present on bid request', () => { + it('should return request with no gpp or gpp_sid properties', function() { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request?.regs?.gpp).to.be.undefined; + expect(request?.regs?.gpp_sid).to.be.undefined; + }); + }); + + describe('when gppConsent is present on bid request', () => { + describe('gppString', () => { + describe('is not defined on request', () => { + it('should return request with gpp undefined', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request?.regs?.gpp).to.be.undefined; + }); + }); + + describe('is defined on request', () => { + it('should return request with gpp set correctly', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + const gppString = 'abcdefgh'; + gppCompliantBidderRequest.gppConsent = { + gppString + } + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request.regs.gpp).to.exist.and.to.equal(gppString); + }); + }); + }); + + describe('applicableSections', () => { + describe('is not defined on request', () => { + it('should return request with gpp_sid undefined', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request?.regs?.gpp_sid).to.be.undefined; + }); + }); + + describe('is defined on request', () => { + it('should return request with gpp_sid set correctly', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + const applicableSections = [8]; + gppCompliantBidderRequest.gppConsent = { + applicableSections + } + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request.regs.gpp_sid).to.deep.equal(applicableSections); + }); + }); + }); }); }); }); From 1f6abf6d3eb83134f1b88c6f659263275026e2a0 Mon Sep 17 00:00:00 2001 From: Nayan Savla Date: Wed, 16 Aug 2023 05:46:01 -0700 Subject: [PATCH 59/88] Yieldmo Bid Adapter : adding 4.x VAST protocol support (#10363) * Adding 4.x VAST Protocol support Adding protocol support for VAST 4.0 and 4.1 * Adding unit tests --- modules/yieldmoBidAdapter.js | 10 ++++++++-- test/spec/modules/yieldmoBidAdapter_spec.js | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 40276aa7a6c..0e2c7b4bf59 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -607,8 +607,14 @@ function validateVideoParams(bid) { } validate('video.protocols', val => isDefined(val), paramRequired); - validate('video.protocols', val => isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), - paramInvalid, 'array of numbers, ex: [2,3]'); + validate( + 'video.protocols', + (val) => + isArrayOfNums(val) && + val.every((v) => v >= 1 && v <= 12 && v != 9 && v != 10), // 9 and 10 are for DAST which are not supported. + paramInvalid, + 'array of numbers between 1 and 12 except for 9 or 10 , ex: [2,3, 7, 11]' + ); validate('video.api', val => isDefined(val), paramRequired); validate('video.api', val => isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index 3706f770da8..25fe176553d 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -447,6 +447,20 @@ describe('YieldmoAdapter', function () { expect(buildVideoBidAndGetVideoParam().mimes).to.deep.equal(['video/mkv']); }); + it('should validate protocol in video bid request', function () { + expect( + spec.isBidRequestValid( + mockVideoBid({}, {}, { protocols: [2, 3, 11] }) + ) + ).to.be.true; + + expect( + spec.isBidRequestValid( + mockVideoBid({}, {}, { protocols: [2, 3, 10] }) + ) + ).to.be.false; + }); + describe('video.skip state check', () => { it('should not set video.skip if neither *.video.skip nor *.video.skippable is present', function () { utils.deepAccess(videoBid, 'mediaTypes.video')['skippable'] = false; From 62c9d2940c39560096ccf75e4c88f00cceb871fd Mon Sep 17 00:00:00 2001 From: Jarrod Swart <1610469+jcswart@users.noreply.github.com> Date: Wed, 16 Aug 2023 10:09:31 -0400 Subject: [PATCH 60/88] Relay Bid Adapter : Initial Release (#10197) * Add Relay Bid Adapter. * Fix imports and lint violations. * Implement PR feedback. --- modules/relayBidAdapter.js | 99 ++++++++++++++++ modules/relayBidAdapter.md | 79 +++++++++++++ test/spec/modules/relayBidAdapter_spec.js | 131 ++++++++++++++++++++++ 3 files changed, 309 insertions(+) create mode 100644 modules/relayBidAdapter.js create mode 100644 modules/relayBidAdapter.md create mode 100644 test/spec/modules/relayBidAdapter_spec.js diff --git a/modules/relayBidAdapter.js b/modules/relayBidAdapter.js new file mode 100644 index 00000000000..af145a5e163 --- /dev/null +++ b/modules/relayBidAdapter.js @@ -0,0 +1,99 @@ +import { isNumber, logMessage } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js' + +const BIDDER_CODE = 'relay'; +const METHOD = 'POST'; +const ENDPOINT_URL = 'https://e.relay.bid/p/openrtb2'; + +// The default impl from the prebid docs. +const CONVERTER = + ortbConverter({ + context: { + netRevenue: true, + ttl: 30 + } + }); + +function buildRequests(bidRequests, bidderRequest) { + const prebidVersion = config.getConfig('prebid_version') || 'v8.1.0'; + // Group bids by accountId param + const groupedByAccountId = bidRequests.reduce((accu, item) => { + const accountId = ((item || {}).params || {}).accountId; + if (!accu[accountId]) { accu[accountId] = []; }; + accu[accountId].push(item); + return accu; + }, {}); + // Send one overall request with all grouped bids per accountId + let reqs = []; + for (const [accountId, accountBidRequests] of Object.entries(groupedByAccountId)) { + const url = `${ENDPOINT_URL}?a=${accountId}&pb=1&pbv=${prebidVersion}`; + const data = CONVERTER.toORTB({ bidRequests: accountBidRequests, bidderRequest }) + const req = { + method: METHOD, + url, + data + }; + reqs.push(req); + } + return reqs; +}; + +function interpretResponse(response, request) { + return CONVERTER.fromORTB({ response: response.body, request: request.data }).bids; +}; + +function isBidRequestValid(bid) { + return isNumber((bid.params || {}).accountId); +}; + +function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + let syncs = [] + for (const response of serverResponses) { + const responseSyncs = ((((response || {}).body || {}).ext || {}).user_syncs || []) + // Relay returns user_syncs in the format expected by prebid. If for any + // reason the request/response failed to properly capture the GDPR settings + // -- fallback to those identified by Prebid. + for (const sync of responseSyncs) { + const syncUrl = new URL(sync.url); + const missingGdpr = !syncUrl.searchParams.has('gdpr'); + const missingGdprConsent = !syncUrl.searchParams.has('gdpr_consent'); + if (missingGdpr) { + syncUrl.searchParams.set('gdpr', Number(gdprConsent.gdprApplies)) + sync.url = syncUrl.toString(); + } + if (missingGdprConsent) { + syncUrl.searchParams.set('gdpr_consent', gdprConsent.consentString); + sync.url = syncUrl.toString(); + } + if (syncOptions.iframeEnabled && sync.type === 'iframe') { + syncs.push(sync); + } else if (syncOptions.pixelEnabled && sync.type === 'image') { + syncs.push(sync); + } + } + } + + return syncs; +} + +export const spec = { + code: BIDDER_CODE, + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, + onTimeout: function (timeoutData) { + logMessage('Timeout: ', timeoutData); + }, + onBidWon: function (bid) { + logMessage('Bid won: ', bid); + }, + onBidderError: function ({ error, bidderRequest }) { + logMessage('Error: ', error, bidderRequest); + }, + supportedMediaTypes: [BANNER, VIDEO, NATIVE] +} +registerBidder(spec); diff --git a/modules/relayBidAdapter.md b/modules/relayBidAdapter.md new file mode 100644 index 00000000000..882e04b7b13 --- /dev/null +++ b/modules/relayBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Relay Bid Adapter +Module Type: Bid Adapter +Maintainer: relay@kevel.co +``` + +# Description + +Connects to Relay exchange API for bids. +Supports Banner, Video and Native. + +# Test Parameters + +``` +var adUnits = [ + // Banner with minimal bid configuration + { + code: 'minimal', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: 'relay', + params: { + accountId: 1234 + }, + ortb2imp: { + ext: { + relay: { + bidders: { + bidderA: { + param: 1234 + } + } + } + } + } + } + ] + }, + // Minimal video + { + code: 'video-minimal', + mediaTypes: { + video: { + maxduration: 30, + api: [1, 3], + mimes: ['video/mp4'], + placement: 3, + protocols: [2,3,5,6] + } + }, + bids: [ + { + bidder: 'relay', + params: { + accountId: 1234 + }, + ortb2imp: { + ext: { + relay: { + bidders: { + bidderA: { + param: 'example' + } + } + } + } + } + } + ] + } +]; +``` diff --git a/test/spec/modules/relayBidAdapter_spec.js b/test/spec/modules/relayBidAdapter_spec.js new file mode 100644 index 00000000000..38a3cfc9b97 --- /dev/null +++ b/test/spec/modules/relayBidAdapter_spec.js @@ -0,0 +1,131 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/relayBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'relay' +const endpoint = 'https://e.relay.bid/p/openrtb2'; + +describe('RelayBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder, + mediaTypes: { [BANNER]: { sizes: [[300, 250]] } }, + params: { + accountId: 15000, + }, + ortb2Imp: { + ext: { + relay: { + bidders: { + bidderA: { + theId: 'abc123' + }, + bidderB: { + theId: 'xyz789' + } + } + } + } + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder, + mediaTypes: { [BANNER]: { sizes: [[300, 250]] } }, + params: { + accountId: 30000, + }, + ortb2Imp: { + ext: { + relay: { + bidders: { + bidderA: { + theId: 'def456' + }, + bidderB: { + theId: 'uvw101112' + } + } + } + } + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: {} + } + + const bidderRequest = {}; + + describe('isBidRequestValid', function () { + it('Valid bids have a params.accountId.', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Invalid bids do not have a params.accountId.', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + const requests = spec.buildRequests(bids, bidderRequest); + const firstRequest = requests[0]; + const secondRequest = requests[1]; + + it('Creates two requests', function () { + expect(firstRequest).to.exist; + expect(firstRequest.data).to.exist; + expect(firstRequest.method).to.exist; + expect(firstRequest.method).to.equal('POST'); + expect(firstRequest.url).to.exist; + expect(firstRequest.url).to.equal(`${endpoint}?a=15000&pb=1&pbv=v8.1.0`); + + expect(secondRequest).to.exist; + expect(secondRequest.data).to.exist; + expect(secondRequest.method).to.exist; + expect(secondRequest.method).to.equal('POST'); + expect(secondRequest.url).to.exist; + expect(secondRequest.url).to.equal(`${endpoint}?a=30000&pb=1&pbv=v8.1.0`); + }); + + it('Does not generate requests when there are no bids', function () { + const request = spec.buildRequests([], bidderRequest); + expect(request).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function () { + it('Uses Prebid consent values if incoming sync URLs lack consent.', function () { + const syncOpts = { + iframeEnabled: true, + pixelEnabled: true + }; + const test_gdpr_applies = true; + const test_gdpr_consent_str = 'TEST_GDPR_CONSENT_STRING'; + const responses = [{ + body: { + ext: { + user_syncs: [ + { type: 'image', url: 'https://image-example.com' }, + { type: 'iframe', url: 'https://iframe-example.com' } + ] + } + } + }]; + + const sync_urls = spec.getUserSyncs(syncOpts, responses, { gdprApplies: test_gdpr_applies, consentString: test_gdpr_consent_str }); + expect(sync_urls).to.be.an('array'); + expect(sync_urls[0].url).to.equal('https://image-example.com/?gdpr=1&gdpr_consent=TEST_GDPR_CONSENT_STRING'); + expect(sync_urls[1].url).to.equal('https://iframe-example.com/?gdpr=1&gdpr_consent=TEST_GDPR_CONSENT_STRING'); + }); + }); +}); From 7fc315f65c2d86fb0e397e009c2a5054ce05aa5e Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 16 Aug 2023 08:05:08 -0700 Subject: [PATCH 61/88] consentManagementGpp: support GPP 1.1 (#10282) * add option to use callbacks in lieu of return values when talking to CMPs * Add `once` option to cmpClient * introduce MODE_RETURN; add .close() method to CMP clients * GPP 1.0/1.1 clients * update gppControl for 1.1 * linting --- libraries/cmp/cmpClient.js | 62 +- libraries/mspa/activityControls.js | 26 +- modules/consentManagementGpp.js | 402 +++++-- test/spec/libraries/cmp/cmpClient_spec.js | 117 ++- .../libraries/mspa/activityControls_spec.js | 14 +- .../spec/modules/consentManagementGpp_spec.js | 992 +++++++++++------- 6 files changed, 1074 insertions(+), 539 deletions(-) diff --git a/libraries/cmp/cmpClient.js b/libraries/cmp/cmpClient.js index 0e2336cae7a..03a50c37bb3 100644 --- a/libraries/cmp/cmpClient.js +++ b/libraries/cmp/cmpClient.js @@ -4,28 +4,53 @@ import {GreedyPromise} from '../../src/utils/promise.js'; * @typedef {function} CMPClient * * @param {{}} params CMP parameters. Currently this is a subset of {command, callback, parameter, version}. - * @returns {Promise<*>} a promise that: - * - if a `callback` param was provided, resolves (with no result) just before the first time it's run; - * - if `callback` was *not* provided, resolves to the return value of the CMP command + * @param {bool} once if true, discard cross-frame event listeners once a reply message is received. + * @returns {Promise<*>} a promise to the API's "result" - see the `mode` argument to `cmpClient` on how that's determined. * @property {boolean} isDirect true if the CMP is directly accessible (no postMessage required) + * @property {() => void} close close the client; currently, this just stops listening for cross-frame messages. */ /** - * Returns a function that can interface with a CMP regardless of where it's located. + * Returns a client function that can interface with a CMP regardless of where it's located. * * @param apiName name of the CMP api, e.g. "__gpp" * @param apiVersion? CMP API version * @param apiArgs? names of the arguments taken by the api function, in order. * @param callbackArgs? names of the cross-frame response payload properties that should be passed as callback arguments, in order + * @param mode? controls the callbacks passed to the underlying API, and how the promises returned by the client are resolved. + * + * The client behaves differently when it's provided a `callback` argument vs when it's not - for short, let's name these + * cases "subscriptions" and "one-shot calls" respectively: + * + * With `mode: MODE_MIXED` (the default), promises returned on subscriptions are resolved to undefined when the callback + * is first run (that is, the promise resolves when the CMP replies, but what it replies with is discarded and + * left for the callback to deal with). For one-shot calls, the returned promise is resolved to the API's + * return value when it's directly accessible, or with the result from the first (and, presumably, the only) + * cross-frame reply when it's not; + * + * With `mode: MODE_RETURN`, the returned promise always resolves to the API's return value - which is taken to be undefined + * when cross-frame; + * + * With `mode: MODE_CALLBACK`, the underlying API is expected to never directly return anything significant; instead, + * it should always accept a callback and - for one-shot calls - invoke it only once with the result. The client will + * automatically generate an appropriate callback for one-shot calls and use the result it's given to resolve + * the returned promise. Subscriptions are treated in the same way as MODE_MIXED. + * * @param win * @returns {CMPClient} CMP invocation function (or null if no CMP was found). */ + +export const MODE_MIXED = 0; +export const MODE_RETURN = 1; +export const MODE_CALLBACK = 2; + export function cmpClient( { apiName, apiVersion, apiArgs = ['command', 'callback', 'parameter', 'version'], callbackArgs = ['returnValue', 'success'], + mode = MODE_MIXED, }, win = window ) { @@ -89,15 +114,15 @@ export function cmpClient( } function wrapCallback(callback, resolve, reject, preamble) { + const haveCb = typeof callback === 'function'; + return function (result, success) { preamble && preamble(); - const resolver = success == null || success ? resolve : reject; - if (typeof callback === 'function') { - resolver(); - return callback.apply(this, arguments); - } else { - resolver(result); + if (mode !== MODE_RETURN) { + const resolver = success == null || success ? resolve : reject; + resolver(haveCb ? undefined : result); } + haveCb && callback.apply(this, arguments); } } @@ -108,9 +133,9 @@ export function cmpClient( return new GreedyPromise((resolve, reject) => { const ret = cmpFrame[apiName](...resolveParams({ ...params, - callback: params.callback && wrapCallback(params.callback, resolve, reject) + callback: (params.callback || mode === MODE_CALLBACK) ? wrapCallback(params.callback, resolve, reject) : undefined, }).map(([_, val]) => val)); - if (params.callback == null) { + if (mode === MODE_RETURN || (params.callback == null && mode === MODE_MIXED)) { resolve(ret); } }); @@ -118,7 +143,7 @@ export function cmpClient( } else { win.addEventListener('message', handleMessage, false); - client = function invokeCMPFrame(params) { + client = function invokeCMPFrame(params, once = false) { return new GreedyPromise((resolve, reject) => { // call CMP via postMessage const callId = Math.random().toString(); @@ -129,11 +154,16 @@ export function cmpClient( } }; - cmpCallbacks[callId] = wrapCallback(params?.callback, resolve, reject, params?.callback == null && (() => { delete cmpCallbacks[callId] })); + cmpCallbacks[callId] = wrapCallback(params?.callback, resolve, reject, (once || params?.callback == null) && (() => { delete cmpCallbacks[callId] })); cmpFrame.postMessage(msg, '*'); + if (mode === MODE_RETURN) resolve(); }); }; } - client.isDirect = isDirect; - return client; + return Object.assign(client, { + isDirect, + close() { + !isDirect && win.removeEventListener('message', handleMessage); + } + }) } diff --git a/libraries/mspa/activityControls.js b/libraries/mspa/activityControls.js index d18f0617bfe..baaf81a8671 100644 --- a/libraries/mspa/activityControls.js +++ b/libraries/mspa/activityControls.js @@ -87,26 +87,38 @@ const CONSENT_RULES = { [ACTIVITY_ENRICH_EIDS]: isConsentDenied, [ACTIVITY_ENRICH_UFPD]: isTransmitUfpdConsentDenied, [ACTIVITY_TRANSMIT_PRECISE_GEO]: isTransmitGeoConsentDenied -} +}; export function mspaRule(sids, getConsent, denies, applicableSids = () => gppDataHandler.getConsentData()?.applicableSections) { - return function() { + return function () { if (applicableSids().some(sid => sids.includes(sid))) { const consent = getConsent(); if (consent == null) { return {allow: false, reason: 'consent data not available'}; } if (denies(consent)) { - return {allow: false} + return {allow: false}; } } - } + }; +} + +function flatSection(subsections) { + if (subsections == null) return subsections; + return subsections.reduceRight((subsection, consent) => { + return Object.assign(consent, subsection); + }, {}); } export function setupRules(api, sids, normalizeConsent = (c) => c, rules = CONSENT_RULES, registerRule = registerActivityControl, getConsentData = () => gppDataHandler.getConsentData()) { const unreg = []; Object.entries(rules).forEach(([activity, denies]) => { - unreg.push(registerRule(activity, `MSPA (${api})`, mspaRule(sids, () => normalizeConsent(getConsentData()?.sectionData?.[api]), denies, () => getConsentData()?.applicableSections || []))) - }) - return () => unreg.forEach(ur => ur()) + unreg.push(registerRule(activity, `MSPA (${api})`, mspaRule( + sids, + () => normalizeConsent(flatSection(getConsentData()?.parsedSections?.[api])), + denies, + () => getConsentData()?.applicableSections || [] + ))); + }); + return () => unreg.forEach(ur => ur()); } diff --git a/modules/consentManagementGpp.js b/modules/consentManagementGpp.js index 393b7f8fe4e..69fc5789953 100644 --- a/modules/consentManagementGpp.js +++ b/modules/consentManagementGpp.js @@ -4,19 +4,18 @@ * and make it available for any GPP supported adapters to read/pass this information to * their system and for various other features/modules in Prebid.js. */ -import {deepSetValue, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; +import {deepSetValue, isEmpty, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import {gppDataHandler} from '../src/adapterManager.js'; import {timedAuctionHook} from '../src/utils/perfMetrics.js'; -import { enrichFPD } from '../src/fpd/enrichment.js'; +import {enrichFPD} from '../src/fpd/enrichment.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import {cmpClient} from '../libraries/cmp/cmpClient.js'; +import {cmpClient, MODE_CALLBACK, MODE_MIXED, MODE_RETURN} from '../libraries/cmp/cmpClient.js'; import {GreedyPromise} from '../src/utils/promise.js'; import {buildActivityParams} from '../src/activities/params.js'; const DEFAULT_CMP = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 10000; -const CMP_VERSION = 1; export let userCMP; export let consentTimeout; @@ -25,87 +24,294 @@ let staticConsentData; let consentData; let addedConsentHook = false; -// add new CMPs here, with their dedicated lookup function -const cmpCallMap = { - 'iab': lookupIabConsent, - 'static': lookupStaticConsentData -}; +function pipeCallbacks(fn, {onSuccess, onError}) { + new GreedyPromise((resolve) => resolve(fn())).then(onSuccess, (err) => { + if (err instanceof GPPError) { + onError(err.message, ...err.args); + } else { + onError(`GPP error:`, err); + } + }); +} -/** - * This function checks the state of the IAB gppData's applicableSections field (to ensure it's populated and has a valid value). - * section === 0 represents a CMP's default value when CMP is loading, it shoud not be used a real user's section. - * @param gppData represents the IAB gppData object - * @returns {Array} - */ -function applicableSections(gppData) { - return gppData && Array.isArray(gppData.applicableSections) && gppData.applicableSections.length > 0 && gppData.applicableSections[0] !== 0 - ? gppData.applicableSections - : []; +function lookupStaticConsentData(callbacks) { + return pipeCallbacks(() => processCmpData(staticConsentData), callbacks); } -/** - * This function reads the consent string from the config to obtain the consent information of the user. - * @param {function({})} onSuccess acts as a success callback when the value is read from config; pass along consentObject from CMP - */ -function lookupStaticConsentData({onSuccess, onError}) { - processCmpData(staticConsentData, {onSuccess, onError}); +const GPP_10 = '1.0'; +const GPP_11 = '1.1'; + +class GPPError { + constructor(message, arg) { + this.message = message; + this.args = arg == null ? [] : [arg]; + } } -/** - * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. - * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function - * based on the appropriate result. - * @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP - * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) - */ -export function lookupIabConsent({onSuccess, onError}, mkClient = cmpClient) { - const cmp = mkClient({ - apiName: '__gpp', - apiVersion: CMP_VERSION, - }); - if (!cmp) { - return onError('GPP CMP not found.'); +export class GPPClient { + static CLIENTS = {}; + + static register(apiVersion, defaultVersion = false) { + this.apiVersion = apiVersion; + this.CLIENTS[apiVersion] = this; + if (defaultVersion) { + this.CLIENTS.default = this; + } } - const startupMsg = (cmp.isDirect) ? 'Detected GPP CMP API is directly accessible, calling it now...' - : 'Detected GPP CMP is outside the current iframe where Prebid.js is located, calling it now...'; - logInfo(startupMsg); - - let versionMismatch = false; - - cmp({ - command: 'addEventListener', - callback: function (evt) { - if (evt && !versionMismatch) { - logInfo(`Received a ${(cmp.isDirect ? 'direct' : 'postmsg')} response from GPP CMP for event`, evt); - const cmpVer = evt?.pingData?.gppVersion; - if (cmpVer != null && cmpVer !== '1.0') { - logWarn(`Unsupported GPP CMP version: ${cmpVer}. Continuing auction without consent`); - versionMismatch = true; - onSuccess(storeConsentData()); + static INST; + + /** + * Ping the CMP to set up an appropriate client for it, and initialize it. + * + * @param mkCmp + * @returns {Promise<[GPPClient,Promise<{}>]>} a promise to two objects: + * - a GPPClient that talks the best GPP dialect we know for the CMP's version; + * - a promise to GPP data. + */ + static init(mkCmp = cmpClient) { + if (this.INST == null) { + this.INST = this.ping(mkCmp).catch(e => { + this.INST = null; + throw e; + }); + } + return this.INST.then(([client, pingData]) => [ + client, + client.initialized ? client.refresh() : client.init(pingData) + ]); + } + + /** + * Ping the CMP to determine its version and set up a client appropriate for it. + * + * @param mkCmp + * @returns {Promise<[GPPClient, {}]>} a promise to two objects: + * - a GPPClient that talks the best GPP dialect we know for the CMP's version; + * - the result from pinging the CMP. + */ + static ping(mkCmp = cmpClient) { + const cmpOptions = { + apiName: '__gpp', + apiArgs: ['command', 'callback', 'parameter'], // do not pass version - not clear what it's for (or what we should use) + }; + + // in 1.0, 'ping' should return pingData but ignore callback; + // in 1.1 it should not return anything but run the callback + // the following looks for either - but once the version is known, produce a client that knows whether the + // rest of the interactions should pick return values or pass callbacks + + const probe = mkCmp({...cmpOptions, mode: MODE_RETURN}); + return new GreedyPromise((resolve, reject) => { + if (probe == null) { + reject(new GPPError('GPP CMP not found')); + return; + } + let done = false; // some CMPs do both return value and callbacks - avoid repeating log messages + const pong = (result, success) => { + if (done) return; + if (success != null && !success) { + reject(result); return; } - if (evt.eventName === 'sectionChange' || evt.pingData.cmpStatus === 'loaded') { - cmp({command: 'getGPPData'}).then((gppData) => { - logInfo(`Received a ${cmp.isDirect ? 'direct' : 'postmsg'} response from GPP CMP for getGPPData`, gppData); - return GreedyPromise.all( - (gppData?.pingData?.supportedAPIs || []) - .map((name) => cmp({command: 'getSection', parameter: name}) - .catch(() => { logError(`Could not retrieve section data for GPP section '${name}'`) }) - .then((res) => [name, res])) - ).then((sections) => { - const sectionData = Object.fromEntries(sections.filter(([_, val]) => val != null)); - processCmpData({gppData, sectionData}, {onSuccess, onError}); - }) - }); - } else if (evt.pingData.cmpStatus === 'error') { - onError('CMP returned with a cmpStatus:error response. Please check CMP setup.'); + if (result == null) return; + done = true; + const cmpVersion = result?.gppVersion; + const Client = this.getClient(cmpVersion); + if (cmpVersion !== Client.apiVersion) { + logWarn(`Unrecognized GPP CMP version: ${cmpVersion}. Continuing using GPP API version ${Client}...`); + } else { + logInfo(`Using GPP version ${cmpVersion}`); } + const mode = Client.apiVersion === GPP_10 ? MODE_MIXED : MODE_CALLBACK; + const client = new Client( + cmpVersion, + mkCmp({...cmpOptions, mode}) + ); + resolve([client, result]); + }; + + probe({ + command: 'ping', + callback: pong + }).then((res) => pong(res, true), reject); + }).finally(() => { + probe && probe.close(); + }); + } + + static getClient(cmpVersion) { + return this.CLIENTS.hasOwnProperty(cmpVersion) ? this.CLIENTS[cmpVersion] : this.CLIENTS.default; + } + + #resolve; + #reject; + #pending = []; + + initialized = false; + + constructor(cmpVersion, cmp) { + this.apiVersion = this.constructor.apiVersion; + this.cmpVersion = cmp; + this.cmp = cmp; + [this.#resolve, this.#reject] = [0, 1].map(slot => (result) => { + while (this.#pending.length) { + this.#pending.pop()[slot](result); } + }); + } + + /** + * initialize this client - update consent data if already available, + * and set up event listeners to also update on CMP changes + * + * @param pingData + * @returns {Promise<{}>} a promise to GPP consent data + */ + init(pingData) { + const ready = this.updateWhenReady(pingData); + if (!this.initialized) { + this.initialized = true; + this.cmp({ + command: 'addEventListener', + callback: (event, success) => { + if (success != null && !success) { + this.#reject(new GPPError('Received error response from CMP', event)); + } else if (event?.pingData?.cmpStatus === 'error') { + this.#reject(new GPPError('CMP status is "error"; please check CMP setup', event)); + } else if (this.isCMPReady(event?.pingData || {}) && this.events.includes(event?.eventName)) { + this.#resolve(this.updateConsent(event.pingData)); + } + } + }); } - }); + return ready; + } + + refresh() { + return this.cmp({command: 'ping'}).then(this.updateWhenReady.bind(this)); + } + + /** + * Retrieve and store GPP consent data. + * + * @param pingData + * @returns {Promise<{}>} a promise to GPP consent data + */ + updateConsent(pingData) { + return this.getGPPData(pingData).then((data) => { + if (data == null || isEmpty(data)) { + throw new GPPError('Received empty response from CMP', data); + } + return processCmpData(data); + }).then((data) => { + logInfo('Retrieved GPP consent from CMP:', data); + return data; + }); + } + + /** + * Return a promise to GPP consent data, to be retrieved the next time the CMP signals it's ready. + * + * @returns {Promise<{}>} + */ + nextUpdate() { + return new GreedyPromise((resolve, reject) => { + this.#pending.push([resolve, reject]); + }); + } + + /** + * Return a promise to GPP consent data, to be retrieved immediately if the CMP is ready according to `pingData`, + * or as soon as it signals that it's ready otherwise. + * + * @param pingData + * @returns {Promise<{}>} + */ + updateWhenReady(pingData) { + return this.isCMPReady(pingData) ? this.updateConsent(pingData) : this.nextUpdate(); + } +} + +// eslint-disable-next-line no-unused-vars +class GPP10Client extends GPPClient { + static { + super.register(GPP_10); + } + + events = ['sectionChange', 'cmpStatus']; + + isCMPReady(pingData) { + return pingData.cmpStatus === 'loaded'; + } + + getGPPData(pingData) { + const parsedSections = GreedyPromise.all( + pingData.supportedAPIs.map((api) => this.cmp({ + command: 'getSection', + parameter: api + }).catch(err => { + logWarn(`Could not retrieve GPP section '${api}'`, err); + }).then((section) => [api, section])) + ).then(sections => { + // parse single section object into [core, gpc] to uniformize with 1.1 parsedSections + return Object.fromEntries( + sections.filter(([_, val]) => val != null) + .map(([api, section]) => { + const subsections = [ + Object.fromEntries(Object.entries(section).filter(([k]) => k !== 'Gpc')) + ]; + if (section.Gpc != null) { + subsections.push({ + SubsectionType: 1, + Gpc: section.Gpc + }); + } + return [api, subsections]; + }) + ); + }); + return GreedyPromise.all([ + this.cmp({command: 'getGPPData'}), + parsedSections + ]).then(([gppData, parsedSections]) => Object.assign({}, gppData, {parsedSections})); + } +} + +// eslint-disable-next-line no-unused-vars +class GPP11Client extends GPPClient { + static { + super.register(GPP_11, true); + } + + events = ['sectionChange', 'signalStatus']; + + isCMPReady(pingData) { + return pingData.signalStatus === 'ready'; + } + + getGPPData(pingData) { + return GreedyPromise.resolve(pingData); + } } +/** + * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. + * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function + * based on the appropriate result. + * @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP + * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) + */ +export function lookupIabConsent({onSuccess, onError}, mkCmp = cmpClient) { + pipeCallbacks(() => GPPClient.init(mkCmp).then(([client, gppDataPm]) => gppDataPm), {onSuccess, onError}); +} + +// add new CMPs here, with their dedicated lookup function +const cmpCallMap = { + 'iab': lookupIabConsent, + 'static': lookupStaticConsentData +}; + /** * Look up consent data and store it in the `consentData` global as well as `adapterManager.js`' gdprDataHandler. * @@ -137,19 +343,19 @@ function loadConsentData(cb) { onError: function (msg, ...extraArgs) { done(null, true, msg, ...extraArgs); } - } + }; cmpCallMap[userCMP](callbacks); if (!isDone) { const onTimeout = () => { const continueToAuction = (data) => { done(data, false, 'GPP CMP did not load, continuing auction...'); - } - processCmpData(consentData, { + }; + pipeCallbacks(() => processCmpData(consentData), { onSuccess: continueToAuction, onError: () => continueToAuction(storeConsentData()) - }) - } + }); + }; if (consentTimeout === 0) { onTimeout(); } else { @@ -204,27 +410,15 @@ export const requestBidsHook = timedAuctionHook('gpp', function requestBidsHook( }); }); -/** - * This function checks the consent data provided by CMP to ensure it's in an expected state. - * If it's bad, we call `onError` - * If it's good, then we store the value and call `onSuccess` - */ -function processCmpData(consentData, {onSuccess, onError}) { - function checkData() { - const gppString = consentData?.gppData?.gppString; - const gppSection = consentData?.gppData?.applicableSections; - - return !!( - (!Array.isArray(gppSection)) || - (Array.isArray(gppSection) && (!gppString || !isStr(gppString))) - ); - } - - if (checkData()) { - onError(`CMP returned unexpected value during lookup process.`, consentData); - } else { - onSuccess(storeConsentData(consentData)); +function processCmpData(consentData) { + if ( + (consentData?.applicableSections != null && !Array.isArray(consentData.applicableSections)) || + (consentData?.gppString != null && !isStr(consentData.gppString)) || + (consentData?.parsedSections != null && !isPlainObject(consentData.parsedSections)) + ) { + throw new GPPError('CMP returned unexpected value during lookup process.', consentData); } + return storeConsentData(consentData); } /** @@ -232,14 +426,14 @@ function processCmpData(consentData, {onSuccess, onError}) { * @param {{}} gppData the result of calling a CMP's `getGPPData` (or equivalent) * @param {{}} sectionData map from GPP section name to the result of calling a CMP's `getSection` (or equivalent) */ -export function storeConsentData({gppData, sectionData} = {}) { +export function storeConsentData(gppData = {}) { consentData = { - gppString: (gppData) ? gppData.gppString : undefined, - gppData: (gppData) || undefined, + gppString: gppData?.gppString, + applicableSections: gppData?.applicableSections || [], + parsedSections: gppData?.parsedSections || {}, + gppData: gppData }; - consentData.applicableSections = applicableSections(gppData); - consentData.apiVersion = CMP_VERSION; - consentData.sectionData = sectionData; + gppDataHandler.setConsentData(gppData); return consentData; } @@ -251,6 +445,7 @@ export function resetConsentData() { userCMP = undefined; consentTimeout = undefined; gppDataHandler.reset(); + GPPClient.INST = null; } /** @@ -280,7 +475,7 @@ export function setConsentConfig(config) { if (userCMP === 'static') { if (isPlainObject(config.consentData)) { - staticConsentData = {gppData: config.consentData, sectionData: config.sectionData}; + staticConsentData = config.consentData; consentTimeout = 0; } else { logError(`consentManagement.gpp config with cmpApi: 'static' did not specify consentData. No consents will be available to adapters.`); @@ -299,6 +494,7 @@ export function setConsentConfig(config) { gppDataHandler.enable(); loadConsentData(); // immediately look up consent data to make it available without requiring an auction } + config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); export function enrichFPDHook(next, fpd) { diff --git a/test/spec/libraries/cmp/cmpClient_spec.js b/test/spec/libraries/cmp/cmpClient_spec.js index 56dd8e12605..adbbbf5cb1d 100644 --- a/test/spec/libraries/cmp/cmpClient_spec.js +++ b/test/spec/libraries/cmp/cmpClient_spec.js @@ -1,4 +1,4 @@ -import {cmpClient} from '../../../../libraries/cmp/cmpClient.js'; +import {cmpClient, MODE_CALLBACK, MODE_RETURN} from '../../../../libraries/cmp/cmpClient.js'; describe('cmpClient', () => { function mockWindow(props = {}) { @@ -7,6 +7,9 @@ describe('cmpClient', () => { addEventListener: sinon.stub().callsFake((evt, listener) => { evt === 'message' && listeners.push(listener) }), + removeEventListener: sinon.stub().callsFake((evt, listener) => { + evt === 'message' && (listeners = listeners.filter((l) => l !== listener)); + }), postMessage: sinon.stub().callsFake((msg) => { listeners.forEach(ln => ln({data: msg})) }), @@ -62,10 +65,15 @@ describe('cmpClient', () => { return 'val' }) }) + Object.entries({ callback: [sinon.stub(), 'undefined', undefined], - 'no callback': [undefined, 'api return value', 'val'] - }).forEach(([t, [callback, tResult, expectedResult]]) => { + 'callback, mode = MODE_CALLBACK': [sinon.stub(), 'undefined', undefined, MODE_CALLBACK], + 'callback, mode = MODE_RETURN': [sinon.stub(), 'api return value', 'val', MODE_RETURN], + 'no callback': [undefined, 'api return value', 'val'], + 'no callback, mode = MODE_CALLBACK': [undefined, 'callback arg', 'cbVal', MODE_CALLBACK], + 'no callback, mode = MODE_RETURN': [undefined, 'api return value', 'val', MODE_RETURN], + }).forEach(([t, [callback, tResult, expectedResult, mode]]) => { describe(`when ${t} is provided`, () => { Object.entries({ 'no success flag': undefined, @@ -73,23 +81,36 @@ describe('cmpClient', () => { }).forEach(([t, success]) => { it(`resolves to ${tResult} (${t})`, (done) => { cbResult = ['cbVal', success]; - mkClient()({callback}).then((val) => { + mkClient({mode})({callback}).then((val) => { expect(val).to.equal(expectedResult); done(); }) + }); + + it('should pass either a function or undefined as callback', () => { + mkClient({mode})({callback}); + sinon.assert.calledWith(mockApiFn, sinon.match.any, sinon.match(arg => typeof arg === 'undefined' || typeof arg === 'function')) }) }); }) }); - it('rejects to undefined when callback is provided and success = false', () => { + it('rejects to undefined when callback is provided and success = false', (done) => { cbResult = ['cbVal', false]; mkClient()({callback: sinon.stub()}).catch(val => { - expect(val).to.equal('cbVal'); + expect(val).to.not.exist; done(); }) }); + it('rejects to callback arg when callback is NOT provided, success = false, mode = MODE_CALLBACK', (done) => { + cbResult = ['cbVal', false]; + mkClient({mode: MODE_CALLBACK})().catch(val => { + expect(val).to.eql('cbVal'); + done(); + }) + }) + it('rejects when CMP api throws', (done) => { mockApiFn.reset(); const e = new Error(); @@ -98,7 +119,7 @@ describe('cmpClient', () => { expect(val).to.equal(e); done(); }); - }) + }); }) it('should use apiArgs to choose and order the arguments to pass to the API fn', () => { @@ -109,6 +130,10 @@ describe('cmpClient', () => { }); sinon.assert.calledWith(mockApiFn, 'mockParam', 'mockCmd'); }); + + it('should not choke on .close()', () => { + mkClient({}).close(); + }) }) }) }) @@ -189,8 +214,12 @@ describe('cmpClient', () => { }) Object.entries({ 'callback': [sinon.stub(), 'undefined', undefined], + 'callback, mode = MODE_RETURN': [sinon.stub(), 'undefined', undefined, MODE_RETURN], + 'callback, mode = MODE_CALLBACK': [sinon.stub(), 'undefined', undefined, MODE_CALLBACK], 'no callback': [undefined, 'response returnValue', 'val'], - }).forEach(([t, [callback, tResult, expectedResult]]) => { + 'no callback, mode = MODE_RETURN': [undefined, 'undefined', undefined, MODE_RETURN], + 'no callback, mode = MODE_CALLBACK': [undefined, 'response returnValue', 'val', MODE_CALLBACK], + }).forEach(([t, [callback, tResult, expectedResult, mode]]) => { describe(`when ${t} is provided`, () => { Object.entries({ 'no success flag': {}, @@ -198,35 +227,69 @@ describe('cmpClient', () => { }).forEach(([t, resp]) => { it(`resolves to ${tResult} (${t})`, () => { Object.assign(response, resp); - mkClient()({callback}).then((val) => { + mkClient({mode})({callback}).then((val) => { expect(val).to.equal(expectedResult); }) }) }); - it(`rejects to ${tResult} when success = false`, (done) => { - response.success = false; - mkClient()({callback}).catch((err) => { - expect(err).to.equal(expectedResult); - done(); + if (mode !== MODE_RETURN) { // in return mode, the promise never rejects + it(`rejects to ${tResult} when success = false`, (done) => { + response.success = false; + mkClient()({mode, callback}).catch((err) => { + expect(err).to.equal(expectedResult); + done(); + }); }); - }); + } }) }); }); - it('should re-use callback for messages with same callId', () => { - messenger.reset(); - let callId; - messenger.callsFake((msg) => { if (msg.mockApiCall) callId = msg.mockApiCall.callId }); - const callback = sinon.stub(); - mkClient()({callback}); - expect(callId).to.exist; - win.postMessage({mockApiReturn: {callId, returnValue: 'a'}}); - win.postMessage({mockApiReturn: {callId, returnValue: 'b'}}); - sinon.assert.calledWith(callback, 'a'); - sinon.assert.calledWith(callback, 'b'); - }) + describe('messages with same callID', () => { + let callback, callId; + + function runCallback(returnValue) { + win.postMessage({mockApiReturn: {callId, returnValue}}); + } + + beforeEach(() => { + callId = null; + messenger.reset(); + messenger.callsFake((msg) => { + if (msg.mockApiCall) callId = msg.mockApiCall.callId; + }); + callback = sinon.stub(); + }); + + it('should re-use callback for messages with same callId', () => { + mkClient()({callback}); + expect(callId).to.exist; + runCallback('a'); + runCallback('b'); + sinon.assert.calledWith(callback, 'a'); + sinon.assert.calledWith(callback, 'b'); + }); + + it('should NOT re-use callback if once = true', () => { + mkClient()({callback}, true); + expect(callId).to.exist; + runCallback('a'); + runCallback('b'); + sinon.assert.calledWith(callback, 'a'); + sinon.assert.calledOnce(callback); + }); + + it('should NOT fire again after .close()', () => { + const client = mkClient(); + client({callback}); + runCallback('a'); + client.close(); + runCallback('b'); + sinon.assert.calledWith(callback, 'a'); + sinon.assert.calledOnce(callback); + }) + }); }); }); }); diff --git a/test/spec/libraries/mspa/activityControls_spec.js b/test/spec/libraries/mspa/activityControls_spec.js index 247e405683a..f232dc2563f 100644 --- a/test/spec/libraries/mspa/activityControls_spec.js +++ b/test/spec/libraries/mspa/activityControls_spec.js @@ -209,10 +209,12 @@ describe('setupRules', () => { ([registerRule, isAllowed] = ruleRegistry()); consent = { applicableSections: [1], - sectionData: { - mockApi: { - mock: 'consent' - } + parsedSections: { + mockApi: [ + { + mock: 'consent' + } + ] } }; }); @@ -221,7 +223,7 @@ describe('setupRules', () => { return setupRules(api, sids, normalize, rules, registerRule, () => consent) } - it('should use section data for the given api', () => { + it('should use flatten section data for the given api', () => { runSetup('mockApi', [1]); expect(isAllowed('mockActivity', {})).to.equal(false); sinon.assert.calledWith(rules.mockActivity, {mock: 'consent'}) @@ -238,7 +240,7 @@ describe('setupRules', () => { expect(isAllowed('mockActivity', {})).to.equal(true); }); - it('should pass consent through normalizeConsent', () => { + it('should pass flattened consent through normalizeConsent', () => { const normalize = sinon.stub().returns({normalized: 'consent'}) runSetup('mockApi', [1], normalize); expect(isAllowed('mockActivity', {})).to.equal(false); diff --git a/test/spec/modules/consentManagementGpp_spec.js b/test/spec/modules/consentManagementGpp_spec.js index 37776c15cea..e15ce30940c 100644 --- a/test/spec/modules/consentManagementGpp_spec.js +++ b/test/spec/modules/consentManagementGpp_spec.js @@ -1,19 +1,23 @@ import { - setConsentConfig, + consentTimeout, + GPPClient, requestBidsHook, resetConsentData, - userCMP, - consentTimeout, - storeConsentData, lookupIabConsent + setConsentConfig, + userCMP } from 'modules/consentManagementGpp.js'; -import { gppDataHandler } from 'src/adapterManager.js'; +import {gppDataHandler} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import { config } from 'src/config.js'; +import {config} from 'src/config.js'; import 'src/prebid.js'; +import {MODE_CALLBACK, MODE_MIXED} from '../../../libraries/cmp/cmpClient.js'; +import {GreedyPromise} from '../../../src/utils/promise.js'; let expect = require('chai').expect; describe('consentManagementGpp', function () { + beforeEach(resetConsentData); + describe('setConsentConfig tests:', function () { describe('empty setConsentConfig value', function () { beforeEach(function () { @@ -101,80 +105,6 @@ describe('consentManagementGpp', function () { }); }); - describe('lookupIABConsent', () => { - let mockCmp, mockCmpEvent, gppData, sectionData - beforeEach(() => { - gppData = { - gppString: 'mockString', - applicableSections: [], - pingData: {} - }; - sectionData = {}; - mockCmp = sinon.stub().callsFake(({command, callback, parameter}) => { - let res; - switch (command) { - case 'addEventListener': - mockCmpEvent = callback; - break; - case 'getGPPData': - res = gppData; - break; - case 'getSection': - res = sectionData[parameter]; - break; - } - return Promise.resolve(res); - }); - }) - - function runLookup() { - return new Promise((resolve, reject) => lookupIabConsent({onSuccess: resolve, onError: reject}, () => mockCmp)); - } - - function oneShotLookup() { - const pm = runLookup(); - mockCmpEvent({eventName: 'sectionChange'}); - return pm; - } - - it('fetches all sections', () => { - gppData.pingData.supportedAPIs = ['usnat', 'usca'] - sectionData = { - usnat: {mock: 'usnat'}, - usca: {mock: 'usca'} - }; - return oneShotLookup().then((res) => { - expect(res.sectionData).to.eql(sectionData); - }); - }); - - it('does not choke if some section data is not available', () => { - gppData.pingData.supportedAPIs = ['usnat', 'usca'] - sectionData = { - usca: {mock: 'data'} - }; - return oneShotLookup().then((res) => { - expect(res.sectionData).to.eql(sectionData); - }) - }); - - it('continues with no consent when CMP version is not 1.0', () => { - const pm = runLookup(); - mockCmpEvent({ - eventName: 'listenerRegistered', - pingData: { - gppVersion: '1.1' - } - }); - return pm.then((res) => { - sinon.assert.match(res, { - gppString: undefined, - applicableSections: [] - }) - }) - }) - }) - describe('static consent string setConsentConfig value', () => { afterEach(() => { config.resetConfig(); @@ -185,17 +115,19 @@ describe('consentManagementGpp', function () { gpp: { cmpApi: 'static', timeout: 7500, - sectionData: { - usnat: { - MockUsnatParsedFlag: true - } - }, consentData: { applicableSections: [7], gppString: 'ABCDEFG1234', gppVersion: 1, sectionId: 3, - sectionList: [] + sectionList: [], + parsedSections: { + usnat: [ + { + MockUsnatParsedFlag: true + }, + ] + }, } } }; @@ -209,6 +141,588 @@ describe('consentManagementGpp', function () { }); }); }); + describe('GPPClient.ping', () => { + function mkPingData(gppVersion) { + return { + gppVersion + } + } + Object.entries({ + 'unknown': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData(), + apiVersion: '1.1', + client({callback}) { + callback(this.pingData); + } + }, + '1.0': { + expectedMode: MODE_MIXED, + pingData: mkPingData('1.0'), + apiVersion: '1.0', + client() { + return this.pingData; + } + }, + '1.1 that runs callback immediately': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData('1.1'), + apiVersion: '1.1', + client({callback}) { + callback(this.pingData); + } + }, + '1.1 that defers callback': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData('1.1'), + apiVersion: '1.1', + client({callback}) { + setTimeout(() => callback(this.pingData), 10); + } + }, + '> 1.1': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData('1.2'), + apiVersion: '1.1', + client({callback}) { + setTimeout(() => callback(this.pingData), 10); + } + } + }).forEach(([t, scenario]) => { + describe(`using CMP version ${t}`, () => { + let clients, mkClient; + beforeEach(() => { + clients = []; + mkClient = ({mode}) => { + const mockClient = function (args) { + if (args.command === 'ping') { + return Promise.resolve(scenario.client(args)); + } + } + mockClient.mode = mode; + mockClient.close = sinon.stub(); + clients.push(mockClient); + return mockClient; + } + }); + + it('should resolve to client with the correct mode', () => { + return GPPClient.ping(mkClient).then(([client]) => { + expect(client.cmp.mode).to.eql(scenario.expectedMode); + }); + }); + + it('should resolve to pingData', () => { + return GPPClient.ping(mkClient).then(([_, pingData]) => { + expect(pingData).to.eql(scenario.pingData); + }); + }); + + it('should .close the probing client', () => { + return GPPClient.ping(mkClient).then(([client]) => { + sinon.assert.called(clients[0].close); + sinon.assert.notCalled(client.cmp.close); + }) + }); + + it('should .tag the client with version', () => { + return GPPClient.ping(mkClient).then(([client]) => { + expect(client.apiVersion).to.eql(scenario.apiVersion); + }) + }) + }) + }); + + it('should reject when mkClient returns null (CMP not found)', () => { + return GPPClient.ping(() => null).catch((err) => { + expect(err.message).to.match(/not found/); + }); + }); + + it('should reject when client rejects', () => { + const err = {some: 'prop'}; + const mockClient = () => Promise.reject(err); + mockClient.close = sinon.stub(); + return GPPClient.ping(() => mockClient).catch((result) => { + expect(result).to.eql(err); + sinon.assert.called(mockClient.close); + }); + }); + + it('should reject when callback is invoked with success = false', () => { + const err = 'error'; + const mockClient = ({callback}) => callback(err, false); + mockClient.close = sinon.stub(); + return GPPClient.ping(() => mockClient).catch((result) => { + expect(result).to.eql(err); + sinon.assert.called(mockClient.close); + }) + }) + }); + + describe('GPPClient.init', () => { + let makeCmp, cmpCalls, cmpResult; + + beforeEach(() => { + cmpResult = {signalStatus: 'ready', gppString: 'mock-str'}; + cmpCalls = []; + makeCmp = sinon.stub().callsFake(() => { + function mockCmp(args) { + cmpCalls.push(args); + return GreedyPromise.resolve(cmpResult); + } + mockCmp.close = sinon.stub(); + return mockCmp; + }); + }); + + it('should re-use same client', (done) => { + GPPClient.init(makeCmp).then(([client]) => { + GPPClient.init(makeCmp).then(([client2, consentPm]) => { + expect(client2).to.equal(client); + expect(cmpCalls.filter((el) => el.command === 'ping').length).to.equal(2) // recycled client should be refreshed + consentPm.then((consent) => { + expect(consent.gppString).to.eql('mock-str'); + done() + }) + }); + }); + }); + + it('should not re-use errors', (done) => { + cmpResult = Promise.reject(new Error()); + GPPClient.init(makeCmp).catch(() => { + cmpResult = {signalStatus: 'ready'}; + return GPPClient.init(makeCmp).then(([client]) => { + expect(client).to.exist; + done() + }) + }) + }) + }) + + describe('GPP client', () => { + const CHANGE_EVENTS = ['sectionChange', 'signalStatus']; + + let gppClient, gppData, cmpReady, eventListener; + + function mockClient(apiVersion = '1.1', cmpVersion = '1.1') { + const mockCmp = sinon.stub().callsFake(function ({command, callback}) { + if (command === 'addEventListener') { + eventListener = callback; + } else { + throw new Error('unexpected command: ' + command); + } + }) + const client = new GPPClient(cmpVersion, mockCmp); + client.apiVersion = apiVersion; + client.getGPPData = sinon.stub().callsFake(() => Promise.resolve(gppData)); + client.isCMPReady = sinon.stub().callsFake(() => cmpReady); + client.events = CHANGE_EVENTS; + return client; + } + + beforeEach(() => { + gppDataHandler.reset(); + eventListener = null; + cmpReady = true; + gppData = { + applicableSections: [7], + gppString: 'mock-string', + parsedSections: { + usnat: [ + { + Field: 'val' + }, + { + SubsectionType: 1, + Gpc: false + } + ] + } + }; + gppClient = mockClient(); + }); + + describe('updateConsent', () => { + it('should update data handler with consent data', () => { + return gppClient.updateConsent().then(data => { + sinon.assert.match(data, gppData); + sinon.assert.match(gppDataHandler.getConsentData(), gppData); + expect(gppDataHandler.ready).to.be.true; + }); + }); + + Object.entries({ + 'emtpy': {}, + 'missing': null + }).forEach(([t, data]) => { + it(`should not update, and reject promise, when gpp data is ${t}`, (done) => { + gppData = data; + gppClient.updateConsent().catch(err => { + expect(err.message).to.match(/empty/); + expect(err.args).to.eql(data == null ? [] : [data]); + expect(gppDataHandler.ready).to.be.false; + done() + }) + }); + }) + + it('should not update when gpp data rejects', (done) => { + gppData = Promise.reject(new Error('err')); + gppClient.updateConsent().catch(err => { + expect(gppDataHandler.ready).to.be.false; + expect(err.message).to.eql('err'); + done(); + }) + }); + + describe('consent data validation', () => { + Object.entries({ + applicableSections: { + 'not an array': 'not-an-array', + }, + gppString: { + 'not a string': 234 + }, + parsedSections: { + 'not an object': 'not-an-object' + } + }).forEach(([prop, tests]) => { + describe(`validation: when ${prop} is`, () => { + Object.entries(tests).forEach(([t, value]) => { + describe(t, () => { + it('should not update', (done) => { + Object.assign(gppData, {[prop]: value}); + gppClient.updateConsent().catch(err => { + expect(err.message).to.match(/unexpected/); + expect(err.args).to.eql([gppData]); + expect(gppDataHandler.ready).to.be.false; + done(); + }); + }); + }) + }); + }); + }); + }); + }); + + describe('init', () => { + beforeEach(() => { + gppClient.isCMPReady = function (pingData) { + return pingData.ready; + } + gppClient.getGPPData = function (pingData) { + return Promise.resolve(pingData); + } + }) + + it('does not use initial pingData if CMP is not ready', () => { + gppClient.init({...gppData, ready: false}); + expect(eventListener).to.exist; + expect(gppDataHandler.ready).to.be.false; + }); + + it('uses initial pingData (and resolves promise) if CMP is ready', () => { + return gppClient.init({...gppData, ready: true}).then(data => { + expect(eventListener).to.exist; + sinon.assert.match(data, gppData); + sinon.assert.match(gppDataHandler.getConsentData(), gppData); + }) + }); + + it('rejects promise when CMP errors out', (done) => { + gppClient.init({ready: false}).catch((err) => { + expect(err.message).to.match(/error/); + expect(err.args).to.eql(['error']) + done(); + }); + eventListener('error', false); + }); + + Object.entries({ + 'empty': {}, + 'null': null, + 'irrelevant': {eventName: 'irrelevant'} + }).forEach(([t, evt]) => { + it(`ignores ${t} events`, () => { + let pm = gppClient.init({ready: false}).catch((err) => err.args[0] !== 'done' && Promise.reject(err)); + eventListener(evt); + eventListener('done', false); + return pm; + }) + }); + + it('rejects the promise when cmpStatus is "error"', (done) => { + const evt = {eventName: 'other', pingData: {cmpStatus: 'error'}}; + gppClient.init({ready: false}).catch(err => { + expect(err.message).to.match(/error/); + expect(err.args).to.eql([evt]); + done(); + }); + eventListener(evt); + }) + + CHANGE_EVENTS.forEach(evt => { + describe(`event: ${evt}`, () => { + function makeEvent(pingData) { + return { + eventName: evt, + pingData + } + } + + let gppData2 + beforeEach(() => { + gppData2 = Object.assign(gppData, {gppString: '2nd'}); + }); + + it('does not fire consent data updates if the CMP is not ready', (done) => { + gppClient.init({ready: false}).catch(() => { + expect(gppDataHandler.ready).to.be.false; + done(); + }); + eventListener({...gppData2, ready: false}); + eventListener('done', false); + }) + + it('fires consent data updates (and resolves promise) if CMP is ready', (done) => { + gppClient.init({ready: false}).then(data => { + sinon.assert.match(data, gppData2); + done() + }); + cmpReady = true; + eventListener(makeEvent({...gppData2, ready: true})); + }); + + it('keeps updating consent data on new events', () => { + let pm = gppClient.init({ready: false}).then(data => { + sinon.assert.match(data, gppData); + sinon.assert.match(gppDataHandler.getConsentData(), gppData); + }); + eventListener(makeEvent({...gppData, ready: true})); + return pm.then(() => { + eventListener(makeEvent({...gppData2, ready: true})) + }).then(() => { + sinon.assert.match(gppDataHandler.getConsentData(), gppData2); + }); + }); + }) + }) + }); + }); + + describe('GPP 1.0 protocol', () => { + let mockCmp, gppClient; + beforeEach(() => { + mockCmp = sinon.stub(); + gppClient = new (GPPClient.getClient('1.0'))('1.0', mockCmp); + }); + + describe('isCMPReady', () => { + Object.entries({ + 'loaded': [true, 'loaded'], + 'other': [false, 'other'], + 'undefined': [false, undefined] + }).forEach(([t, [expected, cmpStatus]]) => { + it(`should be ${expected} when cmpStatus is ${t}`, () => { + expect(gppClient.isCMPReady(Object.assign({}, {cmpStatus}))).to.equal(expected); + }); + }); + }); + + describe('getGPPData', () => { + let gppData, pingData; + beforeEach(() => { + gppData = { + gppString: 'mock-string', + supportedAPIs: ['usnat'], + applicableSections: [7, 8] + } + pingData = { + supportedAPIs: gppData.supportedAPIs + }; + }); + + function mockCmpCommands(commands) { + mockCmp.callsFake(({command, parameter}) => { + if (commands.hasOwnProperty((command))) { + return Promise.resolve(commands[command](parameter)); + } else { + return Promise.reject(new Error(`unrecognized command ${command}`)) + } + }) + } + + it('should retrieve consent string and applicableSections', () => { + mockCmpCommands({ + getGPPData: () => gppData + }) + return gppClient.getGPPData(pingData).then(data => { + sinon.assert.match(data, gppData); + }) + }); + + it('should reject when getGPPData rejects', (done) => { + mockCmpCommands({ + getGPPData: () => Promise.reject(new Error('err')) + }); + gppClient.getGPPData(pingData).catch(err => { + expect(err.message).to.eql('err'); + done(); + }); + }) + + describe('section data', () => { + let usnat, parsedUsnat; + + function mockSections(sections) { + mockCmpCommands({ + getGPPData: () => gppData, + getSection: (api) => (sections[api]) + }); + }; + + beforeEach(() => { + usnat = { + MockField: 'val', + OtherField: 'o', + Gpc: true + }; + parsedUsnat = [ + { + MockField: 'val', + OtherField: 'o' + }, + { + SubsectionType: 1, + Gpc: true + } + ] + }); + + it('retrieves section data', () => { + mockSections({usnat}); + return gppClient.getGPPData(pingData).then(data => { + expect(data.parsedSections).to.eql({usnat: parsedUsnat}) + }); + }); + + it('does not choke if a section is missing', () => { + mockSections({usnat}); + gppData.supportedAPIs = ['usnat', 'missing']; + return gppClient.getGPPData(pingData).then(data => { + expect(data.parsedSections).to.eql({usnat: parsedUsnat}); + }) + }); + + it('does not choke if a section fails', () => { + mockSections({usnat, err: Promise.reject(new Error('err'))}); + gppData.supportedAPIs = ['usnat', 'err']; + return gppClient.getGPPData(pingData).then(data => { + expect(data.parsedSections).to.eql({usnat: parsedUsnat}); + }) + }); + }) + }); + }); + + describe('GPP 1.1 protocol', () => { + let mockCmp, gppClient; + beforeEach(() => { + mockCmp = sinon.stub(); + gppClient = new (GPPClient.getClient('1.1'))('1.1', mockCmp); + }); + + describe('isCMPReady', () => { + Object.entries({ + 'ready': [true, 'ready'], + 'not ready': [false, 'not ready'], + 'undefined': [false, undefined] + }).forEach(([t, [expected, signalStatus]]) => { + it(`should be ${expected} when signalStatus is ${t}`, () => { + expect(gppClient.isCMPReady(Object.assign({}, {signalStatus}))).to.equal(expected); + }); + }); + }); + + it('gets GPPData from pingData', () => { + mockCmp.throws(new Error()); + const pingData = { + 'gppVersion': '1.1', + 'cmpStatus': 'loaded', + 'cmpDisplayStatus': 'disabled', + 'supportedAPIs': [ + '5:tcfcav1', + '7:usnat', + '8:usca', + '9:usva', + '10:usco', + '11:usut', + '12:usct' + ], + 'signalStatus': 'ready', + 'cmpId': 31, + 'sectionList': [ + 7 + ], + 'applicableSections': [ + 7 + ], + 'gppString': 'DBABL~BAAAAAAAAgA.QA', + 'parsedSections': { + 'usnat': [ + { + 'Version': 1, + 'SharingNotice': 0, + 'SaleOptOutNotice': 0, + 'SharingOptOutNotice': 0, + 'TargetedAdvertisingOptOutNotice': 0, + 'SensitiveDataProcessingOptOutNotice': 0, + 'SensitiveDataLimitUseNotice': 0, + 'SaleOptOut': 0, + 'SharingOptOut': 0, + 'TargetedAdvertisingOptOut': 0, + 'SensitiveDataProcessing': [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + 'KnownChildSensitiveDataConsents': [ + 0, + 0 + ], + 'PersonalDataConsents': 0, + 'MspaCoveredTransaction': 2, + 'MspaOptOutOptionMode': 0, + 'MspaServiceProviderMode': 0 + }, + { + 'SubsectionType': 1, + 'Gpc': false + } + ] + } + }; + return gppClient.getGPPData(pingData).then((gppData) => { + sinon.assert.match(gppData, { + gppString: pingData.gppString, + applicableSections: pingData.applicableSections, + parsedSections: pingData.parsedSections + }) + }) + }) + }) describe('requestBidsHook tests:', function () { let goodConfig = { @@ -313,13 +827,13 @@ describe('consentManagementGpp', function () { }); it('should continue the auction immediately, without consent data, if timeout is 0', (done) => { + window.__gpp = function () {}; setConsentConfig({ gpp: { cmpApi: 'iab', timeout: 0 } }); - window.__gpp = function () {}; try { requestBidsHook(() => { const consent = gppDataHandler.getConsentData(); @@ -336,14 +850,16 @@ describe('consentManagementGpp', function () { describe('already known consentData:', function () { let cmpStub = sinon.stub(); - function mockCMP(cmpResponse) { - return function (...args) { - if (args[0] === 'addEventListener') { - args[1](({ - eventName: 'sectionChange' - })); - } else if (args[0] === 'getGPPData') { - return cmpResponse; + function mockCMP(pingData) { + return function (command, callback) { + switch (command) { + case 'addEventListener': + // eslint-disable-next-line standard/no-callback-literal + callback({eventName: 'sectionChange', pingData}) + break; + case 'ping': + callback(pingData) + break; } } } @@ -366,7 +882,7 @@ describe('consentManagementGpp', function () { gppString: 'xyz', }; - cmpStub = sinon.stub(window, '__gpp').callsFake(mockCMP(testConsentData)); + cmpStub = sinon.stub(window, '__gpp').callsFake(mockCMP({...testConsentData, signalStatus: 'ready'})); setConsentConfig(goodConfig); requestBidsHook(() => {}, {}); cmpStub.reset(); @@ -382,289 +898,5 @@ describe('consentManagementGpp', function () { sinon.assert.notCalled(cmpStub); }); }); - - describe('iframe tests', function () { - let cmpPostMessageCb = () => {}; - let stringifyResponse; - - function createIFrameMarker(frameName) { - let ifr = document.createElement('iframe'); - ifr.width = 0; - ifr.height = 0; - ifr.name = frameName; - document.body.appendChild(ifr); - return ifr; - } - - function creatCmpMessageHandler(prefix, returnEvtValue, returnGPPValue) { - return function (event) { - if (event && event.data) { - let data = event.data; - if (data[`${prefix}Call`]) { - let callId = data[`${prefix}Call`].callId; - let response; - if (data[`${prefix}Call`].command === 'addEventListener') { - response = { - [`${prefix}Return`]: { - callId, - returnValue: returnEvtValue, - success: true - } - } - } else if (data[`${prefix}Call`].command === 'getGPPData') { - response = { - [`${prefix}Return`]: { - callId, - returnValue: returnGPPValue, - success: true - } - } - } else if (data[`${prefix}Call`].command === 'getSection') { - response = { - [`${prefix}Return`]: { - callId, - returnValue: {}, - success: true - } - } - } - event.source.postMessage(stringifyResponse ? JSON.stringify(response) : response, '*'); - } - } - } - } - - function testIFramedPage(testName, messageFormatString, tarConsentString, tarSections) { - it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { - stringifyResponse = messageFormatString; - setConsentConfig(goodConfig); - requestBidsHook(() => { - let consent = gppDataHandler.getConsentData(); - sinon.assert.notCalled(utils.logError); - expect(consent.gppString).to.equal(tarConsentString); - expect(consent.applicableSections).to.deep.equal(tarSections); - done(); - }, {}); - }); - } - - beforeEach(function () { - sinon.stub(utils, 'logError'); - sinon.stub(utils, 'logWarn'); - }); - - afterEach(function () { - utils.logError.restore(); - utils.logWarn.restore(); - config.resetConfig(); - resetConsentData(); - }); - - describe('workflow for iframe pages:', function () { - stringifyResponse = false; - let ifr2 = null; - - beforeEach(function () { - ifr2 = createIFrameMarker('__gppLocator'); - cmpPostMessageCb = creatCmpMessageHandler('__gpp', { - eventName: 'sectionChange' - }, { - gppString: 'abc12345234', - applicableSections: [7] - }); - window.addEventListener('message', cmpPostMessageCb, false); - }); - - afterEach(function () { - delete window.__gpp; // deletes the local copy made by the postMessage CMP call function - document.body.removeChild(ifr2); - window.removeEventListener('message', cmpPostMessageCb); - }); - - testIFramedPage('with/JSON response', false, 'abc12345234', [7]); - testIFramedPage('with/String response', true, 'abc12345234', [7]); - }); - }); - - describe('direct calls to CMP API tests', function () { - let cmpStub = sinon.stub(); - - beforeEach(function () { - didHookReturn = false; - sinon.stub(utils, 'logError'); - sinon.stub(utils, 'logWarn'); - }); - - afterEach(function () { - config.resetConfig(); - cmpStub.restore(); - utils.logError.restore(); - utils.logWarn.restore(); - resetConsentData(); - }); - - describe('CMP workflow for normal pages:', function () { - beforeEach(function () { - window.__gpp = function () {}; - }); - - afterEach(function () { - delete window.__gpp; - }); - - it('performs lookup check and stores consentData for a valid existing user', function () { - let testConsentData = { - gppString: 'abc12345234', - applicableSections: [7] - }; - cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { - if (args[0] === 'addEventListener') { - args[1]({ - eventName: 'sectionChange' - }); - } else if (args[0] === 'getGPPData') { - return testConsentData; - } - }); - - setConsentConfig(goodConfig); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); - let consent = gppDataHandler.getConsentData(); - sinon.assert.notCalled(utils.logError); - expect(didHookReturn).to.be.true; - expect(consent.gppString).to.equal(testConsentData.gppString); - expect(consent.applicableSections).to.deep.equal(testConsentData.applicableSections); - }); - - it('produces gdpr metadata', function () { - let testConsentData = { - gppString: 'abc12345234', - applicableSections: [7] - }; - cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { - if (args[0] === 'addEventListener') { - args[1]({ - eventName: 'sectionChange' - }); - } else if (args[0] === 'getGPPData') { - return testConsentData; - } - }); - - setConsentConfig(goodConfig); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); - let consentMeta = gppDataHandler.getConsentMeta(); - sinon.assert.notCalled(utils.logError); - expect(consentMeta.generatedAt).to.be.above(1644367751709); - }); - - it('throws an error when processCmpData check fails + does not call requestBids callback', function () { - let testConsentData = {}; - let bidsBackHandlerReturn = false; - - cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { - if (args[0] === 'addEventListener') { - args[1]({ - eventName: 'sectionChange' - }); - } else if (args[0] === 'getGPPData') { - return testConsentData; - } - }); - - setConsentConfig(goodConfig); - - sinon.assert.notCalled(utils.logWarn); - sinon.assert.notCalled(utils.logError); - - [utils.logWarn, utils.logError].forEach((stub) => stub.reset()); - - requestBidsHook(() => { - didHookReturn = true; - }, { - bidsBackHandler: () => bidsBackHandlerReturn = true - }); - let consent = gppDataHandler.getConsentData(); - - sinon.assert.calledOnce(utils.logError); - sinon.assert.notCalled(utils.logWarn); - expect(didHookReturn).to.be.false; - expect(bidsBackHandlerReturn).to.be.true; - expect(consent).to.be.null; - expect(gppDataHandler.ready).to.be.true; - }); - - describe('when proper consent is not available', () => { - let gppStub; - - function runAuction() { - setConsentConfig({ - gpp: { - cmpApi: 'iab', - timeout: 10, - } - }); - return new Promise((resolve, reject) => { - requestBidsHook(() => { - didHookReturn = true; - }, {}); - setTimeout(() => didHookReturn ? resolve() : reject(new Error('Auction did not run')), 20); - }) - } - - function mockGppCmp(gppdata) { - gppStub.callsFake((api, cb) => { - if (api === 'addEventListener') { - // eslint-disable-next-line standard/no-callback-literal - cb({ - pingData: { - cmpStatus: 'loaded' - } - }, true); - } - if (api === 'getGPPData') { - return gppdata; - } - }); - } - - beforeEach(() => { - gppStub = sinon.stub(window, '__gpp'); - }); - - afterEach(() => { - gppStub.restore(); - }) - - it('should continue auction with null consent when CMP is unresponsive', () => { - return runAuction().then(() => { - const consent = gppDataHandler.getConsentData(); - expect(consent.applicableSections).to.deep.equal([]); - expect(consent.gppString).to.be.undefined; - expect(gppDataHandler.ready).to.be.true; - }); - }); - - it('should use consent provided by events other than sectionChange', () => { - mockGppCmp({ - gppString: 'mock-consent-string', - applicableSections: [7] - }); - return runAuction().then(() => { - const consent = gppDataHandler.getConsentData(); - expect(consent.applicableSections).to.deep.equal([7]); - expect(consent.gppString).to.equal('mock-consent-string'); - expect(gppDataHandler.ready).to.be.true; - }); - }); - }); - }); - }); }); }); From 0525202626a71ec340cfd271ae637de0126fac57 Mon Sep 17 00:00:00 2001 From: ccorbo Date: Wed, 16 Aug 2023 13:33:41 -0400 Subject: [PATCH 62/88] fix: consolidate banner format array (#10365) Co-authored-by: Chris Corbo --- modules/ixBidAdapter.js | 8 +++--- test/spec/modules/ixBidAdapter_spec.js | 37 +++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index d6ac3f3f9a4..d083fb46798 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -168,6 +168,7 @@ const MEDIA_TYPES = { function bidToBannerImp(bid) { const imp = bidToImp(bid, BANNER); imp.banner = {}; + imp.adunitCode = bid.adUnitCode; const impSize = deepAccess(bid, 'params.size'); if (impSize) { imp.banner.w = impSize[0]; @@ -344,7 +345,6 @@ function bidToImp(bid, mediaType) { imp.id = bid.bidId; imp.ext = {}; - if (deepAccess(bid, `params.${mediaType}.siteId`) && !isNaN(Number(bid.params[mediaType].siteId))) { switch (mediaType) { case BANNER: @@ -955,10 +955,10 @@ function addImpressions(impressions, impKeys, r, adUnitIndex) { if (bannerImpressions.length > 0) { const bannerImpsKeyed = bannerImpressions.reduce((acc, bannerImp) => { - if (!acc[bannerImp.id]) { - acc[bannerImp.id] = [] + if (!acc[bannerImp.adunitCode]) { + acc[bannerImp.adunitCode] = [] } - acc[bannerImp.id].push(bannerImp); + acc[bannerImp.adunitCode].push(bannerImp); return acc; }, {}); for (const impId in bannerImpsKeyed) { diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 7f5b882fed2..eee0335524d 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -1983,6 +1983,37 @@ describe('IndexexchangeAdapter', function () { }); }); + it('multi-configured size params should have the correct imp[].banner.format[].ext.siteID', function () { + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + const bid2 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.params.siteId = 1234; + bid1.bidId = '27fc897708d826'; + bid2.params.siteId = 4321; + bid2.bidId = '34df030c33dc68'; + bid2.params.size = [300, 600]; + request = spec.buildRequests([bid1, bid2], DEFAULT_OPTION)[0]; + + const payload = extractPayload(request); + expect(payload.imp[0].banner.format[0].ext.siteID).to.equal('1234'); + expect(payload.imp[0].banner.format[1].ext.siteID).to.equal('4321'); + }); + + it('multi-configured size params should be added to the imp[].banner.format[] array', function () { + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + const bid2 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.params.siteId = 1234; + bid1.bidId = '27fc897708d826'; + bid2.params.siteId = 4321; + bid2.bidId = '34df030c33dc68'; + bid2.params.size = [300, 600]; + request = spec.buildRequests([bid1, bid2], DEFAULT_OPTION)[0]; + + const payload = extractPayload(request); + expect(payload.imp[0].banner.format.length).to.equal(2); + expect(`${payload.imp[0].banner.format[0].w}x${payload.imp[0].banner.format[0].h}`).to.equal('300x250'); + expect(`${payload.imp[0].banner.format[1].w}x${payload.imp[0].banner.format[1].h}`).to.equal('300x600'); + }); + describe('build requests with price floors', () => { const highFloor = 4.5; const lowFloor = 3.5; @@ -4121,8 +4152,8 @@ describe('IndexexchangeAdapter', function () { const bids = [DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0], DEFAULT_MULTIFORMAT_NATIVE_VALID_BID[0]]; bids[0].params.bidFloor = 2.05; bids[0].params.bidFloorCur = 'USD'; - let tid = bids[1].transactionId; - bids[1].transactionId = bids[0].transactionId; + let adunitcode = bids[1].adUnitCode; + bids[1].adUnitCode = bids[0].adUnitCode; bids[1].params.bidFloor = 2.35; bids[1].params.bidFloorCur = 'USD'; const request = spec.buildRequests(bids, {}); @@ -4131,7 +4162,7 @@ describe('IndexexchangeAdapter', function () { expect(extractPayload(request[0]).imp[0].bidfloor).to.equal(2.05); expect(extractPayload(request[0]).imp[0].bidfloorcur).to.equal('USD'); expect(extractPayload(request[0]).imp[0].native.ext.bidfloor).to.equal(2.35); - bids[1].transactionId = tid; + bids[1].adUnitCode = adunitcode; }); it('should return valid banner and video requests, different adunit, creates multiimp request', function () { From 57f2ac8236e6064c120ac9a616ea3638649c7df0 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 16 Aug 2023 11:53:54 -0700 Subject: [PATCH 63/88] Prebid Server adapter: improve cookie_sync tests, check GPP fields are populated (#10362) --- .../modules/prebidServerBidAdapter_spec.js | 375 +++++++----------- 1 file changed, 150 insertions(+), 225 deletions(-) diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 1626d6f2c9d..ad6c4318cc7 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -879,7 +879,7 @@ describe('S2S Adapter', function () { expect(adapter.callBids).to.exist.and.to.be.a('function'); }); - function mockConsent({applies = true, hasP1Consent = true} = {}) { + function mockTCF({applies = true, hasP1Consent = true} = {}) { return { consentString: 'mockConsent', gdprApplies: applies, @@ -897,7 +897,7 @@ describe('S2S Adapter', function () { config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = mockConsent(); + gdprBidRequest[0].gdprConsent = mockTCF(); adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, gdprBidRequest), gdprBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); @@ -920,7 +920,7 @@ describe('S2S Adapter', function () { config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = Object.assign(mockConsent(), { + gdprBidRequest[0].gdprConsent = Object.assign(mockTCF(), { addtlConsent: 'superduperconsent', }); @@ -940,69 +940,6 @@ describe('S2S Adapter', function () { expect(requestBid.regs).to.not.exist; expect(requestBid.user).to.not.exist; }); - - it('check gdpr info gets added into cookie_sync request: have consent data', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); - - let gdprBidRequest = utils.deepClone(BID_REQUESTS); - - gdprBidRequest[0].gdprConsent = mockConsent(); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.gdpr).is.equal(1); - expect(requestBid.gdpr_consent).is.equal('mockConsent'); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - }); - - it('check gdpr info gets added into cookie_sync request: have consent data but gdprApplies is false', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = mockConsent({applies: false}); - - adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.gdpr).is.equal(0); - expect(requestBid.gdpr_consent).is.undefined; - }); - - it('checks gdpr info gets added to cookie_sync request: applies is false', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); - - let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = mockConsent({applies: false}); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.gdpr).is.equal(0); - expect(requestBid.gdpr_consent).is.undefined; - }); }); describe('us_privacy (ccpa) consent data', function () { @@ -1029,25 +966,6 @@ describe('S2S Adapter', function () { expect(requestBid.regs).to.not.exist; }); - - it('is added to cookie_sync request when in bidRequest', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - config.setConfig({ s2sConfig: cookieSyncConfig }); - - let uspBidRequest = utils.deepClone(BID_REQUESTS); - uspBidRequest[0].uspConsent = '1YNN'; - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - adapter.callBids(s2sBidRequest, uspBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.us_privacy).is.equal('1YNN'); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - }); }); describe('gdpr and us_privacy (ccpa) consent data', function () { @@ -1060,7 +978,7 @@ describe('S2S Adapter', function () { let consentBidRequest = utils.deepClone(BID_REQUESTS); consentBidRequest[0].uspConsent = '1NYN'; - consentBidRequest[0].gdprConsent = mockConsent(); + consentBidRequest[0].gdprConsent = mockTCF(); adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, consentBidRequest), consentBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); @@ -1086,7 +1004,7 @@ describe('S2S Adapter', function () { let consentBidRequest = utils.deepClone(BID_REQUESTS); consentBidRequest[0].uspConsent = '1YNN'; - consentBidRequest[0].gdprConsent = mockConsent(); + consentBidRequest[0].gdprConsent = mockTCF(); const s2sBidRequest = utils.deepClone(REQUEST); s2sBidRequest.s2sConfig = cookieSyncConfig @@ -1843,170 +1761,177 @@ describe('S2S Adapter', function () { }]); }); - describe('filterSettings', function () { - const getRequestBid = userSync => { - let cookieSyncConfig = utils.deepClone(CONFIG); - const s2sBidRequest = utils.deepClone(REQUEST); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - s2sBidRequest.s2sConfig = cookieSyncConfig; + describe('cookie sync', () => { + let s2sConfig, bidderReqs; - config.setConfig({ userSync, s2sConfig: cookieSyncConfig }); + beforeEach(() => { + bidderReqs = utils.deepClone(BID_REQUESTS); + s2sConfig = utils.deepClone(CONFIG); + s2sConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; + }) - let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); + function callCookieSync() { + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + config.setConfig({ s2sConfig: s2sConfig }); + adapter.callBids(s2sBidRequest, bidderReqs, addBidResponse, done, ajax); return JSON.parse(server.requests[0].requestBody); } - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the all key is present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - all: { - bidders: ['appnexus', 'rubicon', 'pubmatic'], - filter: 'exclude' + describe('filterSettings', function () { + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the all key is present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + all: { + bidders: ['appnexus', 'rubicon', 'pubmatic'], + filter: 'exclude' + } + } } - } - }; - const requestBid = getRequestBid(userSync); - - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': ['appnexus', 'rubicon', 'pubmatic'], - 'filter': 'exclude' - }, - 'iframe': { - 'bidders': ['appnexus', 'rubicon', 'pubmatic'], - 'filter': 'exclude' - } + }); + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': ['appnexus', 'rubicon', 'pubmatic'], + 'filter': 'exclude' + }, + 'iframe': { + 'bidders': ['appnexus', 'rubicon', 'pubmatic'], + 'filter': 'exclude' + } + }); }); - }); - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the iframe key is present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - iframe: { - bidders: ['rubicon', 'pubmatic'], - filter: 'include' + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the iframe key is present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + iframe: { + bidders: ['rubicon', 'pubmatic'], + filter: 'include' + } + } } - } - }; - const requestBid = getRequestBid(userSync); + }) - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': '*', - 'filter': 'include' - }, - 'iframe': { - 'bidders': ['rubicon', 'pubmatic'], - 'filter': 'include' - } + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': '*', + 'filter': 'include' + }, + 'iframe': { + 'bidders': ['rubicon', 'pubmatic'], + 'filter': 'include' + } + }); }); - }); - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the image and iframe keys are both present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - image: { - bidders: ['triplelift', 'appnexus'], - filter: 'include' - }, - iframe: { - bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - filter: 'exclude' + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the image and iframe keys are both present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + image: { + bidders: ['triplelift', 'appnexus'], + filter: 'include' + }, + iframe: { + bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + filter: 'exclude' + } + } } - } - }; - const requestBid = getRequestBid(userSync); + }) - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': ['triplelift', 'appnexus'], - 'filter': 'include' - }, - 'iframe': { - 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - 'filter': 'exclude' - } + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': ['triplelift', 'appnexus'], + 'filter': 'include' + }, + 'iframe': { + 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + 'filter': 'exclude' + } + }); }); - }); - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the all and iframe keys are both present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - all: { - bidders: ['triplelift', 'appnexus'], - filter: 'include' - }, - iframe: { - bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - filter: 'exclude' + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the all and iframe keys are both present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + all: { + bidders: ['triplelift', 'appnexus'], + filter: 'include' + }, + iframe: { + bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + filter: 'exclude' + } + } } - } - }; - const requestBid = getRequestBid(userSync); + }) - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': ['triplelift', 'appnexus'], - 'filter': 'include' - }, - 'iframe': { - 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - 'filter': 'exclude' - } + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': ['triplelift', 'appnexus'], + 'filter': 'include' + }, + 'iframe': { + 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + 'filter': 'exclude' + } + }); }); }); - }); - it('adds limit to the cookie_sync request if userSyncLimit is greater than 0', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - cookieSyncConfig.userSyncLimit = 1; - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - config.setConfig({ s2sConfig: cookieSyncConfig }); - - let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - expect(requestBid.limit).is.equal(1); - }); - - it('does not add limit to cooke_sync request if userSyncLimit is missing or 0', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - config.setConfig({ s2sConfig: cookieSyncConfig }); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); + describe('limit', () => { + it('is added to request if userSyncLimit is greater than 0', function () { + s2sConfig.userSyncLimit = 1; + const req = callCookieSync(); + expect(req.limit).is.equal(1); + }); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - expect(requestBid.limit).is.undefined; + Object.entries({ + 'missing': () => null, + '0': () => { s2sConfig.userSyncLimit = 0; } + }).forEach(([t, setup]) => { + it(`is not added to request if userSyncLimit is ${t}`, () => { + setup(); + const req = callCookieSync(); + expect(req.limit).to.not.exist; + }); + }); + }); - cookieSyncConfig.userSyncLimit = 0; - config.resetConfig(); - config.setConfig({ s2sConfig: cookieSyncConfig }); + describe('gdpr data is set', () => { + it('when we have consent data', function () { + bidderReqs[0].gdprConsent = mockTCF(); + const req = callCookieSync(); + expect(req.gdpr).is.equal(1); + expect(req.gdpr_consent).is.equal('mockConsent'); + }); - const s2sBidRequest2 = utils.deepClone(REQUEST); - s2sBidRequest2.s2sConfig = cookieSyncConfig; + it('when gdprApplies is false', () => { + bidderReqs[0].gdprConsent = mockTCF({applies: false}); + const req = callCookieSync(); + expect(req.gdpr).is.equal(0); + expect(req.gdpr_consent).is.undefined; + }); + }); - bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest2, bidRequest, addBidResponse, done, ajax); - requestBid = JSON.parse(server.requests[0].requestBody); + it('adds USP data from bidder request', () => { + bidderReqs[0].uspConsent = '1YNN'; + expect(callCookieSync().us_privacy).to.equal('1YNN'); + }); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - expect(requestBid.limit).is.undefined; + it('adds GPP data from bidder requests', () => { + bidderReqs[0].gppConsent = { + applicableSections: [1, 2, 3], + gppString: 'mock-string' + }; + const req = callCookieSync(); + expect(req.gpp).to.eql('mock-string'); + expect(req.gpp_sid).to.eql('1,2,3'); + }); }); it('adds s2sConfig adapterOptions to request for ORTB', function () { From 8d6ca3e1a8bb1b72df1cece8490cac209893a305 Mon Sep 17 00:00:00 2001 From: Yoko OYAMA Date: Thu, 17 Aug 2023 20:47:55 +0900 Subject: [PATCH 64/88] fluct Bid Adapter: add user.data to bid requests (#10318) * no filter eids by source * Update fluctBidAdapter_spec.js kick off cirleci tests * add user.data * fix Object.assign side-effect * merge ortb2.user.ext.eids into user.eids * replace || w/ ?? * run circleci * kick off tests * kick --------- Co-authored-by: Chris Huie --- modules/fluctBidAdapter.js | 17 ++--- test/spec/modules/fluctBidAdapter_spec.js | 86 +++++++++++++++++++---- 2 files changed, 79 insertions(+), 24 deletions(-) diff --git a/modules/fluctBidAdapter.js b/modules/fluctBidAdapter.js index b7cabfa95a0..b566769c00e 100644 --- a/modules/fluctBidAdapter.js +++ b/modules/fluctBidAdapter.js @@ -8,16 +8,6 @@ const VERSION = '1.2'; const NET_REVENUE = true; const TTL = 300; -/** - * See modules/userId/eids.js for supported sources - */ -const SUPPORTED_USER_ID_SOURCES = [ - 'adserver.org', - 'criteo.com', - 'intimatemerger.com', - 'liveramp.com', -]; - export const spec = { code: BIDDER_CODE, aliases: ['adingo'], @@ -36,6 +26,7 @@ export const spec = { * Make a server request from the list of BidRequests. * * @param {validBidRequests[]} - an array of bids. + * @param {bidderRequest} bidderRequest bidder request object. * @return ServerRequest Info describing the request to the server. */ buildRequests: (validBidRequests, bidderRequest) => { @@ -50,7 +41,11 @@ export const spec = { data.adUnitCode = request.adUnitCode; data.bidId = request.bidId; data.user = { - eids: (request.userIdAsEids || []).filter((eid) => SUPPORTED_USER_ID_SOURCES.indexOf(eid.source) !== -1) + data: bidderRequest.ortb2?.user?.data ?? [], + eids: [ + ...(request.userIdAsEids ?? []), + ...(bidderRequest.ortb2?.user?.ext?.eids ?? []), + ], }; if (impExt) { diff --git a/test/spec/modules/fluctBidAdapter_spec.js b/test/spec/modules/fluctBidAdapter_spec.js index ca0f89da10d..ff6f8562a4e 100644 --- a/test/spec/modules/fluctBidAdapter_spec.js +++ b/test/spec/modules/fluctBidAdapter_spec.js @@ -192,14 +192,14 @@ describe('fluctAdapter', function () { expect(request.data.regs).to.eql(undefined); }); - it('includes filtered user.eids if any exist', function () { + it('includes filtered user.eids if any exists', function () { const bidRequests2 = bidRequests.map( - (bidReq) => Object.assign(bidReq, { + (bidReq) => Object.assign({}, bidReq, { userIdAsEids: [ { source: 'foobar.com', uids: [ - { id: 'foobar-id' } + { id: 'foobar-id' }, ], }, { @@ -211,19 +211,19 @@ describe('fluctAdapter', function () { { source: 'criteo.com', uids: [ - { id: 'criteo-id' } + { id: 'criteo-id' }, ], }, { source: 'intimatemerger.com', uids: [ - { id: 'imuid' } + { id: 'imuid' }, ], }, { source: 'liveramp.com', uids: [ - { id: 'idl-env' } + { id: 'idl-env' }, ], }, ], @@ -231,36 +231,96 @@ describe('fluctAdapter', function () { ); const request = spec.buildRequests(bidRequests2, bidderRequest)[0]; expect(request.data.user.eids).to.eql([ + { + source: 'foobar.com', + uids: [ + { id: 'foobar-id' }, + ], + }, { source: 'adserver.org', uids: [ - { id: 'tdid' } + { id: 'tdid' }, ], }, { source: 'criteo.com', uids: [ - { id: 'criteo-id' } + { id: 'criteo-id' }, ], }, { source: 'intimatemerger.com', uids: [ - { id: 'imuid' } + { id: 'imuid' }, ], }, { source: 'liveramp.com', uids: [ - { id: 'idl-env' } + { id: 'idl-env' }, ], }, ]); }); + it('includes user.data if any exists', function () { + const bidderRequest2 = Object.assign({}, bidderRequest, { + ortb2: { + user: { + data: [ + { + name: 'a1mediagroup.com', + ext: { + segtax: 900, + }, + segment: [ + { id: 'seg-1' }, + { id: 'seg-2' }, + ], + }, + ], + ext: { + eids: [ + { + source: 'a1mediagroup.com', + uids: [ + { id: 'aud-1' } + ], + }, + ], + }, + }, + }, + }); + const request = spec.buildRequests(bidRequests, bidderRequest2)[0]; + expect(request.data.user).to.eql({ + data: [ + { + name: 'a1mediagroup.com', + ext: { + segtax: 900, + }, + segment: [ + { id: 'seg-1' }, + { id: 'seg-2' }, + ], + }, + ], + eids: [ + { + source: 'a1mediagroup.com', + uids: [ + { id: 'aud-1' } + ], + }, + ], + }); + }); + it('includes data.params.kv if any exists', function () { const bidRequests2 = bidRequests.map( - (bidReq) => Object.assign(bidReq, { + (bidReq) => Object.assign({}, bidReq, { params: { kv: { imsids: ['imsid1', 'imsid2'] @@ -277,7 +337,7 @@ describe('fluctAdapter', function () { it('includes data.schain if any exists', function () { // this should be done by schain.js const bidRequests2 = bidRequests.map( - (bidReq) => Object.assign(bidReq, { + (bidReq) => Object.assign({}, bidReq, { schain: { ver: '1.0', complete: 1, @@ -344,7 +404,7 @@ describe('fluctAdapter', function () { }); }); - describe('interpretResponse', function() { + describe('should interpretResponse', function() { const callBeaconSnippet = '