From 854afb57e343ce1b9c676128d375b5b5e473b6ad Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 17 Apr 2024 08:29:46 -0700 Subject: [PATCH 01/25] refactor size logic --- libraries/ortbConverter/lib/sizes.js | 14 ------- libraries/ortbConverter/processors/banner.js | 12 ++++-- libraries/ortbConverter/processors/video.js | 5 +-- src/utils.js | 44 ++++++++++++++------ test/spec/utils_spec.js | 39 ++++++++++++++++- 5 files changed, 80 insertions(+), 34 deletions(-) delete mode 100644 libraries/ortbConverter/lib/sizes.js diff --git a/libraries/ortbConverter/lib/sizes.js b/libraries/ortbConverter/lib/sizes.js deleted file mode 100644 index 16b75048203..00000000000 --- a/libraries/ortbConverter/lib/sizes.js +++ /dev/null @@ -1,14 +0,0 @@ -import {parseSizesInput} from '../../../src/utils.js'; - -export function sizesToFormat(sizes) { - sizes = parseSizesInput(sizes); - - // get sizes in form [{ w: , h: }, ...] - return sizes.map(size => { - const [width, height] = size.split('x'); - return { - w: parseInt(width, 10), - h: parseInt(height, 10) - }; - }); -} diff --git a/libraries/ortbConverter/processors/banner.js b/libraries/ortbConverter/processors/banner.js index 51c93b652ef..d5c7ded7845 100644 --- a/libraries/ortbConverter/processors/banner.js +++ b/libraries/ortbConverter/processors/banner.js @@ -1,6 +1,12 @@ -import {createTrackPixelHtml, deepAccess, inIframe, mergeDeep} from '../../../src/utils.js'; +import { + createTrackPixelHtml, + deepAccess, + inIframe, + mergeDeep, + sizesToSizeTuples, + sizeTupleToRtbSize +} from '../../../src/utils.js'; import {BANNER} from '../../../src/mediaTypes.js'; -import {sizesToFormat} from '../lib/sizes.js'; /** * fill in a request `imp` with banner parameters from `bidRequest`. @@ -14,7 +20,7 @@ export function fillBannerImp(imp, bidRequest, context) { topframe: inIframe() === true ? 0 : 1 }; if (bannerParams.sizes) { - banner.format = sizesToFormat(bannerParams.sizes); + banner.format = sizesToSizeTuples(bannerParams.sizes).map(sizeTupleToRtbSize); } if (bannerParams.hasOwnProperty('pos')) { banner.pos = bannerParams.pos; diff --git a/libraries/ortbConverter/processors/video.js b/libraries/ortbConverter/processors/video.js index c38231d9002..b10ad4032c5 100644 --- a/libraries/ortbConverter/processors/video.js +++ b/libraries/ortbConverter/processors/video.js @@ -1,6 +1,5 @@ -import {deepAccess, isEmpty, logWarn, mergeDeep} from '../../../src/utils.js'; +import {deepAccess, isEmpty, logWarn, mergeDeep, sizesToSizeTuples, sizeTupleToRtbSize} from '../../../src/utils.js'; import {VIDEO} from '../../../src/mediaTypes.js'; -import {sizesToFormat} from '../lib/sizes.js'; // parameters that share the same name & semantics between pbjs adUnits and imp.video const ORTB_VIDEO_PARAMS = new Set([ @@ -41,7 +40,7 @@ export function fillVideoImp(imp, bidRequest, context) { .filter(([name]) => ORTB_VIDEO_PARAMS.has(name)) ); if (videoParams.playerSize) { - const format = sizesToFormat(videoParams.playerSize); + const format = sizesToSizeTuples(videoParams.playerSize).map(sizeTupleToRtbSize); if (format.length > 1) { logWarn('video request specifies more than one playerSize; all but the first will be ignored') } diff --git a/src/utils.js b/src/utils.js index 71cc78090a6..b06db4283f1 100644 --- a/src/utils.js +++ b/src/utils.js @@ -129,37 +129,55 @@ export function transformAdServerTargetingObj(targeting) { } /** - * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']' - * @param {(Array.|Array.)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]] - * @return {Array.} Array of strings like `["300x250"]` or `["300x250", "728x90"]` + * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of width, height tuples `[[300, 250]]` or '[[300,250], [970,90]]' */ -export function parseSizesInput(sizeObj) { - if (typeof sizeObj === 'string') { +export function sizesToSizeTuples(sizes) { + if (typeof sizes === 'string') { // multiple sizes will be comma-separated - return sizeObj.split(',').filter(sz => sz.match(/^(\d)+x(\d)+$/i)) - } else if (typeof sizeObj === 'object') { - if (sizeObj.length === 2 && typeof sizeObj[0] === 'number' && typeof sizeObj[1] === 'number') { - return [parseGPTSingleSizeArray(sizeObj)]; - } else { - return sizeObj.map(parseGPTSingleSizeArray) + return sizes + .split(/\s*,\s*/) + .map(sz => sz.match(/^(\d+)x(\d+)$/i)) + .filter(match => match) + .map(([_, w, h]) => [parseInt(w, 10), parseInt(h, 10)]) + } else if (Array.isArray(sizes)) { + if (isValidGPTSingleSize(sizes)) { + return [sizes] } + return sizes.filter(isValidGPTSingleSize); } return []; } +/** + * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']' + * @param {(Array.|Array.)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]] + * @return {Array.} Array of strings like `["300x250"]` or `["300x250", "728x90"]` + */ +export function parseSizesInput(sizeObj) { + return sizesToSizeTuples(sizeObj).map(sizeTupleToSizeString); +} + +function sizeTupleToSizeString(size) { + return size[0] + 'x' + size[1] +} + // Parse a GPT style single size array, (i.e [300, 250]) // into an AppNexus style string, (i.e. 300x250) export function parseGPTSingleSizeArray(singleSize) { if (isValidGPTSingleSize(singleSize)) { - return singleSize[0] + 'x' + singleSize[1]; + return sizeTupleToSizeString(singleSize); } } +export function sizeTupleToRtbSize(size) { + return {w: size[0], h: size[1]}; +} + // Parse a GPT style single size array, (i.e [300, 250]) // into OpenRTB-compatible (imp.banner.w/h, imp.banner.format.w/h, imp.video.w/h) object(i.e. {w:300, h:250}) export function parseGPTSingleSizeArrayToRtbSize(singleSize) { if (isValidGPTSingleSize(singleSize)) { - return {w: singleSize[0], h: singleSize[1]}; + return sizeTupleToRtbSize(singleSize) } } diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index e0a114d8cf6..56e03b4d730 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -3,7 +3,7 @@ import {expect} from 'chai'; import { TARGETING_KEYS } from 'src/constants.js'; import * as utils from 'src/utils.js'; import {getHighestCpm, getLatestHighestCpmBid, getOldestHighestCpmBid} from '../../src/utils/reducers.js'; -import {binarySearch, deepEqual, memoize, waitForElementToLoad} from 'src/utils.js'; +import {binarySearch, deepEqual, memoize, sizesToSizeTuples, waitForElementToLoad} from 'src/utils.js'; import {convertCamelToUnderscore} from '../../libraries/appnexusUtils/anUtils.js'; var assert = require('assert'); @@ -119,6 +119,43 @@ describe('Utils', function () { }); }); + describe('sizesToSizeTuples', () => { + Object.entries({ + 'single size, numerical': { + in: [1, 2], + out: [[1, 2]] + }, + 'single size, numerical, nested': { + in: [[1, 2]], + out: [[1, 2]] + }, + 'multiple sizes, numerical': { + in: [[1, 2], [3, 4]], + out: [[1, 2], [3, 4]] + }, + 'single size, string': { + in: '1x2', + out: [[1, 2]] + }, + 'multiple sizes, string': { + in: '1x2, 4x3', + out: [[1, 2], [4, 3]] + }, + 'incorrect size, numerical': { + in: [1], + out: [] + }, + 'incorrect size, string': { + in: '1x', + out: [] + } + }).forEach(([t, {in: input, out}]) => { + it(`can parse ${t}`, () => { + expect(sizesToSizeTuples(input)).to.eql(out); + }) + }) + }) + describe('parseSizesInput', function () { it('should return query string using multi size array', function () { var sizes = [[728, 90], [970, 90]]; From 60159d2dbe4c32fd04b54ee15dbffbccfdf10696 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 17 Apr 2024 11:16:53 -0700 Subject: [PATCH 02/25] fill in requestedSize on auction configs --- modules/fledgeForGpt.js | 66 +++++++- modules/paapi.js | 51 ++++-- src/utils.js | 2 +- test/spec/modules/fledgeForGpt_spec.js | 30 +++- test/spec/modules/paapi_spec.js | 217 ++++++++++++++++++++----- 5 files changed, 300 insertions(+), 66 deletions(-) diff --git a/modules/fledgeForGpt.js b/modules/fledgeForGpt.js index bda4494faaf..e4ed55b1a8e 100644 --- a/modules/fledgeForGpt.js +++ b/modules/fledgeForGpt.js @@ -1,8 +1,8 @@ /** * GPT-specific slot configuration logic for PAAPI. */ -import {submodule} from '../src/hook.js'; -import {deepAccess, logInfo, logWarn} from '../src/utils.js'; +import {getHook, submodule} from '../src/hook.js'; +import {deepAccess, logInfo, logWarn, sizeTupleToSizeString} from '../src/utils.js'; import {getGptSlotForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; import {config} from '../src/config.js'; import {getGlobal} from '../src/prebidGlobal.js'; @@ -12,6 +12,7 @@ import {getGlobal} from '../src/prebidGlobal.js'; // TODO: remove this in prebid 9 // eslint-disable-next-line prebid/validate-imports import './paapi.js'; +import {keyCompare} from '../src/utils/reducers.js'; const MODULE = 'fledgeForGpt'; let getPAAPIConfig; @@ -73,6 +74,66 @@ export function onAuctionConfigFactory(setGptConfig = setComponentAuction) { } } +export const getPAAPISizeHook = (() => { + /* + https://github.com/google/ads-privacy/tree/master/proposals/fledge-multiple-seller-testing#faq + https://support.google.com/admanager/answer/1100453?hl=en + + Ignore any placeholder sizes, where placeholder is defined as a square creative with a side of <= 5 pixels + Look if there are any sizes that are part of the set of supported ad sizes defined here. If there are, choose the largest supported size by area (width * height) + For clarity, the set of supported ad sizes includes all of the ad sizes listed under “Top-performing ad sizes”, “Other supported ad sizes”, and “Regional ad sizes”. + If not, choose the largest remaining size (i.e. that isn’t in the list of supported ad sizes) by area (width * height) + */ + const SUPPORTED_SIZES = [ + [728, 90], + [336, 280], + [300, 250], + [300, 50], + [160, 600], + [1024, 768], + [970, 250], + [970, 90], + [768, 1024], + [480, 320], + [468, 60], + [320, 480], + [320, 100], + [320, 50], + [300, 600], + [300, 100], + [250, 250], + [234, 60], + [200, 200], + [180, 150], + [125, 125], + [120, 600], + [120, 240], + [120, 60], + [88, 31], + [980, 120], + [980, 90], + [950, 90], + [930, 180], + [750, 300], + [750, 200], + [750, 100], + [580, 400], + [250, 360], + [240, 400], + ].sort(keyCompare(([w, h]) => -(w * h))) + .map(size => [size, sizeTupleToSizeString(size)]); + + return function(next, sizes) { + const sizeStrings = new Set(sizes.map(sizeTupleToSizeString)); + const preferredSize = SUPPORTED_SIZES.find(([_, sizeStr]) => sizeStrings.has(sizeStr)); + if (preferredSize) { + next.bail(preferredSize[0]) + } else { + next(sizes); + } + } +})(); + export function setPAAPIConfigFactory( getConfig = (filters) => getPAAPIConfig(filters, true), setGptConfig = setComponentAuction) { @@ -105,5 +166,6 @@ submodule('paapi', { onAuctionConfig: onAuctionConfigFactory(), init(params) { getPAAPIConfig = params.getPAAPIConfig; + getHook('getPAAPISize').before(getPAAPISizeHook); } }); diff --git a/modules/paapi.js b/modules/paapi.js index 9122ecce1a0..59f9638e7a3 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -2,13 +2,13 @@ * Collect PAAPI component auction configs from bid adapters and make them available through `pbjs.getPAAPIConfig()` */ import {config} from '../src/config.js'; -import {getHook, module} from '../src/hook.js'; -import {deepSetValue, logInfo, logWarn, mergeDeep, parseSizesInput} from '../src/utils.js'; +import {getHook, hook, module} from '../src/hook.js'; +import {deepSetValue, logInfo, logWarn, mergeDeep, parseSizesInput, sizesToSizeTuples} from '../src/utils.js'; import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js'; import * as events from '../src/events.js'; import {EVENTS} from '../src/constants.js'; import {currencyCompare} from '../libraries/currencyUtils/currency.js'; -import {maximum, minimum} from '../src/utils/reducers.js'; +import {keyCompare, maximum, minimum} from '../src/utils/reducers.js'; import {auctionManager} from '../src/auctionManager.js'; import {getGlobal} from '../src/prebidGlobal.js'; @@ -69,7 +69,7 @@ getHook('addComponentAuction').before(addComponentAuctionHook); getHook('makeBidRequests').after(markForFledge); events.on(EVENTS.AUCTION_END, onAuctionEnd); -function getSlotSignals(bidsReceived = [], bidRequests = []) { +function getSlotSignals(adUnit = {}, bidsReceived = [], bidRequests = []) { let bidfloor, bidfloorcur; if (bidsReceived.length > 0) { const bestBid = bidsReceived.reduce(maximum(currencyCompare(bid => [bid.cpm, bid.currency]))); @@ -86,6 +86,10 @@ function getSlotSignals(bidsReceived = [], bidRequests = []) { deepSetValue(cfg, 'auctionSignals.prebid.bidfloor', bidfloor); bidfloorcur && deepSetValue(cfg, 'auctionSignals.prebid.bidfloorcur', bidfloorcur); } + const requestedSize = getRequestedSize(adUnit); + if (requestedSize) { + cfg.requestedSize = requestedSize; + } return cfg; } @@ -99,23 +103,11 @@ function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adU }); Object.entries(pendingForAuction(auctionId) || {}).forEach(([adUnitCode, auctionConfigs]) => { const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode; - const slotSignals = getSlotSignals(bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit)); + const slotSignals = getSlotSignals(adUnitsByCode[adUnitCode], bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit)); paapiConfigs[adUnitCode] = { ...slotSignals, componentAuctions: auctionConfigs.map(cfg => mergeDeep({}, slotSignals, cfg)) }; - // TODO: need to flesh out size treatment: - // - which size should the paapi auction pick? (this uses the first one defined) - // - should we signal it to SSPs, and how? - // - what should we do if adapters pick a different one? - // - what does size mean for video and native? - const size = parseSizesInput(adUnitsByCode[adUnitCode]?.mediaTypes?.banner?.sizes)?.[0]?.split('x'); - if (size) { - paapiConfigs[adUnitCode].requestedSize = { - width: size[0], - height: size[1], - }; - } latestAuctionForAdUnit[adUnitCode] = auctionId; }); configsForAuction(auctionId, paapiConfigs); @@ -192,6 +184,27 @@ function getFledgeConfig() { }; } +/** + * Given an array of size tuples, return the one that should be used for PAAPI. + */ +export const getPAAPISize = hook('sync', function(sizes) { + return sizes + .filter(([w, h]) => !(w === h && w <= 5)) + .reduce(maximum(keyCompare(([w, h]) => w * h))) +}, 'getPAAPISize'); + +function getRequestedSize(adUnit) { + return adUnit.ortb2Imp?.ext?.paapi?.requestedSize || (() => { + const size = getPAAPISize(sizesToSizeTuples(adUnit.mediaTypes?.banner?.sizes)); + if (size) { + return { + width: size[0].toString(), + height: size[1].toString() + } + } + })(); +} + export function markForFledge(next, bidderRequests) { if (isFledgeSupported()) { bidderRequests.forEach((bidderReq) => { @@ -200,6 +213,10 @@ export function markForFledge(next, bidderRequests) { Object.assign(bidderReq, {fledgeEnabled: enabled}); bidderReq.bids.forEach(bidReq => { deepSetValue(bidReq, 'ortb2Imp.ext.ae', bidReq.ortb2Imp?.ext?.ae ?? ae); + const requestedSize = getRequestedSize(bidReq); + if (requestedSize) { + deepSetValue(bidReq, 'ortb2Imp.ext.paapi.requestedSize', requestedSize) + } }); }); }); diff --git a/src/utils.js b/src/utils.js index b06db4283f1..d2eb7d13dad 100644 --- a/src/utils.js +++ b/src/utils.js @@ -157,7 +157,7 @@ export function parseSizesInput(sizeObj) { return sizesToSizeTuples(sizeObj).map(sizeTupleToSizeString); } -function sizeTupleToSizeString(size) { +export function sizeTupleToSizeString(size) { return size[0] + 'x' + size[1] } diff --git a/test/spec/modules/fledgeForGpt_spec.js b/test/spec/modules/fledgeForGpt_spec.js index 8ab11171121..7bac56246f0 100644 --- a/test/spec/modules/fledgeForGpt_spec.js +++ b/test/spec/modules/fledgeForGpt_spec.js @@ -1,4 +1,9 @@ -import {onAuctionConfigFactory, setPAAPIConfigFactory, slotConfigurator} from 'modules/fledgeForGpt.js'; +import { + getPAAPISizeHook, + onAuctionConfigFactory, + setPAAPIConfigFactory, + slotConfigurator +} from 'modules/fledgeForGpt.js'; import * as gptUtils from '../../../libraries/gptUtils/gptUtils.js'; import 'modules/appnexusBidAdapter.js'; import 'modules/rubiconBidAdapter.js'; @@ -173,5 +178,28 @@ describe('fledgeForGpt module', () => { sinon.assert.calledWith(setGptConfig, au, config?.componentAuctions ?? [], true); }) }); + }); + + describe('getPAAPISizeHook', () => { + let next; + beforeEach(() => { + next = sinon.stub(); + next.bail = sinon.stub(); + }); + + it('should pick largest supported size over larger unsupported size', () => { + getPAAPISizeHook(next, [[999, 999], [300, 250], [300, 600], [1234, 4321]]); + sinon.assert.calledWith(next.bail, [300, 600]); + }); + + Object.entries({ + 'present': [], + 'supported': [[123, 4], [321, 5]] + }).forEach(([t, sizes]) => { + it(`should defer to next when no size is ${t}`, () => { + getPAAPISizeHook(next, sizes); + sinon.assert.calledWith(next, sizes); + }) + }) }) }); diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js index c7d6d88bd12..5fedce95132 100644 --- a/test/spec/modules/paapi_spec.js +++ b/test/spec/modules/paapi_spec.js @@ -12,7 +12,7 @@ import { registerSubmodule, setImpExtAe, setResponseFledgeConfigs, - reset + reset, getPAAPISize } from 'modules/paapi.js'; import * as events from 'src/events.js'; import { EVENTS } from 'src/constants.js'; @@ -22,10 +22,20 @@ import {stubAuctionIndex} from '../../helpers/indexStub.js'; import {AuctionIndex} from '../../../src/auctionIndex.js'; describe('paapi module', () => { - let sandbox; - before(reset); + let sandbox, getPAAPISizeStub; + function getPAAPISizeHook(next, sizes) { + next.bail(getPAAPISizeStub(sizes)); + } + before(() => { + reset(); + getPAAPISize.before(getPAAPISizeHook); + }) + after(() => { + getPAAPISize.getHooks({hook: getPAAPISizeHook}).remove(); + }) beforeEach(() => { sandbox = sinon.sandbox.create(); + getPAAPISizeStub = sinon.stub(); }); afterEach(() => { sandbox.restore(); @@ -129,30 +139,6 @@ describe('paapi module', () => { expect(getPAAPIConfig({auctionId})).to.eql({}); }); - it('should use first size as requestedSize', () => { - addComponentAuctionHook(nextFnSpy, { - auctionId, - adUnitCode: 'au1', - }, fledgeAuctionConfig); - events.emit(EVENTS.AUCTION_END, { - auctionId, - adUnits: [ - { - code: 'au1', - mediaTypes: { - banner: { - sizes: [[200, 100], [300, 200]] - } - } - } - ] - }); - expect(getPAAPIConfig({auctionId}).au1.requestedSize).to.eql({ - width: '200', - height: '100' - }) - }) - it('should augment auctionSignals with FPD', () => { addComponentAuctionHook(nextFnSpy, { auctionId, @@ -330,6 +316,63 @@ describe('paapi module', () => { }); }); }); + + describe('requestedSize', () => { + let adUnit; + beforeEach(() => { + adUnit = { + code: 'au', + } + }) + function getConfig() { + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: adUnit.code}, fledgeAuctionConfig); + events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: [adUnit.code], adUnits: [adUnit]}) + return getPAAPIConfig()[adUnit.code]; + } + Object.entries({ + 'adUnit.ortb2Imp.ext.paapi.requestedSize'() { + adUnit.ortb2Imp = { + ext: { + paapi: { + requestedSize: { + width: '123', + height: '321' + } + } + } + } + }, + 'largest size'() { + getPAAPISizeStub.returns([123, 321]); + } + }).forEach(([t, setup]) => { + describe(`should be set from ${t}`, () => { + beforeEach(setup); + + it('without overriding component auctions, if set', () => { + fledgeAuctionConfig.requestedSize = {width: '1', height: '2'}; + expect(getConfig().componentAuctions[0].requestedSize).to.eql({ + width: '1', + height: '2' + }) + }); + + it('on component auction, if missing', () => { + expect(getConfig().componentAuctions[0].requestedSize).to.eql({ + width: '123', + height: '321' + }); + }); + + it('on top level auction', () => { + expect(getConfig().requestedSize).to.eql({ + width: '123', + height: '321' + }) + }) + }); + }); + }); }); describe('with multiple auctions', () => { @@ -442,6 +485,7 @@ describe('paapi module', () => { describe('markForFledge', function () { const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]])); + let adUnits; before(function () { // navigator.runAdAuction & co may not exist, so we can't stub it normally with @@ -457,29 +501,33 @@ describe('paapi module', () => { Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig); }); + beforeEach(() => { + getPAAPISizeStub = sinon.stub(); + adUnits = [{ + 'code': '/19968336/header-bid-tag1', + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + }, + }, + 'bids': [ + { + 'bidder': 'appnexus', + }, + { + 'bidder': 'rubicon', + }, + ] + }]; + }) + afterEach(function () { config.resetConfig(); }); - const adUnits = [{ - 'code': '/19968336/header-bid-tag1', - 'mediaTypes': { - 'banner': { - 'sizes': [[728, 90]] - }, - }, - 'bids': [ - { - 'bidder': 'appnexus', - }, - { - 'bidder': 'rubicon', - }, - ] - }]; - function expectFledgeFlags(...enableFlags) { - const bidRequests = Object.fromEntries( + function mark() { + return Object.fromEntries( adapterManager.makeBidRequests( adUnits, Date.now(), @@ -489,6 +537,10 @@ describe('paapi module', () => { [] ).map(b => [b.bidderCode, b]) ); + } + + function expectFledgeFlags(...enableFlags) { + const bidRequests = mark(); expect(bidRequests.appnexus.fledgeEnabled).to.eql(enableFlags[0].enabled); bidRequests.appnexus.bids.forEach(bid => expect(bid.ortb2Imp.ext.ae).to.eql(enableFlags[0].ae)); @@ -546,10 +598,85 @@ describe('paapi module', () => { expectFledgeFlags({enabled: true, ae: 0}, {enabled: true, ae: 0}); }); }); + + describe('ortb2Imp.ext.paapi.requestedSize', () => { + beforeEach(() => { + config.setConfig({ + [configNS]: { + enabled: true, + defaultForSlots: 1, + } + }); + }); + + it('should default to value returned by getPAAPISize', () => { + getPAAPISizeStub.returns([123, 321]); + Object.values(mark()).flatMap(b => b.bids).forEach(bidRequest => { + sinon.assert.match(bidRequest.ortb2Imp.ext.paapi, { + requestedSize: { + width: '123', + height: '321' + } + }) + }); + }); + + it('should not be overridden, if provided by the pub', () => { + adUnits[0].ortb2Imp = { + ext: { + paapi: { + requestedSize: { + width: '123px', + height: '321px' + } + } + } + } + Object.values(mark()).flatMap(b => b.bids).forEach(bidRequest => { + sinon.assert.match(bidRequest.ortb2Imp.ext.paapi, { + requestedSize: { + width: '123px', + height: '321px' + } + }) + }); + sinon.assert.notCalled(getPAAPISizeStub); + }); + + it('should not be set if adUnit has no banner sizes', () => { + adUnits[0].mediaTypes = { + video: {} + }; + Object.values(mark()).flatMap(b => b.bids).forEach(bidRequest => { + expect(bidRequest.ortb2Imp?.ext?.paapi?.requestedSize).to.not.exist; + }); + }); + }) }); }); }); + describe('getPAAPISize', () => { + before(() => { + getPAAPISize.getHooks().remove(); + }); + + Object.entries({ + 'ignores placeholders': { + in: [[1, 1], [0, 0], [3, 4]], + out: [3, 4] + }, + 'picks largest size by area': { + in: [[200, 100], [150, 150]], + out: [150, 150] + } + }).forEach(([t, {in: input, out}]) => { + it(t, () => { + expect(getPAAPISize(input)).to.eql(out); + }) + }) + }); + describe('ortb processors for fledge', () => { it('imp.ext.ae should be removed if fledge is not enabled', () => { const imp = {ext: {ae: 1}}; From 5650e3e01a10b15766cc64fbaa88aac0dd67bada Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 17 Apr 2024 13:05:10 -0700 Subject: [PATCH 03/25] topLevelPaapi --- modules/fledgeForGpt.js | 14 +++-- modules/paapi.js | 12 ++-- modules/topLevelPaapi.js | 26 ++++++++ test/spec/modules/fledgeForGpt_spec.js | 3 +- test/spec/modules/paapi_spec.js | 40 +++++++++---- test/spec/modules/topLevelPaapi_spec.js | 79 +++++++++++++++++++++++++ 6 files changed, 149 insertions(+), 25 deletions(-) create mode 100644 modules/topLevelPaapi.js create mode 100644 test/spec/modules/topLevelPaapi_spec.js diff --git a/modules/fledgeForGpt.js b/modules/fledgeForGpt.js index e4ed55b1a8e..a356785dbe1 100644 --- a/modules/fledgeForGpt.js +++ b/modules/fledgeForGpt.js @@ -124,13 +124,15 @@ export const getPAAPISizeHook = (() => { .map(size => [size, sizeTupleToSizeString(size)]); return function(next, sizes) { - const sizeStrings = new Set(sizes.map(sizeTupleToSizeString)); - const preferredSize = SUPPORTED_SIZES.find(([_, sizeStr]) => sizeStrings.has(sizeStr)); - if (preferredSize) { - next.bail(preferredSize[0]) - } else { - next(sizes); + if (sizes?.length) { + const sizeStrings = new Set(sizes.map(sizeTupleToSizeString)); + const preferredSize = SUPPORTED_SIZES.find(([_, sizeStr]) => sizeStrings.has(sizeStr)); + if (preferredSize) { + next.bail(preferredSize[0]); + return; + } } + next(sizes); } })(); diff --git a/modules/paapi.js b/modules/paapi.js index 59f9638e7a3..f2f0161bfca 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -3,7 +3,7 @@ */ import {config} from '../src/config.js'; import {getHook, hook, module} from '../src/hook.js'; -import {deepSetValue, logInfo, logWarn, mergeDeep, parseSizesInput, sizesToSizeTuples} from '../src/utils.js'; +import {deepSetValue, logInfo, logWarn, mergeDeep, sizesToSizeTuples} from '../src/utils.js'; import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js'; import * as events from '../src/events.js'; import {EVENTS} from '../src/constants.js'; @@ -187,10 +187,12 @@ function getFledgeConfig() { /** * Given an array of size tuples, return the one that should be used for PAAPI. */ -export const getPAAPISize = hook('sync', function(sizes) { - return sizes - .filter(([w, h]) => !(w === h && w <= 5)) - .reduce(maximum(keyCompare(([w, h]) => w * h))) +export const getPAAPISize = hook('sync', function (sizes) { + if (sizes?.length) { + return sizes + .filter(([w, h]) => !(w === h && w <= 5)) + .reduce(maximum(keyCompare(([w, h]) => w * h))); + } }, 'getPAAPISize'); function getRequestedSize(adUnit) { diff --git a/modules/topLevelPaapi.js b/modules/topLevelPaapi.js new file mode 100644 index 00000000000..85721651194 --- /dev/null +++ b/modules/topLevelPaapi.js @@ -0,0 +1,26 @@ +import {submodule} from '../src/hook.js'; +import {config} from '../src/config.js'; +import {mergeDeep} from '../src/utils.js'; + +let getPAAPIConfig, moduleConfig; + +config.getConfig('paapi', (cfg) => { + moduleConfig = cfg.paapi?.topLevelSeller; +}) + +function onAuctionConfig(auctionId, aucitonConfigs) { + if (moduleConfig) { + Object.values(aucitonConfigs).forEach(auctionConfig => { + mergeDeep(auctionConfig, moduleConfig?.auctionConfig || {}, auctionConfig); + }) + } +} + +export const topLevelPAAPI = { + name: 'topLevelPAAPI', + init(params) { + getPAAPIConfig = params.getPAAPIConfig; + }, + onAuctionConfig +} +submodule('paapi', topLevelPAAPI); diff --git a/test/spec/modules/fledgeForGpt_spec.js b/test/spec/modules/fledgeForGpt_spec.js index 7bac56246f0..aa513f931db 100644 --- a/test/spec/modules/fledgeForGpt_spec.js +++ b/test/spec/modules/fledgeForGpt_spec.js @@ -194,7 +194,8 @@ describe('fledgeForGpt module', () => { Object.entries({ 'present': [], - 'supported': [[123, 4], [321, 5]] + 'supported': [[123, 4], [321, 5]], + 'defined': undefined, }).forEach(([t, sizes]) => { it(`should defer to next when no size is ${t}`, () => { getPAAPISizeHook(next, sizes); diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js index 5fedce95132..43e01d927f4 100644 --- a/test/spec/modules/paapi_spec.js +++ b/test/spec/modules/paapi_spec.js @@ -22,20 +22,10 @@ import {stubAuctionIndex} from '../../helpers/indexStub.js'; import {AuctionIndex} from '../../../src/auctionIndex.js'; describe('paapi module', () => { - let sandbox, getPAAPISizeStub; - function getPAAPISizeHook(next, sizes) { - next.bail(getPAAPISizeStub(sizes)); - } - before(() => { - reset(); - getPAAPISize.before(getPAAPISizeHook); - }) - after(() => { - getPAAPISize.getHooks({hook: getPAAPISizeHook}).remove(); - }) + let sandbox; + before(reset) beforeEach(() => { sandbox = sinon.sandbox.create(); - getPAAPISizeStub = sinon.stub(); }); afterEach(() => { sandbox.restore(); @@ -47,6 +37,23 @@ describe('paapi module', () => { 'paapi' ].forEach(configNS => { describe(`using ${configNS} for configuration`, () => { + let getPAAPISizeStub; + function getPAAPISizeHook(next, sizes) { + next.bail(getPAAPISizeStub(sizes)); + } + + before(() => { + getPAAPISize.before(getPAAPISizeHook, 100); + }); + + after(() => { + getPAAPISize.getHooks({hook: getPAAPISizeHook}).remove(); + }); + + beforeEach(() => { + getPAAPISizeStub = sinon.stub() + }); + describe('getPAAPIConfig', function () { let nextFnSpy, fledgeAuctionConfig; before(() => { @@ -525,7 +532,6 @@ describe('paapi module', () => { config.resetConfig(); }); - function mark() { return Object.fromEntries( adapterManager.makeBidRequests( @@ -669,6 +675,14 @@ describe('paapi module', () => { 'picks largest size by area': { in: [[200, 100], [150, 150]], out: [150, 150] + }, + 'can handle no sizes': { + in: [], + out: undefined + }, + 'can handle no input': { + in: undefined, + out: undefined } }).forEach(([t, {in: input, out}]) => { it(t, () => { diff --git a/test/spec/modules/topLevelPaapi_spec.js b/test/spec/modules/topLevelPaapi_spec.js new file mode 100644 index 00000000000..d351aace7ad --- /dev/null +++ b/test/spec/modules/topLevelPaapi_spec.js @@ -0,0 +1,79 @@ +import { + addComponentAuctionHook, + getPAAPIConfig, + registerSubmodule, + reset as resetPaapi +} from '../../../modules/paapi.js'; +import {config} from 'src/config.js'; +import {EVENTS} from 'src/constants.js'; +import * as events from 'src/events.js'; +import {topLevelPAAPI} from '/modules/topLevelPaapi.js'; +import {auctionManager} from '../../../src/auctionManager.js'; + +describe('topLevelPaapi', () => { + let sandbox, auction, paapiConfig, next, adUnit, auctionId; + before(() => { + resetPaapi(); + registerSubmodule(topLevelPAAPI); + }); + after(() => { + resetPaapi(); + }); + beforeEach(() => { + sandbox = sinon.createSandbox(); + auction = {}; + sandbox.stub(auctionManager.index, 'getAuction').callsFake(() => auction); + next = sinon.stub(); + auctionId = 'auct'; + adUnit = { + code: 'au' + }; + paapiConfig = { + seller: 'mock.seller' + }; + config.setConfig({ + paapi: { + enabled: true, + defaultForSlots: 1 + } + }); + }); + afterEach(() => { + config.resetConfig(); + sandbox.restore(); + }); + + function addPaapiContext() { + addComponentAuctionHook(next, {adUnitCode: adUnit.code, auctionId}, paapiConfig); + events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: [adUnit.code], adUnits: [adUnit]}); + } + + describe('when configured', () => { + let auctionConfig; + beforeEach(() => { + auctionConfig = { + seller: 'top.seller', + decisionLogicURL: 'https://top.seller/decision-logic.js' + }; + config.mergeConfig({ + paapi: { + topLevelSeller: { + auctionConfig + } + } + }); + }); + + it('should augment config returned by getPAAPIConfig', () => { + addPaapiContext(); + sinon.assert.match(getPAAPIConfig()[adUnit.code], auctionConfig); + }); + }); + + describe('when not configured', () => { + it('should not alter configs returned by getPAAPIConfig', () => { + addPaapiContext(); + expect(getPAAPIConfig()[adUnit.code].seller).to.not.exist; + }); + }); +}); From 365802cd2ca95b6f972435e36f3b8f038f635a66 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 18 Apr 2024 09:50:34 -0700 Subject: [PATCH 04/25] WIP --- libraries/weakStore/weakStore.js | 15 +++++++ modules/paapi.js | 60 +++++++++++++-------------- modules/topLevelPaapi.js | 4 ++ test/spec/libraries/weakStore_spec.js | 32 ++++++++++++++ 4 files changed, 80 insertions(+), 31 deletions(-) create mode 100644 libraries/weakStore/weakStore.js create mode 100644 test/spec/libraries/weakStore_spec.js diff --git a/libraries/weakStore/weakStore.js b/libraries/weakStore/weakStore.js new file mode 100644 index 00000000000..09606354dae --- /dev/null +++ b/libraries/weakStore/weakStore.js @@ -0,0 +1,15 @@ +import {auctionManager} from '../../src/auctionManager.js'; + +export function weakStore(get) { + const store = new WeakMap(); + return function (id, init = {}) { + const obj = get(id); + if (obj == null) return; + if (!store.has(obj)) { + store.set(obj, init); + } + return store.get(obj); + }; +} + +export const auctionStore = () => weakStore((auctionId) => auctionManager.index.getAuction({auctionId})); diff --git a/modules/paapi.js b/modules/paapi.js index f2f0161bfca..a97bf336f12 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -9,8 +9,8 @@ import * as events from '../src/events.js'; import {EVENTS} from '../src/constants.js'; import {currencyCompare} from '../libraries/currencyUtils/currency.js'; import {keyCompare, maximum, minimum} from '../src/utils/reducers.js'; -import {auctionManager} from '../src/auctionManager.js'; import {getGlobal} from '../src/prebidGlobal.js'; +import {auctionStore} from '../libraries/weakStore/weakStore.js'; const MODULE = 'PAAPI'; @@ -19,25 +19,15 @@ const USED = new WeakSet(); export function registerSubmodule(submod) { submodules.push(submod); - submod.init && submod.init({getPAAPIConfig}); + submod.init && submod.init({ + getPAAPIConfig, + }); } module('paapi', registerSubmodule); -function auctionConfigs() { - const store = new WeakMap(); - return function (auctionId, init = {}) { - const auction = auctionManager.index.getAuction({auctionId}); - if (auction == null) return; - if (!store.has(auction)) { - store.set(auction, init); - } - return store.get(auction); - }; -} - -const pendingForAuction = auctionConfigs(); -const configsForAuction = auctionConfigs(); +const pendingForAuction = auctionStore(); +const configsForAuction = auctionStore(); let latestAuctionForAdUnit = {}; let moduleConfig = {}; @@ -137,35 +127,43 @@ export function addComponentAuctionHook(next, request, componentAuctionConfig) { next(request, componentAuctionConfig); } +function expandFilters({auctionId, adUnitCode} = {}) { + let adUnitCodes = []; + if (adUnitCode == null) { + adUnitCodes = Object.keys(latestAuctionForAdUnit); + } else if (latestAuctionForAdUnit.hasOwnProperty(adUnitCode)) { + adUnitCodes = [adUnitCode]; + } + return Object.fromEntries( + adUnitCodes.map(au => [au, auctionId ?? latestAuctionForAdUnit[au]]) + ) +} + /** * Get PAAPI auction configuration. * - * @param auctionId? optional auction filter; if omitted, the latest auction for each ad unit is used - * @param adUnitCode? optional ad unit filter + * @param filters + * @param filters.auctionId? optional auction filter; if omitted, the latest auction for each ad unit is used + * @param filters.adUnitCode? optional ad unit filter * @param includeBlanks if true, include null entries for ad units that match the given filters but do not have any available auction configs. * @returns {{}} a map from ad unit code to auction config for the ad unit. */ -export function getPAAPIConfig({auctionId, adUnitCode} = {}, includeBlanks = false) { +export function getPAAPIConfig(filters = {}, includeBlanks = false) { const output = {}; - const targetedAuctionConfigs = auctionId && configsForAuction(auctionId); - Object.keys((auctionId != null ? targetedAuctionConfigs : latestAuctionForAdUnit) ?? []).forEach(au => { - const latestAuctionId = latestAuctionForAdUnit[au]; - const auctionConfigs = targetedAuctionConfigs ?? (latestAuctionId && configsForAuction(latestAuctionId)); - if ((adUnitCode ?? au) === au) { - let candidate; - if (targetedAuctionConfigs?.hasOwnProperty(au)) { - candidate = targetedAuctionConfigs[au]; - } else if (auctionId == null && auctionConfigs?.hasOwnProperty(au)) { - candidate = auctionConfigs[au]; - } + Object.entries(expandFilters(filters)).forEach(([au, auctionId]) => { + const auctionConfigs = configsForAuction(auctionId); + if (auctionConfigs?.hasOwnProperty(au)) { + const candidate = auctionConfigs[au]; if (candidate && !USED.has(candidate)) { output[au] = candidate; USED.add(candidate); } else if (includeBlanks) { output[au] = null; } + } else if (auctionId == null && includeBlanks) { + output[au] = null; } - }); + }) return output; } diff --git a/modules/topLevelPaapi.js b/modules/topLevelPaapi.js index 85721651194..8f3886acd39 100644 --- a/modules/topLevelPaapi.js +++ b/modules/topLevelPaapi.js @@ -16,6 +16,10 @@ function onAuctionConfig(auctionId, aucitonConfigs) { } } +export function getPAAPIBids({adUnitCode, auctionId}) { + +} + export const topLevelPAAPI = { name: 'topLevelPAAPI', init(params) { diff --git a/test/spec/libraries/weakStore_spec.js b/test/spec/libraries/weakStore_spec.js new file mode 100644 index 00000000000..407b83391ef --- /dev/null +++ b/test/spec/libraries/weakStore_spec.js @@ -0,0 +1,32 @@ +import {weakStore} from '../../../libraries/weakStore/weakStore.js'; + +describe('weakStore', () => { + let targets, store; + beforeEach(() => { + targets = { + id: {} + }; + store = weakStore((id) => targets[id]); + }); + + it('returns undef if getter returns undef', () => { + expect(store('missing')).to.not.exist; + }); + + it('inits to empty object by default', () => { + expect(store('id')).to.eql({}); + }); + + it('inits to given value', () => { + expect(store('id', {initial: 'value'})).to.eql({'initial': 'value'}); + }); + + it('returns the same object as long as the target does not change', () => { + expect(store('id')).to.equal(store('id')); + }); + + it('ignores init value if already initialized', () => { + store('id', {initial: 'value'}); + expect(store('id', {second: 'value'})).to.eql({initial: 'value'}); + }) +}); From 457acccf359260f40eef9ed81f25c1891f082edb Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 18 Apr 2024 15:33:39 -0700 Subject: [PATCH 05/25] getPAAPIBids --- modules/paapi.js | 23 ++- modules/topLevelPaapi.js | 65 +++++++-- src/constants.js | 5 +- test/spec/modules/topLevelPaapi_spec.js | 182 ++++++++++++++++++++++-- 4 files changed, 245 insertions(+), 30 deletions(-) diff --git a/modules/paapi.js b/modules/paapi.js index a97bf336f12..e2a76e87922 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -21,6 +21,7 @@ export function registerSubmodule(submod) { submodules.push(submod); submod.init && submod.init({ getPAAPIConfig, + expandFilters }); } @@ -84,7 +85,7 @@ function getSlotSignals(adUnit = {}, bidsReceived = [], bidRequests = []) { } function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adUnits}) { - const adUnitsByCode = Object.fromEntries(adUnits?.map(au => [au.code, au]) || []) + const adUnitsByCode = Object.fromEntries(adUnits?.map(au => [au.code, au]) || []); const allReqs = bidderRequests?.flatMap(br => br.bids); const paapiConfigs = {}; (adUnitCodes || []).forEach(au => { @@ -127,6 +128,16 @@ export function addComponentAuctionHook(next, request, componentAuctionConfig) { next(request, componentAuctionConfig); } +/** + * Expand PAAPI api filters into a map from ad unit code to auctionId. + * + * @param auctionId when specified, the result will have this as the value for each entry. + * when not specified, each ad unit will map to the latest auction that involved that ad unit. + * @param adUnitCode when specified, the result will contain only one entry (for this ad unit) or be empty (if this ad + * unit was never involved in an auction). + * when not specified, the result will contain an entry for every ad unit that was involved in any auction. + * @return {{[adUnitCode: string]: string}} + */ function expandFilters({auctionId, adUnitCode} = {}) { let adUnitCodes = []; if (adUnitCode == null) { @@ -136,7 +147,7 @@ function expandFilters({auctionId, adUnitCode} = {}) { } return Object.fromEntries( adUnitCodes.map(au => [au, auctionId ?? latestAuctionForAdUnit[au]]) - ) + ); } /** @@ -153,6 +164,7 @@ export function getPAAPIConfig(filters = {}, includeBlanks = false) { Object.entries(expandFilters(filters)).forEach(([au, auctionId]) => { const auctionConfigs = configsForAuction(auctionId); if (auctionConfigs?.hasOwnProperty(au)) { + // ad unit was involved in a PAAPI auction const candidate = auctionConfigs[au]; if (candidate && !USED.has(candidate)) { output[au] = candidate; @@ -161,9 +173,10 @@ export function getPAAPIConfig(filters = {}, includeBlanks = false) { output[au] = null; } } else if (auctionId == null && includeBlanks) { + // ad unit was involved in a non-PAAPI auction output[au] = null; } - }) + }); return output; } @@ -200,7 +213,7 @@ function getRequestedSize(adUnit) { return { width: size[0].toString(), height: size[1].toString() - } + }; } })(); } @@ -215,7 +228,7 @@ export function markForFledge(next, bidderRequests) { deepSetValue(bidReq, 'ortb2Imp.ext.ae', bidReq.ortb2Imp?.ext?.ae ?? ae); const requestedSize = getRequestedSize(bidReq); if (requestedSize) { - deepSetValue(bidReq, 'ortb2Imp.ext.paapi.requestedSize', requestedSize) + deepSetValue(bidReq, 'ortb2Imp.ext.paapi.requestedSize', requestedSize); } }); }); diff --git a/modules/topLevelPaapi.js b/modules/topLevelPaapi.js index 8f3886acd39..8f58b866e4b 100644 --- a/modules/topLevelPaapi.js +++ b/modules/topLevelPaapi.js @@ -1,30 +1,77 @@ import {submodule} from '../src/hook.js'; import {config} from '../src/config.js'; import {mergeDeep} from '../src/utils.js'; +import {auctionStore} from '../libraries/weakStore/weakStore.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {emit} from '../src/events.js'; +import {EVENTS} from '../src/constants.js'; -let getPAAPIConfig, moduleConfig; +let getPAAPIConfig, expandFilters, moduleConfig; + +const paapiBids = auctionStore(); config.getConfig('paapi', (cfg) => { moduleConfig = cfg.paapi?.topLevelSeller; -}) +}); + +function getBaseAuctionConfig() { + return Object.assign({ + resolveToConfig: false + }, moduleConfig.auctionConfig); +} -function onAuctionConfig(auctionId, aucitonConfigs) { +function onAuctionConfig(auctionId, auctionConfigs) { if (moduleConfig) { - Object.values(aucitonConfigs).forEach(auctionConfig => { - mergeDeep(auctionConfig, moduleConfig?.auctionConfig || {}, auctionConfig); - }) + const base = getBaseAuctionConfig(); + Object.values(auctionConfigs).forEach(auctionConfig => { + mergeDeep(auctionConfig, base, auctionConfig); + }); } } -export function getPAAPIBids({adUnitCode, auctionId}) { - +/** + * Returns the PAAPI runAdAuction result for the given filters, as a map from ad unit code to auction result + * (either a URN or a FencedFrameConfig, depending on the configured auction config). + * + * @param filters + * @param raa + * @return {Promise<{[p: string]: any}>} + */ +export function getPAAPIBids(filters, raa = (...args) => navigator.runAdAuction(...args)) { + return Promise.all( + Object.entries(expandFilters(filters)) + .map(([adUnitCode, auctionId]) => [adUnitCode, auctionId, paapiBids(auctionId)]) + .filter(([_1, _2, bids]) => bids) + .map(([adUnitCode, auctionId, bids]) => { + if (!bids.hasOwnProperty(adUnitCode)) { + const auctionConfig = getPAAPIConfig({adUnitCode, auctionId})[adUnitCode]; + if (auctionConfig) { + emit(EVENTS.RUN_PAAPI_AUCTION, { + auctionId, + adUnitCode, + auctionConfig + }); + bids[adUnitCode] = raa(auctionConfig).then(bid => { + const evt = {auctionId, adUnitCode}; + if (bid) Object.assign(evt, {bid}) + emit(bid ? EVENTS.PAAPI_BID : EVENTS.PAAPI_NO_BID, evt); + return bid; + }); + } + } + return bids[adUnitCode] ? bids[adUnitCode].then(result => [adUnitCode, result]) : Promise.resolve(); + }) + ).then(result => Object.fromEntries(result.filter(r => r))); } +getGlobal().getPAAPIBids = (filters) => getPAAPIBids(filters); + export const topLevelPAAPI = { name: 'topLevelPAAPI', init(params) { getPAAPIConfig = params.getPAAPIConfig; + expandFilters = params.expandFilters; }, onAuctionConfig -} +}; submodule('paapi', topLevelPAAPI); diff --git a/src/constants.js b/src/constants.js index b40b7ddb9b0..6fe0ffa0783 100644 --- a/src/constants.js +++ b/src/constants.js @@ -41,7 +41,10 @@ export const EVENTS = { BID_VIEWABLE: 'bidViewable', STALE_RENDER: 'staleRender', BILLABLE_EVENT: 'billableEvent', - BID_ACCEPTED: 'bidAccepted' + BID_ACCEPTED: 'bidAccepted', + RUN_PAAPI_AUCTION: 'paapiRunAuction', + PAAPI_BID: 'paapiBid', + PAAPI_NO_BID: 'paapiNoBid', }; export const AD_RENDER_FAILED_REASON = { diff --git a/test/spec/modules/topLevelPaapi_spec.js b/test/spec/modules/topLevelPaapi_spec.js index d351aace7ad..1bde1c9973c 100644 --- a/test/spec/modules/topLevelPaapi_spec.js +++ b/test/spec/modules/topLevelPaapi_spec.js @@ -7,27 +7,26 @@ import { import {config} from 'src/config.js'; import {EVENTS} from 'src/constants.js'; import * as events from 'src/events.js'; -import {topLevelPAAPI} from '/modules/topLevelPaapi.js'; +import {getPAAPIBids, topLevelPAAPI} from '/modules/topLevelPaapi.js'; import {auctionManager} from '../../../src/auctionManager.js'; describe('topLevelPaapi', () => { - let sandbox, auction, paapiConfig, next, adUnit, auctionId; + let sandbox, paapiConfig, next, auctionId, auctions; before(() => { resetPaapi(); + }); + beforeEach(() => { registerSubmodule(topLevelPAAPI); }); - after(() => { + afterEach(() => { resetPaapi(); }); beforeEach(() => { sandbox = sinon.createSandbox(); - auction = {}; - sandbox.stub(auctionManager.index, 'getAuction').callsFake(() => auction); + auctions = {}; + sandbox.stub(auctionManager.index, 'getAuction').callsFake(({auctionId}) => auctions[auctionId]?.auction); next = sinon.stub(); auctionId = 'auct'; - adUnit = { - code: 'au' - }; paapiConfig = { seller: 'mock.seller' }; @@ -43,9 +42,28 @@ describe('topLevelPaapi', () => { sandbox.restore(); }); - function addPaapiContext() { - addComponentAuctionHook(next, {adUnitCode: adUnit.code, auctionId}, paapiConfig); - events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: [adUnit.code], adUnits: [adUnit]}); + function addPaapiConfig(adUnitCode, auctionConfig, _auctionId = auctionId) { + let auction = auctions[_auctionId]; + if (!auction) { + auction = auctions[_auctionId] = { + auction: {}, + adUnits: {} + }; + } + if (!auction.adUnits.hasOwnProperty(adUnitCode)) { + auction.adUnits[adUnitCode] = {code: adUnitCode}; + } + addComponentAuctionHook(next, {adUnitCode, auctionId: _auctionId}, { + ...auctionConfig, + auctionId: _auctionId, + adUnitCode + }); + } + + function endAuctions() { + Object.entries(auctions).forEach(([auctionId, {adUnits}]) => { + events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: Object.keys(adUnits), adUnits: Object.values(adUnits)}); + }); } describe('when configured', () => { @@ -65,15 +83,149 @@ describe('topLevelPaapi', () => { }); it('should augment config returned by getPAAPIConfig', () => { - addPaapiContext(); - sinon.assert.match(getPAAPIConfig()[adUnit.code], auctionConfig); + addPaapiConfig('au', paapiConfig); + endAuctions(); + sinon.assert.match(getPAAPIConfig().au, auctionConfig); + }); + + it('should default resolveToConfig: false', () => { + addPaapiConfig('au', paapiConfig); + endAuctions(); + expect(getPAAPIConfig()['au'].resolveToConfig).to.eql(false); + }); + + describe('getPAAPIBids', () => { + let raa; + beforeEach(() => { + raa = sinon.stub().callsFake((cfg) => { + const {auctionId, adUnitCode} = cfg.componentAuctions[0]; + return Promise.resolve(`raa-${adUnitCode}-${auctionId}`); + }); + }); + + function getBids(filters) { + return getPAAPIBids(filters, raa); + } + + describe('with one auction config', () => { + beforeEach(() => { + addPaapiConfig('au', paapiConfig, 'auct'); + endAuctions(); + }); + it('should resolve to raa result', () => { + return getBids({adUnitCode: 'au', auctionId}).then(result => { + sinon.assert.calledWith(raa, sinon.match({ + ...auctionConfig, + componentAuctions: sinon.match(cmp => cmp.find(cfg => sinon.match(cfg, paapiConfig))) + })); + expect(result).to.eql({ + au: 'raa-au-auct' + }); + }); + }); + + it('should resolve to the same result when called again', () => { + getBids({adUnitCode: 'au', auctionId}); + return getBids({adUnitCode: 'au', auctionId: 'auct'}).then(result => { + sinon.assert.calledOnce(raa); + expect(result).to.eql({ + au: 'raa-au-auct' + }); + }); + }); + describe('events', () => { + beforeEach(() => { + sandbox.stub(events, 'emit'); + }); + it('should fire PAAPI_RUN_AUCTION', () => { + return Promise.all([ + getBids({adUnitCode: 'au', auctionId}), + getBids({adUnitCode: 'other', auctionId}) + ]).then(() => { + sinon.assert.calledWith(events.emit, EVENTS.RUN_PAAPI_AUCTION, { + adUnitCode: 'au', + auctionId, + auctionConfig: sinon.match(auctionConfig) + }); + sinon.assert.neverCalledWith(events.emit, EVENTS.RUN_PAAPI_AUCTION, { + adUnitCode: 'other' + }); + }); + }); + it('should fire PAAPI_BID', () => { + return getBids({adUnitCode: 'au', auctionId}).then(() => { + sinon.assert.calledWith(events.emit, EVENTS.PAAPI_BID, { + bid: 'raa-au-auct', + adUnitCode: 'au', + auctionId: 'auct' + }); + }); + }); + it('should fire PAAPI_NO_BID', () => { + raa = sinon.stub().callsFake(() => Promise.resolve(null)); + return getBids({adUnitCode: 'au', auctionId}).then(() => { + sinon.assert.calledWith(events.emit, EVENTS.PAAPI_NO_BID, { + adUnitCode: 'au', + auctionId: 'auct' + }); + }); + }); + }); + }); + + it('should resolve the same result from different filters', () => { + const targets = { + auct1: ['au1', 'au2'], + auct2: ['au1', 'au3'] + }; + Object.entries(targets).forEach(([auctionId, adUnitCodes]) => { + adUnitCodes.forEach(au => addPaapiConfig(au, paapiConfig, auctionId)); + }); + endAuctions(); + return Promise.all( + [ + [ + {adUnitCode: 'au1', auctionId: 'auct1'}, + { + au1: 'raa-au1-auct1' + } + ], + [ + {}, + { + au1: 'raa-au1-auct2', + au2: 'raa-au2-auct1', + au3: 'raa-au3-auct2' + } + ], + [ + {auctionId: 'auct1'}, + { + au1: 'raa-au1-auct1', + au2: 'raa-au2-auct1' + } + ], + [ + {adUnitCode: 'au1'}, + { + au1: 'raa-au1-auct2' + } + ], + ].map(([filters, expected]) => getBids(filters).then(res => [res, expected])) + ).then(res => { + res.forEach(([actual, expected]) => { + expect(actual).to.eql(expected); + }); + }); + }); }); }); describe('when not configured', () => { it('should not alter configs returned by getPAAPIConfig', () => { - addPaapiContext(); - expect(getPAAPIConfig()[adUnit.code].seller).to.not.exist; + addPaapiConfig('au', paapiConfig); + endAuctions(); + expect(getPAAPIConfig().au.seller).to.not.exist; }); }); }); From 42f7abdf2a82d959bbb4f5384c9808acbf0fe66b Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Mon, 22 Apr 2024 12:02:45 -0700 Subject: [PATCH 06/25] include size in paapi bids --- modules/topLevelPaapi.js | 21 +- test/spec/modules/topLevelPaapi_spec.js | 248 ++++++++++++++---------- 2 files changed, 156 insertions(+), 113 deletions(-) diff --git a/modules/topLevelPaapi.js b/modules/topLevelPaapi.js index 8f58b866e4b..530d6724e66 100644 --- a/modules/topLevelPaapi.js +++ b/modules/topLevelPaapi.js @@ -31,7 +31,7 @@ function onAuctionConfig(auctionId, auctionConfigs) { /** * Returns the PAAPI runAdAuction result for the given filters, as a map from ad unit code to auction result - * (either a URN or a FencedFrameConfig, depending on the configured auction config). + * (an object with `width`, `height`, and one of `urn` or `frameConfig`). * * @param filters * @param raa @@ -51,11 +51,20 @@ export function getPAAPIBids(filters, raa = (...args) => navigator.runAdAuction( adUnitCode, auctionConfig }); - bids[adUnitCode] = raa(auctionConfig).then(bid => { - const evt = {auctionId, adUnitCode}; - if (bid) Object.assign(evt, {bid}) - emit(bid ? EVENTS.PAAPI_BID : EVENTS.PAAPI_NO_BID, evt); - return bid; + bids[adUnitCode] = raa(auctionConfig).then(result => { + let bid = null; + if (result) { + bid = { + adUnitCode, + auctionId, + [typeof result === 'string' ? 'urn' : 'frameConfig']: result, + ...(auctionConfig.requestedSize || {}) + } + emit(EVENTS.PAAPI_BID, bid); + return bid; + } else { + emit(EVENTS.PAAPI_NO_BID, {auctionId, adUnitCode}) + } }); } } diff --git a/test/spec/modules/topLevelPaapi_spec.js b/test/spec/modules/topLevelPaapi_spec.js index 1bde1c9973c..3dce3411df7 100644 --- a/test/spec/modules/topLevelPaapi_spec.js +++ b/test/spec/modules/topLevelPaapi_spec.js @@ -51,7 +51,19 @@ describe('topLevelPaapi', () => { }; } if (!auction.adUnits.hasOwnProperty(adUnitCode)) { - auction.adUnits[adUnitCode] = {code: adUnitCode}; + auction.adUnits[adUnitCode] = { + code: adUnitCode, + ortb2Imp: { + ext: { + paapi: { + requestedSize: { + width: '123', + height: '321' + } + } + } + } + }; } addComponentAuctionHook(next, {adUnitCode, auctionId: _auctionId}, { ...auctionConfig, @@ -95,129 +107,151 @@ describe('topLevelPaapi', () => { }); describe('getPAAPIBids', () => { - let raa; - beforeEach(() => { - raa = sinon.stub().callsFake((cfg) => { - const {auctionId, adUnitCode} = cfg.componentAuctions[0]; - return Promise.resolve(`raa-${adUnitCode}-${auctionId}`); - }); - }); + Object.entries({ + 'a string URN': { + pack: (val) => val, + unpack: (urn) => ({urn}) + }, + 'a frameConfig object': { + pack: (val) => ({val}), + unpack: (val) => ({frameConfig: {val}}) + } + }).forEach(([t, {pack, unpack}]) => { + describe(`when runAdAuction returns ${t}`, () => { + let raa; + beforeEach(() => { + raa = sinon.stub().callsFake((cfg) => { + const {auctionId, adUnitCode} = cfg.componentAuctions[0]; + return Promise.resolve(pack(`raa-${adUnitCode}-${auctionId}`)); + }); + }); - function getBids(filters) { - return getPAAPIBids(filters, raa); - } + function getBids(filters) { + return getPAAPIBids(filters, raa); + } - describe('with one auction config', () => { - beforeEach(() => { - addPaapiConfig('au', paapiConfig, 'auct'); - endAuctions(); - }); - it('should resolve to raa result', () => { - return getBids({adUnitCode: 'au', auctionId}).then(result => { - sinon.assert.calledWith(raa, sinon.match({ - ...auctionConfig, - componentAuctions: sinon.match(cmp => cmp.find(cfg => sinon.match(cfg, paapiConfig))) - })); - expect(result).to.eql({ - au: 'raa-au-auct' + function expectBids(actual, expected) { + expect(Object.keys(actual)).to.eql(Object.keys(expected)); + Object.entries(expected).forEach(([au, val]) => { + sinon.assert.match(actual[au], { + width: '123', + height: '321', + ...unpack(val) + }); }); - }); - }); + } - it('should resolve to the same result when called again', () => { - getBids({adUnitCode: 'au', auctionId}); - return getBids({adUnitCode: 'au', auctionId: 'auct'}).then(result => { - sinon.assert.calledOnce(raa); - expect(result).to.eql({ - au: 'raa-au-auct' + describe('with one auction config', () => { + beforeEach(() => { + addPaapiConfig('au', paapiConfig, 'auct'); + endAuctions(); }); - }); - }); - describe('events', () => { - beforeEach(() => { - sandbox.stub(events, 'emit'); - }); - it('should fire PAAPI_RUN_AUCTION', () => { - return Promise.all([ - getBids({adUnitCode: 'au', auctionId}), - getBids({adUnitCode: 'other', auctionId}) - ]).then(() => { - sinon.assert.calledWith(events.emit, EVENTS.RUN_PAAPI_AUCTION, { - adUnitCode: 'au', - auctionId, - auctionConfig: sinon.match(auctionConfig) + it('should resolve to raa result', () => { + return getBids({adUnitCode: 'au', auctionId}).then(result => { + sinon.assert.calledWith(raa, sinon.match({ + ...auctionConfig, + componentAuctions: sinon.match(cmp => cmp.find(cfg => sinon.match(cfg, paapiConfig))) + })); + expectBids(result, {au: 'raa-au-auct'}); }); - sinon.assert.neverCalledWith(events.emit, EVENTS.RUN_PAAPI_AUCTION, { - adUnitCode: 'other' + }); + + it('should resolve to the same result when called again', () => { + getBids({adUnitCode: 'au', auctionId}); + return getBids({adUnitCode: 'au', auctionId: 'auct'}).then(result => { + sinon.assert.calledOnce(raa); + expectBids(result, {au: 'raa-au-auct'}); }); }); - }); - it('should fire PAAPI_BID', () => { - return getBids({adUnitCode: 'au', auctionId}).then(() => { - sinon.assert.calledWith(events.emit, EVENTS.PAAPI_BID, { - bid: 'raa-au-auct', - adUnitCode: 'au', - auctionId: 'auct' + describe('events', () => { + beforeEach(() => { + sandbox.stub(events, 'emit'); + }); + it('should fire PAAPI_RUN_AUCTION', () => { + return Promise.all([ + getBids({adUnitCode: 'au', auctionId}), + getBids({adUnitCode: 'other', auctionId}) + ]).then(() => { + sinon.assert.calledWith(events.emit, EVENTS.RUN_PAAPI_AUCTION, { + adUnitCode: 'au', + auctionId, + auctionConfig: sinon.match(auctionConfig) + }); + sinon.assert.neverCalledWith(events.emit, EVENTS.RUN_PAAPI_AUCTION, { + adUnitCode: 'other' + }); + }); + }); + it('should fire PAAPI_BID', () => { + return getBids({adUnitCode: 'au', auctionId}).then(() => { + sinon.assert.calledWith(events.emit, EVENTS.PAAPI_BID, sinon.match({ + ...unpack('raa-au-auct'), + adUnitCode: 'au', + auctionId: 'auct' + })); + }); + }); + it('should fire PAAPI_NO_BID', () => { + raa = sinon.stub().callsFake(() => Promise.resolve(null)); + return getBids({adUnitCode: 'au', auctionId}).then(() => { + sinon.assert.calledWith(events.emit, EVENTS.PAAPI_NO_BID, { + adUnitCode: 'au', + auctionId: 'auct' + }); + }); }); }); }); - it('should fire PAAPI_NO_BID', () => { - raa = sinon.stub().callsFake(() => Promise.resolve(null)); - return getBids({adUnitCode: 'au', auctionId}).then(() => { - sinon.assert.calledWith(events.emit, EVENTS.PAAPI_NO_BID, { - adUnitCode: 'au', - auctionId: 'auct' + + it('should resolve the same result from different filters', () => { + const targets = { + auct1: ['au1', 'au2'], + auct2: ['au1', 'au3'] + }; + Object.entries(targets).forEach(([auctionId, adUnitCodes]) => { + adUnitCodes.forEach(au => addPaapiConfig(au, paapiConfig, auctionId)); + }); + endAuctions(); + return Promise.all( + [ + [ + {adUnitCode: 'au1', auctionId: 'auct1'}, + { + au1: 'raa-au1-auct1' + } + ], + [ + {}, + { + au1: 'raa-au1-auct2', + au2: 'raa-au2-auct1', + au3: 'raa-au3-auct2' + } + ], + [ + {auctionId: 'auct1'}, + { + au1: 'raa-au1-auct1', + au2: 'raa-au2-auct1' + } + ], + [ + {adUnitCode: 'au1'}, + { + au1: 'raa-au1-auct2' + } + ], + ].map(([filters, expected]) => getBids(filters).then(res => [res, expected])) + ).then(res => { + res.forEach(([actual, expected]) => { + expectBids(actual, expected); }); }); }); - }); - }); - it('should resolve the same result from different filters', () => { - const targets = { - auct1: ['au1', 'au2'], - auct2: ['au1', 'au3'] - }; - Object.entries(targets).forEach(([auctionId, adUnitCodes]) => { - adUnitCodes.forEach(au => addPaapiConfig(au, paapiConfig, auctionId)); - }); - endAuctions(); - return Promise.all( - [ - [ - {adUnitCode: 'au1', auctionId: 'auct1'}, - { - au1: 'raa-au1-auct1' - } - ], - [ - {}, - { - au1: 'raa-au1-auct2', - au2: 'raa-au2-auct1', - au3: 'raa-au3-auct2' - } - ], - [ - {auctionId: 'auct1'}, - { - au1: 'raa-au1-auct1', - au2: 'raa-au2-auct1' - } - ], - [ - {adUnitCode: 'au1'}, - { - au1: 'raa-au1-auct2' - } - ], - ].map(([filters, expected]) => getBids(filters).then(res => [res, expected])) - ).then(res => { - res.forEach(([actual, expected]) => { - expect(actual).to.eql(expected); - }); }); }); + }); }); From c1ba3a112d8409d5c4a390db395142d8f37102d0 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Mon, 22 Apr 2024 12:18:21 -0700 Subject: [PATCH 07/25] update TL example --- .../gpt/top-level-paapi/tl_paapi_example.html | 57 ++++++++++--------- modules/topLevelPaapi.js | 8 +-- test/spec/modules/topLevelPaapi_spec.js | 12 +++- 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html b/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html index 9a4991d2711..197a48544ae 100644 --- a/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html +++ b/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html @@ -61,6 +61,12 @@ enabled: true, gpt: { autoconfig: false + }, + topLevelSeller: { + auctionConfig: { + seller: window.location.origin, + decisionLogicURL: new URL('decisionLogic.js', window.location).toString(), + } } }, debugging: { @@ -110,39 +116,36 @@ }); function raa() { - return Promise.all( - Object.entries(pbjs.getPAAPIConfig()) - .map(([adUnitCode, auctionConfig]) => { - return navigator.runAdAuction({ - seller: window.location.origin, - decisionLogicURL: new URL('decisionLogic.js', window.location).toString(), - resolveToConfig: false, - ...auctionConfig - }).then(urn => { - if (urn) { - // if we have a paapi winner, replace the adunit div - // with an iframe that renders it - const iframe = document.createElement('iframe'); - Object.assign(iframe, { - src: urn, - frameBorder: 0, - scrolling: 'no', - }, auctionConfig.requestedSize); - const div = document.getElementById(adUnitCode); - div.parentElement.insertBefore(iframe, div); - div.remove(); - return true; - } + return pbjs.getPAAPIBids().then(bids => { + let contextual = false; + Object.entries(bids).forEach(([adUnitCode, bid]) => { + // if we have a paapi winner, replace the adunit div + // with an iframe that renders it + if (bid) { + const iframe = document.createElement('iframe'); + Object.assign(iframe, { + src: bid.urn, + frameBorder: 0, + scrolling: 'no', + width: bid.width, + height: bid.height, }); - }) - ).then(won => won.every(el => el)); + const div = document.getElementById(adUnitCode); + div.parentElement.insertBefore(iframe, div); + div.remove(); + } else { + contextual = true; + } + }) + return contextual; + }) } function sendAdserverRequest() { if (pbjs.adserverRequestSent) return; pbjs.adserverRequestSent = true; - raa().then((allPaapi) => { - if (!allPaapi) { + raa().then((contextual) => { + if (contextual) { googletag.cmd.push(function () { pbjs.que.push(function () { pbjs.setTargetingForGPTAsync(); diff --git a/modules/topLevelPaapi.js b/modules/topLevelPaapi.js index 530d6724e66..2862032c40e 100644 --- a/modules/topLevelPaapi.js +++ b/modules/topLevelPaapi.js @@ -52,18 +52,18 @@ export function getPAAPIBids(filters, raa = (...args) => navigator.runAdAuction( auctionConfig }); bids[adUnitCode] = raa(auctionConfig).then(result => { - let bid = null; if (result) { - bid = { + const bid = { + ...(auctionConfig.requestedSize || {}), adUnitCode, auctionId, [typeof result === 'string' ? 'urn' : 'frameConfig']: result, - ...(auctionConfig.requestedSize || {}) } emit(EVENTS.PAAPI_BID, bid); return bid; } else { - emit(EVENTS.PAAPI_NO_BID, {auctionId, adUnitCode}) + emit(EVENTS.PAAPI_NO_BID, {auctionId, adUnitCode}); + return null; } }); } diff --git a/test/spec/modules/topLevelPaapi_spec.js b/test/spec/modules/topLevelPaapi_spec.js index 3dce3411df7..7cdd8e0b954 100644 --- a/test/spec/modules/topLevelPaapi_spec.js +++ b/test/spec/modules/topLevelPaapi_spec.js @@ -133,7 +133,7 @@ describe('topLevelPaapi', () => { function expectBids(actual, expected) { expect(Object.keys(actual)).to.eql(Object.keys(expected)); Object.entries(expected).forEach(([au, val]) => { - sinon.assert.match(actual[au], { + sinon.assert.match(actual[au], val == null ? val : { width: '123', height: '321', ...unpack(val) @@ -156,6 +156,13 @@ describe('topLevelPaapi', () => { }); }); + it('should resolve to null when runAdAuction returns null', () => { + raa = sinon.stub().callsFake(() => Promise.resolve()); + return getBids({adUnitCode: 'au', auctionId: 'auct'}).then(result => { + expectBids(result, {au: null}); + }); + }); + it('should resolve to the same result when called again', () => { getBids({adUnitCode: 'au', auctionId}); return getBids({adUnitCode: 'au', auctionId: 'auct'}).then(result => { @@ -163,6 +170,7 @@ describe('topLevelPaapi', () => { expectBids(result, {au: 'raa-au-auct'}); }); }); + describe('events', () => { beforeEach(() => { sandbox.stub(events, 'emit'); @@ -248,10 +256,8 @@ describe('topLevelPaapi', () => { }); }); }); - }); }); - }); }); From 82f72fe6354fd165d8d02cc386c6ff413186fa9b Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Mon, 22 Apr 2024 13:51:01 -0700 Subject: [PATCH 08/25] slightly nicer example --- .../gpt/top-level-paapi/tl_paapi_example.html | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html b/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html index 197a48544ae..8ee20751dd2 100644 --- a/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html +++ b/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html @@ -1,4 +1,4 @@ - + + +
+
+
+ +
From 3200b11d7ed6e748633e428c0bbb53daa85b5a69 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Mon, 22 Apr 2024 14:24:29 -0700 Subject: [PATCH 09/25] slight improvement --- modules/topLevelPaapi.js | 14 ++++++++------ test/spec/modules/topLevelPaapi_spec.js | 9 +++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/modules/topLevelPaapi.js b/modules/topLevelPaapi.js index 2862032c40e..5951ae6624e 100644 --- a/modules/topLevelPaapi.js +++ b/modules/topLevelPaapi.js @@ -15,16 +15,18 @@ config.getConfig('paapi', (cfg) => { }); function getBaseAuctionConfig() { - return Object.assign({ - resolveToConfig: false - }, moduleConfig.auctionConfig); + if (moduleConfig?.auctionConfig) { + return Object.assign({ + resolveToConfig: false + }, moduleConfig.auctionConfig); + } } function onAuctionConfig(auctionId, auctionConfigs) { - if (moduleConfig) { - const base = getBaseAuctionConfig(); + const base = getBaseAuctionConfig(); + if (base) { Object.values(auctionConfigs).forEach(auctionConfig => { - mergeDeep(auctionConfig, base, auctionConfig); + mergeDeep(auctionConfig, base); }); } } diff --git a/test/spec/modules/topLevelPaapi_spec.js b/test/spec/modules/topLevelPaapi_spec.js index 7cdd8e0b954..ae7ce4b2cbb 100644 --- a/test/spec/modules/topLevelPaapi_spec.js +++ b/test/spec/modules/topLevelPaapi_spec.js @@ -100,6 +100,15 @@ describe('topLevelPaapi', () => { sinon.assert.match(getPAAPIConfig().au, auctionConfig); }); + it('should not choke if auction config is not defined', () => { + const cfg = config.getConfig('paapi'); + delete cfg.topLevelSeller.auctionConfig; + config.setConfig(cfg); + addPaapiConfig('au', paapiConfig); + endAuctions(); + expect(getPAAPIConfig().au.componentAuctions).to.exist; + }) + it('should default resolveToConfig: false', () => { addPaapiConfig('au', paapiConfig); endAuctions(); From 1c594f833ca67b5eacdb15801f00193c3ad6783a Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 23 Apr 2024 06:59:56 -0700 Subject: [PATCH 10/25] refactor --- modules/topLevelPaapi.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/modules/topLevelPaapi.js b/modules/topLevelPaapi.js index 5951ae6624e..7b7b1ae677f 100644 --- a/modules/topLevelPaapi.js +++ b/modules/topLevelPaapi.js @@ -42,10 +42,9 @@ function onAuctionConfig(auctionId, auctionConfigs) { export function getPAAPIBids(filters, raa = (...args) => navigator.runAdAuction(...args)) { return Promise.all( Object.entries(expandFilters(filters)) - .map(([adUnitCode, auctionId]) => [adUnitCode, auctionId, paapiBids(auctionId)]) - .filter(([_1, _2, bids]) => bids) - .map(([adUnitCode, auctionId, bids]) => { - if (!bids.hasOwnProperty(adUnitCode)) { + .map(([adUnitCode, auctionId]) => { + const bids = paapiBids(auctionId); + if (bids && !bids.hasOwnProperty(adUnitCode)) { const auctionConfig = getPAAPIConfig({adUnitCode, auctionId})[adUnitCode]; if (auctionConfig) { emit(EVENTS.RUN_PAAPI_AUCTION, { @@ -70,9 +69,9 @@ export function getPAAPIBids(filters, raa = (...args) => navigator.runAdAuction( }); } } - return bids[adUnitCode] ? bids[adUnitCode].then(result => [adUnitCode, result]) : Promise.resolve(); - }) - ).then(result => Object.fromEntries(result.filter(r => r))); + return bids?.[adUnitCode] ? bids[adUnitCode].then(result => [adUnitCode, result]) : null; + }).filter(e => e) + ).then(result => Object.fromEntries(result)); } getGlobal().getPAAPIBids = (filters) => getPAAPIBids(filters); From ac43f5b27535c9d14d354b1b7a3cb64904be25a7 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 23 Apr 2024 07:27:08 -0700 Subject: [PATCH 11/25] add PAAPI_ERROR event --- modules/topLevelPaapi.js | 10 +++++++--- src/constants.js | 1 + test/spec/modules/topLevelPaapi_spec.js | 12 ++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/modules/topLevelPaapi.js b/modules/topLevelPaapi.js index 7b7b1ae677f..35d38ed7717 100644 --- a/modules/topLevelPaapi.js +++ b/modules/topLevelPaapi.js @@ -1,6 +1,6 @@ import {submodule} from '../src/hook.js'; import {config} from '../src/config.js'; -import {mergeDeep} from '../src/utils.js'; +import {logError, mergeDeep} from '../src/utils.js'; import {auctionStore} from '../libraries/weakStore/weakStore.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {emit} from '../src/events.js'; @@ -66,10 +66,14 @@ export function getPAAPIBids(filters, raa = (...args) => navigator.runAdAuction( emit(EVENTS.PAAPI_NO_BID, {auctionId, adUnitCode}); return null; } - }); + }).catch(error => { + logError(`topLevelPaapi error (auction "${auctionId}", adUnit "${adUnitCode}"):`, error) + emit(EVENTS.PAAPI_ERROR, {auctionId, adUnitCode, error}) + return null; + }).then(res => [adUnitCode, res]); } } - return bids?.[adUnitCode] ? bids[adUnitCode].then(result => [adUnitCode, result]) : null; + return bids?.[adUnitCode] }).filter(e => e) ).then(result => Object.fromEntries(result)); } diff --git a/src/constants.js b/src/constants.js index 6fe0ffa0783..3a85c3ae3cb 100644 --- a/src/constants.js +++ b/src/constants.js @@ -45,6 +45,7 @@ export const EVENTS = { RUN_PAAPI_AUCTION: 'paapiRunAuction', PAAPI_BID: 'paapiBid', PAAPI_NO_BID: 'paapiNoBid', + PAAPI_ERROR: 'paapiError', }; export const AD_RENDER_FAILED_REASON = { diff --git a/test/spec/modules/topLevelPaapi_spec.js b/test/spec/modules/topLevelPaapi_spec.js index ae7ce4b2cbb..eecfbe36e31 100644 --- a/test/spec/modules/topLevelPaapi_spec.js +++ b/test/spec/modules/topLevelPaapi_spec.js @@ -217,6 +217,18 @@ describe('topLevelPaapi', () => { }); }); }); + + it('should fire PAAPI_ERROR', () => { + raa = sinon.stub().callsFake(() => Promise.reject(new Error('message'))); + return getBids({adUnitCode: 'au', auctionId}).then(res => { + expect(res).to.eql({au: null}); + sinon.assert.calledWith(events.emit, EVENTS.PAAPI_ERROR, { + adUnitCode: 'au', + auctionId: 'auct', + error: sinon.match({message: 'message'}) + }) + }) + }) }); }); From ff72e385d9eef73a0fd354b9a36a4511f2155cf2 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 23 Apr 2024 08:10:53 -0700 Subject: [PATCH 12/25] use optable in TL example --- .../gpt/top-level-paapi/tl_paapi_example.html | 49 ++----------------- 1 file changed, 4 insertions(+), 45 deletions(-) diff --git a/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html b/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html index 8ee20751dd2..4ddd8a19f50 100644 --- a/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html +++ b/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html @@ -31,15 +31,11 @@ }, bids: [ { - bidder: 'openx', + bidder: 'optable', params: { - unit: '538703464', - response_template_name: 'test_banner_ad', - test: true, - delDomain: 'sademo-d.openx.net' - } + site: 'daa30ba1-5613-4a2c-b7f0-34e2c033202a' + }, }, - ], } ] @@ -69,43 +65,6 @@ } } }, - debugging: { - enabled: true, - intercept: [ - { - when: { - bidder: 'openx', - }, - then: { - cpm: 0.1 - }, - paapi() { - return [ - { - 'seller': 'https://privacysandbox.openx.net', - 'decisionLogicURL': 'https://privacysandbox.openx.net/fledge/decision-logic-component.js', - 'sellerSignals': { - 'floor': 0.01, - 'currency': 'USD', - 'auctionTimestamp': new Date().getTime(), - 'publisherId': '537143056', - 'adUnitId': '538703464' - }, - 'interestGroupBuyers': [ - 'https://privacysandbox.openx.net' - ], - 'perBuyerSignals': { - 'https://privacysandbox.openx.net': { - 'bid': 1.5 - } - }, - 'sellerCurrency': 'USD' - } - ]; - } - } - ] - }, }); pbjs.addAdUnits(adUnits); @@ -183,7 +142,7 @@

Standalone PAAPI Prebid.js Example

Chrome flags:

--enable-features=CookieDeprecationFacilitatedTesting:label/treatment_1.2/force_eligible/true --privacy-sandbox-enrollment-overrides=https://localhost:9999 -

Join interest group at https://privacysandbox.openx.net/fledge/advertiser +

Join interest group at https://www.optable.co/

Div-1
From 5db17b965acfd91330f66d7bf2f8dea4fcaa650e Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 23 Apr 2024 09:56:12 -0700 Subject: [PATCH 13/25] allow async bid retrieval on render: safeframes --- src/adRendering.js | 15 +- src/secureCreatives.js | 24 +-- test/spec/unit/secureCreatives_spec.js | 235 +++++++++++++------------ 3 files changed, 144 insertions(+), 130 deletions(-) diff --git a/src/adRendering.js b/src/adRendering.js index 7d306adc9cc..1f2b95d85e2 100644 --- a/src/adRendering.js +++ b/src/adRendering.js @@ -8,10 +8,22 @@ import {auctionManager} from './auctionManager.js'; import {getCreativeRenderer} from './creativeRenderers.js'; import {hook} from './hook.js'; import {fireNativeTrackers} from './native.js'; +import {GreedyPromise} from './utils/promise.js'; const { AD_RENDER_FAILED, AD_RENDER_SUCCEEDED, STALE_RENDER, BID_WON } = EVENTS; const { EXCEPTION } = AD_RENDER_FAILED_REASON; +export const getBidToRender = hook('sync', function (adId, override = GreedyPromise.resolve()) { + return override + .then(bid => bid ?? auctionManager.findBidByAdId(adId)) + .catch(() => undefined) +}, 'getBidToRender') + +export const markWinningBid = hook('sync', function (bid) { + events.emit(BID_WON, bid); + auctionManager.addWinningBid(bid); +}, 'markWinningBid') + /** * Emit the AD_RENDER_FAILED event. * @@ -168,8 +180,7 @@ export function handleRender({renderFn, resizeFn, adId, options, bidResponse, do bid: bidResponse }); } - auctionManager.addWinningBid(bidResponse); - events.emit(BID_WON, bidResponse); + markWinningBid(bidResponse); } export function renderAdDirect(doc, adId, options) { diff --git a/src/secureCreatives.js b/src/secureCreatives.js index 96ace0792e4..189e888c32e 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -3,19 +3,15 @@ access to a publisher page from creative payloads. */ -import * as events from './events.js'; import {getAllAssetsMessage, getAssetMessage} from './native.js'; -import { BID_STATUS, EVENTS, MESSAGES } from './constants.js'; +import {BID_STATUS, MESSAGES} from './constants.js'; import {isApnGetTagDefined, isGptPubadsDefined, logError, logWarn} from './utils.js'; -import {auctionManager} from './auctionManager.js'; import {find, includes} from './polyfill.js'; -import {handleCreativeEvent, handleNativeMessage, handleRender} from './adRendering.js'; +import {getBidToRender, handleCreativeEvent, handleNativeMessage, handleRender, markWinningBid} from './adRendering.js'; import {getCreativeRendererSource} from './creativeRenderers.js'; const { REQUEST, RESPONSE, NATIVE, EVENT } = MESSAGES; -const BID_WON = EVENTS.BID_WON; - const HANDLER_MAP = { [REQUEST]: handleRenderRequest, [EVENT]: handleEventRequest, @@ -28,7 +24,9 @@ if (FEATURES.NATIVE) { } export function listenMessagesFromCreative() { - window.addEventListener('message', receiveMessage, false); + window.addEventListener('message', function (ev) { + receiveMessage(ev); + }, false); } export function getReplier(ev) { @@ -58,13 +56,10 @@ export function receiveMessage(ev) { return; } - if (data && data.adId && data.message) { - const adObject = find(auctionManager.getBidsReceived(), function (bid) { - return bid.adId === data.adId; - }); - if (HANDLER_MAP.hasOwnProperty(data.message)) { + if (data && data.adId && data.message && HANDLER_MAP.hasOwnProperty(data.message)) { + return getBidToRender(data.adId).then(adObject => { HANDLER_MAP[data.message](getReplier(ev), data, adObject); - } + }) } } @@ -100,8 +95,7 @@ function handleNativeRequest(reply, data, adObject) { } if (adObject.status !== BID_STATUS.RENDERED) { - auctionManager.addWinningBid(adObject); - events.emit(BID_WON, adObject); + markWinningBid(adObject); } switch (data.action) { diff --git a/test/spec/unit/secureCreatives_spec.js b/test/spec/unit/secureCreatives_spec.js index 189066f7f88..b51f152f1d8 100644 --- a/test/spec/unit/secureCreatives_spec.js +++ b/test/spec/unit/secureCreatives_spec.js @@ -13,7 +13,7 @@ import 'modules/nativeRendering.js'; import {expect} from 'chai'; -import { AD_RENDER_FAILED_REASON, BID_STATUS, EVENTS } from 'src/constants.js'; +import {AD_RENDER_FAILED_REASON, BID_STATUS, EVENTS} from 'src/constants.js'; describe('secureCreatives', () => { let sandbox; @@ -30,6 +30,11 @@ describe('secureCreatives', () => { return Object.assign({origin: 'mock-origin', ports: []}, ev) } + function receive(ev) { + // make sure that bids can be retrieved asynchronously (from getBidToRender) + return Promise.resolve().then(() => receiveMessage(ev)); + } + describe('getReplier', () => { it('should use source.postMessage if no MessagePort is available', () => { const ev = { @@ -139,6 +144,7 @@ describe('secureCreatives', () => { }); describe('Prebid Request', function() { + it('should render', function () { pushBidResponseToAuction({ renderer: {render: sinon.stub(), url: 'some url'} @@ -153,17 +159,18 @@ describe('secureCreatives', () => { data: JSON.stringify(data), }); - receiveMessage(ev); + return receive(ev).then(() => { + sinon.assert.neverCalledWith(spyLogWarn, warning); + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.calledWith(spyAddWinningBid, adResponse); + sinon.assert.calledOnce(adResponse.renderer.render); + sinon.assert.calledWith(adResponse.renderer.render, adResponse); + sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); + sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); - sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); - sinon.assert.calledOnce(adResponse.renderer.render); - sinon.assert.calledWith(adResponse.renderer.render, adResponse); - sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); - sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); + expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); + }); - expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); }); it('should allow stale rendering without config', function () { @@ -180,29 +187,26 @@ describe('secureCreatives', () => { data: JSON.stringify(data) }); - receiveMessage(ev); - - sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); - sinon.assert.calledOnce(adResponse.renderer.render); - sinon.assert.calledWith(adResponse.renderer.render, adResponse); - sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); - sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); - - expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); - - resetHistories(adResponse.renderer.render); - - receiveMessage(ev); - - sinon.assert.calledWith(spyLogWarn, warning); - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); - sinon.assert.calledOnce(adResponse.renderer.render); - sinon.assert.calledWith(adResponse.renderer.render, adResponse); - sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); - sinon.assert.calledWith(stubEmit, EVENTS.STALE_RENDER, adResponse); + return receive(ev).then(() => { + sinon.assert.neverCalledWith(spyLogWarn, warning); + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.calledWith(spyAddWinningBid, adResponse); + sinon.assert.calledOnce(adResponse.renderer.render); + sinon.assert.calledWith(adResponse.renderer.render, adResponse); + sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); + sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); + expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); + resetHistories(adResponse.renderer.render); + return receive(ev); + }).then(() => { + sinon.assert.calledWith(spyLogWarn, warning); + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.calledWith(spyAddWinningBid, adResponse); + sinon.assert.calledOnce(adResponse.renderer.render); + sinon.assert.calledWith(adResponse.renderer.render, adResponse); + sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); + sinon.assert.calledWith(stubEmit, EVENTS.STALE_RENDER, adResponse); + }); }); it('should stop stale rendering with config', function () { @@ -221,29 +225,27 @@ describe('secureCreatives', () => { data: JSON.stringify(data) }); - receiveMessage(ev); - - sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); - sinon.assert.calledOnce(adResponse.renderer.render); - sinon.assert.calledWith(adResponse.renderer.render, adResponse); - sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); - sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); - - expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); - - resetHistories(adResponse.renderer.render); - - receiveMessage(ev); - - sinon.assert.calledWith(spyLogWarn, warning); - sinon.assert.notCalled(spyAddWinningBid); - sinon.assert.notCalled(adResponse.renderer.render); - sinon.assert.neverCalledWith(stubEmit, EVENTS.BID_WON, adResponse); - sinon.assert.calledWith(stubEmit, EVENTS.STALE_RENDER, adResponse); - - configObj.setConfig({'auctionOptions': {}}); + return receive(ev).then(() => { + sinon.assert.neverCalledWith(spyLogWarn, warning); + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.calledWith(spyAddWinningBid, adResponse); + sinon.assert.calledOnce(adResponse.renderer.render); + sinon.assert.calledWith(adResponse.renderer.render, adResponse); + sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); + sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); + + expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); + + resetHistories(adResponse.renderer.render); + return receive(ev) + }).then(() => { + sinon.assert.calledWith(spyLogWarn, warning); + sinon.assert.notCalled(spyAddWinningBid); + sinon.assert.notCalled(adResponse.renderer.render); + sinon.assert.neverCalledWith(stubEmit, EVENTS.BID_WON, adResponse); + sinon.assert.calledWith(stubEmit, EVENTS.STALE_RENDER, adResponse); + configObj.setConfig({'auctionOptions': {}}); + }); }); it('should emit AD_RENDER_FAILED if requested missing adId', () => { @@ -253,11 +255,12 @@ describe('secureCreatives', () => { adId: 'missing' }) }); - receiveMessage(ev); - sinon.assert.calledWith(stubEmit, EVENTS.AD_RENDER_FAILED, sinon.match({ - reason: AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, - adId: 'missing' - })); + return receive(ev).then(() => { + sinon.assert.calledWith(stubEmit, EVENTS.AD_RENDER_FAILED, sinon.match({ + reason: AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, + adId: 'missing' + })); + }); }); it('should emit AD_RENDER_FAILED if creative can\'t be sent to rendering frame', () => { @@ -271,11 +274,12 @@ describe('secureCreatives', () => { adId: bidId }) }); - receiveMessage(ev) - sinon.assert.calledWith(stubEmit, EVENTS.AD_RENDER_FAILED, sinon.match({ - reason: AD_RENDER_FAILED_REASON.EXCEPTION, - adId: bidId - })); + return receive(ev).then(() => { + sinon.assert.calledWith(stubEmit, EVENTS.AD_RENDER_FAILED, sinon.match({ + reason: AD_RENDER_FAILED_REASON.EXCEPTION, + adId: bidId + })); + }) }); it('should include renderers in responses', () => { @@ -287,8 +291,9 @@ describe('secureCreatives', () => { }, data: JSON.stringify({adId: bidId, message: 'Prebid Request'}) }); - receiveMessage(ev); - sinon.assert.calledWith(ev.source.postMessage, sinon.match(ob => JSON.parse(ob).renderer === 'mock-renderer')); + return receive(ev).then(() => { + sinon.assert.calledWith(ev.source.postMessage, sinon.match(ob => JSON.parse(ob).renderer === 'mock-renderer')); + }); }); if (FEATURES.NATIVE) { @@ -318,23 +323,24 @@ describe('secureCreatives', () => { }, data: JSON.stringify({adId: bidId, message: 'Prebid Request'}) }) - receiveMessage(ev); - sinon.assert.calledWith(ev.source.postMessage, sinon.match(ob => { - const data = JSON.parse(ob); - ['width', 'height'].forEach(prop => expect(data[prop]).to.not.exist); - const native = data.native; - sinon.assert.match(native, { - ortb: bid.native.ortb, - adTemplate: bid.native.adTemplate, - rendererUrl: bid.native.rendererUrl, - }) - expect(Object.fromEntries(native.assets.map(({key, value}) => [key, value]))).to.eql({ - adTemplate: bid.native.adTemplate, - rendererUrl: bid.native.rendererUrl, - body: 'vbody' - }); - return true; - })) + return receive(ev).then(() => { + sinon.assert.calledWith(ev.source.postMessage, sinon.match(ob => { + const data = JSON.parse(ob); + ['width', 'height'].forEach(prop => expect(data[prop]).to.not.exist); + const native = data.native; + sinon.assert.match(native, { + ortb: bid.native.ortb, + adTemplate: bid.native.adTemplate, + rendererUrl: bid.native.rendererUrl, + }) + expect(Object.fromEntries(native.assets.map(({key, value}) => [key, value]))).to.eql({ + adTemplate: bid.native.adTemplate, + rendererUrl: bid.native.rendererUrl, + body: 'vbody' + }); + return true; + })) + }); }) } }); @@ -361,16 +367,16 @@ describe('secureCreatives', () => { origin: 'any origin' }); - receiveMessage(ev); - - sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(stubGetAllAssetsMessage); - sinon.assert.calledWith(stubGetAllAssetsMessage, data, adResponse); - sinon.assert.calledOnce(ev.source.postMessage); - sinon.assert.notCalled(stubFireNativeTrackers); - sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); + return receive(ev).then(() => { + sinon.assert.neverCalledWith(spyLogWarn, warning); + sinon.assert.calledOnce(stubGetAllAssetsMessage); + sinon.assert.calledWith(stubGetAllAssetsMessage, data, adResponse); + sinon.assert.calledOnce(ev.source.postMessage); + sinon.assert.notCalled(stubFireNativeTrackers); + sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); + }); }); it('Prebid native should not fire BID_WON when receiveMessage is called more than once', () => { @@ -391,11 +397,12 @@ describe('secureCreatives', () => { origin: 'any origin' }); - receiveMessage(ev); - sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); - - receiveMessage(ev); - stubEmit.withArgs(EVENTS.BID_WON, adResponse).calledOnce; + return receive(ev).then(() => { + sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); + return receive(ev); + }).then(() => { + stubEmit.withArgs(EVENTS.BID_WON, adResponse).calledOnce; + }); }); }); @@ -422,13 +429,14 @@ describe('secureCreatives', () => { }, }) }); - receiveMessage(event); - expect(stubEmit.calledWith(EVENTS.AD_RENDER_FAILED, { - adId: bidId, - bid: adResponse, - reason: 'Fail reason', - message: 'Fail message' - })).to.equal(shouldEmit); + return receive(event).then(() => { + expect(stubEmit.calledWith(EVENTS.AD_RENDER_FAILED, { + adId: bidId, + bid: adResponse, + reason: 'Fail reason', + message: 'Fail message' + })).to.equal(shouldEmit); + }); }); it(`should${shouldEmit ? ' ' : ' not '}emit AD_RENDER_SUCCEEDED`, () => { @@ -439,12 +447,13 @@ describe('secureCreatives', () => { adId: bidId, }) }); - receiveMessage(event); - expect(stubEmit.calledWith(EVENTS.AD_RENDER_SUCCEEDED, { - adId: bidId, - bid: adResponse, - doc: null - })).to.equal(shouldEmit); + return receive(event).then(() => { + expect(stubEmit.calledWith(EVENTS.AD_RENDER_SUCCEEDED, { + adId: bidId, + bid: adResponse, + doc: null + })).to.equal(shouldEmit); + }); }); }); }); From f390f459461d5daf076cb7e4886d341e908a512a Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 23 Apr 2024 10:42:33 -0700 Subject: [PATCH 14/25] allow async bid retrieval on render: renderAd --- src/adRendering.js | 9 +- test/spec/unit/adRendering_spec.js | 24 ++- test/spec/unit/pbjs_api_spec.js | 214 ++++++++++++++----------- test/spec/unit/secureCreatives_spec.js | 2 - 4 files changed, 149 insertions(+), 100 deletions(-) diff --git a/src/adRendering.js b/src/adRendering.js index 1f2b95d85e2..136146f12a9 100644 --- a/src/adRendering.js +++ b/src/adRendering.js @@ -16,7 +16,7 @@ const { EXCEPTION } = AD_RENDER_FAILED_REASON; export const getBidToRender = hook('sync', function (adId, override = GreedyPromise.resolve()) { return override .then(bid => bid ?? auctionManager.findBidByAdId(adId)) - .catch(() => undefined) + .catch(() => {}) }, 'getBidToRender') export const markWinningBid = hook('sync', function (bid) { @@ -222,12 +222,13 @@ export function renderAdDirect(doc, adId, options) { if (!adId || !doc) { fail(AD_RENDER_FAILED_REASON.MISSING_DOC_OR_ADID, `missing ${adId ? 'doc' : 'adId'}`); } else { - bid = auctionManager.findBidByAdId(adId); - if ((doc === document && !inIframe())) { fail(AD_RENDER_FAILED_REASON.PREVENT_WRITING_ON_MAIN_DOCUMENT, `renderAd was prevented from writing to the main document.`); } else { - handleRender({renderFn, resizeFn, adId, options: {clickUrl: options?.clickThrough}, bidResponse: bid, doc}); + getBidToRender(adId).then(bidResponse => { + bid = bidResponse; + handleRender({renderFn, resizeFn, adId, options: {clickUrl: options?.clickThrough}, bidResponse, doc}); + }); } } } catch (e) { diff --git a/test/spec/unit/adRendering_spec.js b/test/spec/unit/adRendering_spec.js index df837e5547e..5001e2e4ed3 100644 --- a/test/spec/unit/adRendering_spec.js +++ b/test/spec/unit/adRendering_spec.js @@ -1,7 +1,7 @@ import * as events from 'src/events.js'; import * as utils from 'src/utils.js'; import { - doRender, + doRender, getBidToRender, getRenderingData, handleCreativeEvent, handleNativeMessage, @@ -24,6 +24,28 @@ describe('adRendering', () => { sandbox.restore(); }) + describe('getBidToRender', () => { + beforeEach(() => { + sandbox.stub(auctionManager, 'findBidByAdId').callsFake(() => 'auction-bid') + }); + it('should default to bid from auctionManager', () => { + return getBidToRender('adId', Promise.resolve(null)).then((res) => { + expect(res).to.eql('auction-bid'); + sinon.assert.calledWith(auctionManager.findBidByAdId, 'adId'); + }); + }); + it('should give precedence to override promise', () => { + return getBidToRender('adId', Promise.resolve('override')).then((res) => { + expect(res).to.eql('override'); + sinon.assert.notCalled(auctionManager.findBidByAdId); + }) + }); + it('should return undef when override rejects', () => { + return getBidToRender('adId', Promise.reject(new Error('any reason'))).then(res => { + expect(res).to.not.exist; + }) + }) + }) describe('getRenderingData', () => { let bidResponse; beforeEach(() => { diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index deb80873cfa..94643f34a05 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -28,6 +28,7 @@ import {mockFpdEnrichments} from '../../helpers/fpd.js'; import {generateUUID} from '../../../src/utils.js'; import {getCreativeRenderer} from '../../../src/creativeRenderers.js'; import { BID_STATUS, EVENTS, GRANULARITY_OPTIONS, TARGETING_KEYS } from 'src/constants.js'; +import {getBidToRender} from '../../../src/adRendering.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -201,12 +202,16 @@ window.apntag = { describe('Unit: Prebid Module', function () { let bidExpiryStub, sandbox; - + function getBidToRenderHook(next, adId) { + // make sure we can handle async bidToRender + next(adId, new Promise((resolve) => setTimeout(resolve))) + } before((done) => { hook.ready(); $$PREBID_GLOBAL$$.requestBids.getHooks().remove(); resetDebugging(); sinon.stub(filters, 'isActualBid').returns(true); // stub this out so that we can use vanilla objects as bids + getBidToRender.before(getBidToRenderHook, 100); // preload creative renderer getCreativeRenderer({}).then(() => done()); }); @@ -229,6 +234,7 @@ describe('Unit: Prebid Module', function () { after(function() { auctionManager.clearAllAuctions(); filters.isActualBid.restore(); + getBidToRender.getHooks({hook: getBidToRenderHook}).remove(); }); describe('and global adUnits', () => { @@ -1246,16 +1252,25 @@ describe('Unit: Prebid Module', function () { spyAddWinningBid.restore(); }); + function renderAd(...args) { + $$PREBID_GLOBAL$$.renderAd(...args); + return new Promise((resolve) => { + setTimeout(resolve, 10); + }); + } + it('should require doc and id params', function () { - $$PREBID_GLOBAL$$.renderAd(); - var error = 'Error rendering ad (id: undefined): missing adId'; - assert.ok(spyLogError.calledWith(error), 'expected param error was logged'); + return renderAd().then(() => { + var error = 'Error rendering ad (id: undefined): missing adId'; + assert.ok(spyLogError.calledWith(error), 'expected param error was logged'); + }) }); it('should log message with bid id', function () { - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - var message = 'Calling renderAd with adId :' + bidId; - assert.ok(spyLogMessage.calledWith(message), 'expected message was logged'); + return renderAd(doc, bidId).then(() => { + var message = 'Calling renderAd with adId :' + bidId; + assert.ok(spyLogMessage.calledWith(message), 'expected message was logged'); + }) }); it('should write the ad to the doc', function () { @@ -1263,23 +1278,26 @@ describe('Unit: Prebid Module', function () { ad: "" }); adResponse.ad = ""; - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - assert.ok(doc.write.calledWith(adResponse.ad), 'ad was written to doc'); - assert.ok(doc.close.called, 'close method called'); + return renderAd(doc, bidId).then(() => { + assert.ok(doc.write.calledWith(adResponse.ad), 'ad was written to doc'); + assert.ok(doc.close.called, 'close method called'); + }) }); it('should place the url inside an iframe on the doc', function () { pushBidResponseToAuction({ adUrl: 'http://server.example.com/ad/ad.js' }); - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - sinon.assert.calledWith(doc.createElement, 'iframe'); + return renderAd(doc, bidId).then(() => { + sinon.assert.calledWith(doc.createElement, 'iframe'); + }); }); it('should log an error when no ad or url', function () { pushBidResponseToAuction({}); - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - sinon.assert.called(spyLogError); + return renderAd(doc, bidId).then(() => { + sinon.assert.called(spyLogError); + }); }); it('should log an error when not in an iFrame', function () { @@ -1287,17 +1305,19 @@ describe('Unit: Prebid Module', function () { ad: "" }); inIframe = false; - $$PREBID_GLOBAL$$.renderAd(document, bidId); - const error = `Error rendering ad (id: ${bidId}): renderAd was prevented from writing to the main document.`; - assert.ok(spyLogError.calledWith(error), 'expected error was logged'); + return renderAd(document, bidId).then(() => { + const error = `Error rendering ad (id: ${bidId}): renderAd was prevented from writing to the main document.`; + assert.ok(spyLogError.calledWith(error), 'expected error was logged'); + }); }); it('should not render videos', function () { pushBidResponseToAuction({ mediatype: 'video' }); - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - sinon.assert.notCalled(doc.write); + return renderAd(doc, bidId).then(() => { + sinon.assert.notCalled(doc.write); + }); }); it('should catch errors thrown when trying to write ads to the page', function () { @@ -1307,25 +1327,28 @@ describe('Unit: Prebid Module', function () { var error = { message: 'doc write error' }; doc.write = sinon.stub().throws(error); - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - var errorMessage = `Error rendering ad (id: ${bidId}): doc write error` - assert.ok(spyLogError.calledWith(errorMessage), 'expected error was logged'); + return renderAd(doc, bidId).then(() => { + var errorMessage = `Error rendering ad (id: ${bidId}): doc write error` + assert.ok(spyLogError.calledWith(errorMessage), 'expected error was logged'); + }); }); it('should log an error when ad not found', function () { var fakeId = 99; - $$PREBID_GLOBAL$$.renderAd(doc, fakeId); - var error = `Error rendering ad (id: ${fakeId}): Cannot find ad '${fakeId}'` - assert.ok(spyLogError.calledWith(error), 'expected error was logged'); + return renderAd(doc, fakeId).then(() => { + var error = `Error rendering ad (id: ${fakeId}): Cannot find ad '${fakeId}'` + assert.ok(spyLogError.calledWith(error), 'expected error was logged'); + }); }); it('should save bid displayed to winning bid', function () { pushBidResponseToAuction({ ad: "" }); - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - assert.deepEqual($$PREBID_GLOBAL$$.getAllWinningBids()[0], adResponse); + return renderAd(doc, bidId).then(() => { + assert.deepEqual($$PREBID_GLOBAL$$.getAllWinningBids()[0], adResponse); + }); }); it('fires billing url if present on s2s bid', function () { @@ -1336,22 +1359,23 @@ describe('Unit: Prebid Module', function () { burl }); - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - - sinon.assert.calledOnce(triggerPixelStub); - sinon.assert.calledWith(triggerPixelStub, burl); + return renderAd(doc, bidId).then(() => { + sinon.assert.calledOnce(triggerPixelStub); + sinon.assert.calledWith(triggerPixelStub, burl); + }); }); it('should call addWinningBid', function () { pushBidResponseToAuction({ ad: "" }); - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - var message = 'Calling renderAd with adId :' + bidId; - sinon.assert.calledWith(spyLogMessage, message); + return renderAd(doc, bidId).then(() => { + var message = 'Calling renderAd with adId :' + bidId; + sinon.assert.calledWith(spyLogMessage, message); - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.calledWith(spyAddWinningBid, adResponse); + }); }); it('should warn stale rendering', function () { @@ -1368,38 +1392,40 @@ describe('Unit: Prebid Module', function () { }); // First render should pass with no warning and added to winning bids - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - sinon.assert.calledWith(spyLogMessage, message); - sinon.assert.neverCalledWith(spyLogWarn, warning); + return renderAd(doc, bidId).then(() => { + sinon.assert.calledWith(spyLogMessage, message); + sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.calledWith(spyAddWinningBid, adResponse); - sinon.assert.calledWith(onWonEvent, adResponse); - sinon.assert.notCalled(onStaleEvent); - expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); + sinon.assert.calledWith(onWonEvent, adResponse); + sinon.assert.notCalled(onStaleEvent); + expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); - // Reset call history for spies and stubs - spyLogMessage.resetHistory(); - spyLogWarn.resetHistory(); - spyAddWinningBid.resetHistory(); - onWonEvent.resetHistory(); - onStaleEvent.resetHistory(); + // Reset call history for spies and stubs + spyLogMessage.resetHistory(); + spyLogWarn.resetHistory(); + spyAddWinningBid.resetHistory(); + onWonEvent.resetHistory(); + onStaleEvent.resetHistory(); - // Second render should have a warning but still added to winning bids - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - sinon.assert.calledWith(spyLogMessage, message); - sinon.assert.calledWith(spyLogWarn, warning); + // Second render should have a warning but still added to winning bids + return renderAd(doc, bidId); + }).then(() => { + sinon.assert.calledWith(spyLogMessage, message); + sinon.assert.calledWith(spyLogWarn, warning); - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.calledWith(spyAddWinningBid, adResponse); - sinon.assert.calledWith(onWonEvent, adResponse); - sinon.assert.calledWith(onStaleEvent, adResponse); + sinon.assert.calledWith(onWonEvent, adResponse); + sinon.assert.calledWith(onStaleEvent, adResponse); - // Clean up - $$PREBID_GLOBAL$$.offEvent(EVENTS.BID_WON, onWonEvent); - $$PREBID_GLOBAL$$.offEvent(EVENTS.STALE_RENDER, onStaleEvent); + // Clean up + $$PREBID_GLOBAL$$.offEvent(EVENTS.BID_WON, onWonEvent); + $$PREBID_GLOBAL$$.offEvent(EVENTS.STALE_RENDER, onStaleEvent); + }); }); it('should stop stale rendering', function () { @@ -1419,38 +1445,40 @@ describe('Unit: Prebid Module', function () { }); // First render should pass with no warning and added to winning bids - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - sinon.assert.calledWith(spyLogMessage, message); - sinon.assert.neverCalledWith(spyLogWarn, warning); - - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); - expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); - - sinon.assert.calledWith(onWonEvent, adResponse); - sinon.assert.notCalled(onStaleEvent); - - // Reset call history for spies and stubs - spyLogMessage.resetHistory(); - spyLogWarn.resetHistory(); - spyAddWinningBid.resetHistory(); - onWonEvent.resetHistory(); - onStaleEvent.resetHistory(); - - // Second render should have a warning and do not proceed further - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - sinon.assert.calledWith(spyLogMessage, message); - sinon.assert.calledWith(spyLogWarn, warning); - - sinon.assert.notCalled(spyAddWinningBid); - - sinon.assert.notCalled(onWonEvent); - sinon.assert.calledWith(onStaleEvent, adResponse); - - // Clean up - $$PREBID_GLOBAL$$.offEvent(EVENTS.BID_WON, onWonEvent); - $$PREBID_GLOBAL$$.offEvent(EVENTS.STALE_RENDER, onStaleEvent); - configObj.setConfig({'auctionOptions': {}}); + return renderAd(doc, bidId).then(() => { + sinon.assert.calledWith(spyLogMessage, message); + sinon.assert.neverCalledWith(spyLogWarn, warning); + + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.calledWith(spyAddWinningBid, adResponse); + expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); + + sinon.assert.calledWith(onWonEvent, adResponse); + sinon.assert.notCalled(onStaleEvent); + + // Reset call history for spies and stubs + spyLogMessage.resetHistory(); + spyLogWarn.resetHistory(); + spyAddWinningBid.resetHistory(); + onWonEvent.resetHistory(); + onStaleEvent.resetHistory(); + + // Second render should have a warning and do not proceed further + return renderAd(doc, bidId); + }).then(() => { + sinon.assert.calledWith(spyLogMessage, message); + sinon.assert.calledWith(spyLogWarn, warning); + + sinon.assert.notCalled(spyAddWinningBid); + + sinon.assert.notCalled(onWonEvent); + sinon.assert.calledWith(onStaleEvent, adResponse); + + // Clean up + $$PREBID_GLOBAL$$.offEvent(EVENTS.BID_WON, onWonEvent); + $$PREBID_GLOBAL$$.offEvent(EVENTS.STALE_RENDER, onStaleEvent); + configObj.setConfig({'auctionOptions': {}}); + }); }); }); diff --git a/test/spec/unit/secureCreatives_spec.js b/test/spec/unit/secureCreatives_spec.js index b51f152f1d8..a07f16c85c0 100644 --- a/test/spec/unit/secureCreatives_spec.js +++ b/test/spec/unit/secureCreatives_spec.js @@ -144,7 +144,6 @@ describe('secureCreatives', () => { }); describe('Prebid Request', function() { - it('should render', function () { pushBidResponseToAuction({ renderer: {render: sinon.stub(), url: 'some url'} @@ -170,7 +169,6 @@ describe('secureCreatives', () => { expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); }); - }); it('should allow stale rendering without config', function () { From c94413f24d5e76037c5a0e1152afdad8c1481f35 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 23 Apr 2024 10:54:31 -0700 Subject: [PATCH 15/25] do not force string on requestedSize --- modules/paapi.js | 4 ++-- test/spec/modules/paapi_spec.js | 22 +++++++++++----------- test/spec/modules/topLevelPaapi_spec.js | 8 ++++---- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/modules/paapi.js b/modules/paapi.js index e2a76e87922..58b45703d1b 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -211,8 +211,8 @@ function getRequestedSize(adUnit) { const size = getPAAPISize(sizesToSizeTuples(adUnit.mediaTypes?.banner?.sizes)); if (size) { return { - width: size[0].toString(), - height: size[1].toString() + width: size[0], + height: size[1] }; } })(); diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js index 43e01d927f4..300347ee870 100644 --- a/test/spec/modules/paapi_spec.js +++ b/test/spec/modules/paapi_spec.js @@ -342,8 +342,8 @@ describe('paapi module', () => { ext: { paapi: { requestedSize: { - width: '123', - height: '321' + width: 123, + height: 321 } } } @@ -357,24 +357,24 @@ describe('paapi module', () => { beforeEach(setup); it('without overriding component auctions, if set', () => { - fledgeAuctionConfig.requestedSize = {width: '1', height: '2'}; + fledgeAuctionConfig.requestedSize = {width: '1px', height: '2px'}; expect(getConfig().componentAuctions[0].requestedSize).to.eql({ - width: '1', - height: '2' + width: '1px', + height: '2px' }) }); it('on component auction, if missing', () => { expect(getConfig().componentAuctions[0].requestedSize).to.eql({ - width: '123', - height: '321' + width: 123, + height: 321 }); }); it('on top level auction', () => { expect(getConfig().requestedSize).to.eql({ - width: '123', - height: '321' + width: 123, + height: 321, }) }) }); @@ -620,8 +620,8 @@ describe('paapi module', () => { Object.values(mark()).flatMap(b => b.bids).forEach(bidRequest => { sinon.assert.match(bidRequest.ortb2Imp.ext.paapi, { requestedSize: { - width: '123', - height: '321' + width: 123, + height: 321 } }) }); diff --git a/test/spec/modules/topLevelPaapi_spec.js b/test/spec/modules/topLevelPaapi_spec.js index eecfbe36e31..958a4c75050 100644 --- a/test/spec/modules/topLevelPaapi_spec.js +++ b/test/spec/modules/topLevelPaapi_spec.js @@ -57,8 +57,8 @@ describe('topLevelPaapi', () => { ext: { paapi: { requestedSize: { - width: '123', - height: '321' + width: 123, + height: 321 } } } @@ -143,8 +143,8 @@ describe('topLevelPaapi', () => { expect(Object.keys(actual)).to.eql(Object.keys(expected)); Object.entries(expected).forEach(([au, val]) => { sinon.assert.match(actual[au], val == null ? val : { - width: '123', - height: '321', + width: 123, + height: 321, ...unpack(val) }); }); From 914cc14a82888d18b2acffe57898320f2dc55a5b Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 23 Apr 2024 14:37:35 -0700 Subject: [PATCH 16/25] support rendering of paapi bids --- .../gpt/top-level-paapi/tl_paapi_example.html | 22 ++-- modules/topLevelPaapi.js | 94 +++++++++++++- src/adRendering.js | 4 +- test/spec/modules/paapi_spec.js | 72 ++++++----- test/spec/modules/topLevelPaapi_spec.js | 117 ++++++++++++++++-- 5 files changed, 244 insertions(+), 65 deletions(-) diff --git a/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html b/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html index 4ddd8a19f50..b90f0c5ff03 100644 --- a/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html +++ b/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html @@ -79,27 +79,19 @@ let contextual = false; Object.entries(bids).forEach(([adUnitCode, bid]) => { const container = document.getElementById(adUnitCode); - const paapiDiv = container.querySelector('.paapi'); + const paapiFrame = container.querySelector('.paapi'); const gamDiv = container.querySelector('.gam'); if (bid) { // if we have a paapi winner, hide the GAM slot - // and insert a paapi iframe - const iframe = document.createElement('iframe'); - Object.assign(iframe, { - src: bid.urn, - frameBorder: 0, - scrolling: 'no', - width: bid.width, - height: bid.height, - }); - paapiDiv.innerHTML = ''; - paapiDiv.appendChild(iframe); - paapiDiv.style.display = 'block' + // render in the paapi iframe + paapiFrame.contentDocument.body.innerHTML = ''; + pbjs.renderAd(paapiFrame.contentDocument, bid.adId); + paapiFrame.style.display = 'block' gamDiv.style.display = 'none'; } else { // otherwise, hide the paapi iframe and show the ad slot contextual = true; - paapiDiv.style.display = 'none'; + paapiFrame.style.display = 'none'; gamDiv.style.display = 'block'; } }); @@ -147,7 +139,7 @@

Standalone PAAPI Prebid.js Example

Div-1
-
+
- + + @@ -36,6 +27,12 @@ site: 'daa30ba1-5613-4a2c-b7f0-34e2c033202a' }, }, + { + bidder: 'contextual', + params: { + site: 'daa30ba1-5613-4a2c-b7f0-34e2c033202a' + } + } ], } ] @@ -61,56 +58,33 @@ topLevelSeller: { auctionConfig: { seller: window.location.origin, - decisionLogicURL: new URL('decisionLogic.js', window.location).toString(), - } + decisionLogicURL: new URL('shared/decisionLogic.js', window.location).toString(), + }, + overrideWinner: true } }, }); pbjs.addAdUnits(adUnits); + requestBids(); + }); + + function requestBids() { + pbjs.adserverRequestSent = false; pbjs.requestBids({ bidsBackHandler: sendAdserverRequest, timeout: PREBID_TIMEOUT }); - }); - - function raa() { - return pbjs.getPAAPIBids().then(bids => { - let contextual = false; - Object.entries(bids).forEach(([adUnitCode, bid]) => { - const container = document.getElementById(adUnitCode); - const paapiFrame = container.querySelector('.paapi'); - const gamDiv = container.querySelector('.gam'); - if (bid) { - // if we have a paapi winner, hide the GAM slot - // render in the paapi iframe - paapiFrame.contentDocument.body.innerHTML = ''; - pbjs.renderAd(paapiFrame.contentDocument, bid.adId); - paapiFrame.style.display = 'block' - gamDiv.style.display = 'none'; - } else { - // otherwise, hide the paapi iframe and show the ad slot - contextual = true; - paapiFrame.style.display = 'none'; - gamDiv.style.display = 'block'; - } - }); - return contextual; - }); } function sendAdserverRequest() { if (pbjs.adserverRequestSent) return; pbjs.adserverRequestSent = true; - raa().then((contextual) => { - if (contextual) { - googletag.cmd.push(function () { - pbjs.que.push(function () { - pbjs.setTargetingForGPTAsync(); - googletag.pubads().refresh(); - }); - }); - } + googletag.cmd.push(function () { + pbjs.que.push(function () { + pbjs.setTargetingForGPTAsync(); + googletag.pubads().refresh(); + }); }); } @@ -119,7 +93,7 @@ }, FAILSAFE_TIMEOUT); googletag.cmd.push(function () { - googletag.defineSlot('/19968336/header-bid-tag-0', [[300, 250], [300, 600]], 'div-1-gam').addService(googletag.pubads()); + googletag.defineSlot('/41758329/integ-test', [[300, 250], [300, 600]], 'div-1').setTargeting('creative', 'banner-safeframe').addService(googletag.pubads()); googletag.pubads().enableSingleRequest(); googletag.enableServices(); @@ -128,7 +102,26 @@ -

Standalone PAAPI Prebid.js Example

+

GAM contextual + Publisher as top level PAAPI seller example

+ +

+ This example starts PAAPI auctions at the same time as GAM targeting. The flow is + similar to a typical GAM auction, but if Prebid wins, and got a + PAAPI bid, it is rendered instead of the contextual bid. +

+
+ +
+
Div-1
+
+ +
+
+

Instructions

Start local server with:

gulp serve-fast --https

Chrome flags:

@@ -136,18 +129,6 @@

Standalone PAAPI Prebid.js Example

--privacy-sandbox-enrollment-overrides=https://localhost:9999

Join interest group at https://www.optable.co/

-
Div-1
- -
- -
- -
- diff --git a/integrationExamples/gpt/top-level-paapi/decisionLogic.js b/integrationExamples/top-level-paapi/shared/decisionLogic.js similarity index 100% rename from integrationExamples/gpt/top-level-paapi/decisionLogic.js rename to integrationExamples/top-level-paapi/shared/decisionLogic.js diff --git a/integrationExamples/top-level-paapi/shared/example-setup.js b/integrationExamples/top-level-paapi/shared/example-setup.js new file mode 100644 index 00000000000..9c259427676 --- /dev/null +++ b/integrationExamples/top-level-paapi/shared/example-setup.js @@ -0,0 +1,94 @@ +// intercept navigator.runAdAuction and print parameters to console +(() => { + var originalRunAdAuction = navigator.runAdAuction; + navigator.runAdAuction = function (...args) { + console.log('%c runAdAuction', 'background: cyan; border: 2px; border-radius: 3px', ...args); + return originalRunAdAuction.apply(navigator, args); + }; +})(); +init(); +setupContextualResponse(); + +function addExampleControls(requestBids) { + const ctl = document.createElement('div'); + ctl.innerHTML = ` + + Simulate contextual bid: + + + + `; + ctl.style = 'margin-top: 30px'; + document.body.appendChild(ctl); + ctl.addEventListener('click', function (ev) { + const cpm = ev.target?.dataset?.cpm; + if (cpm) { + setupContextualResponse(parseInt(cpm, 10)); + } + requestBids(); + }); +} + +function init() { + window.pbjs = window.pbjs || {que: []}; + window.pbjs.que.push(() => { + pbjs.aliasBidder('optable', 'contextual'); + [ + 'auctionInit', + 'auctionTimeout', + 'auctionEnd', + 'bidAdjustment', + 'bidTimeout', + 'bidRequested', + 'bidResponse', + 'bidRejected', + 'noBid', + 'seatNonBid', + 'bidWon', + 'bidderDone', + 'bidderError', + 'setTargeting', + 'beforeRequestBids', + 'beforeBidderHttp', + 'requestBids', + 'addAdUnits', + 'adRenderFailed', + 'adRenderSucceeded', + 'tcf2Enforcement', + 'auctionDebug', + 'bidViewable', + 'staleRender', + 'billableEvent', + 'bidAccepted', + 'paapiRunAuction', + 'paapiBid', + 'paapiNoBid', + 'paapiError', + ].forEach(evt => { + pbjs.onEvent(evt, (arg) => { + console.log('Event:', evt, arg); + }) + }); + }); +} + +function setupContextualResponse(cpm = 1) { + pbjs.que.push(() => { + pbjs.setConfig({ + debugging: { + enabled: true, + intercept: [ + { + when: { + bidder: 'contextual' + }, + then: { + cpm, + currency: 'USD' + } + } + ] + } + }); + }); +} diff --git a/modules/topLevelPaapi.js b/modules/topLevelPaapi.js index e9bb5e0e25b..0847e29a49d 100644 --- a/modules/topLevelPaapi.js +++ b/modules/topLevelPaapi.js @@ -1,6 +1,6 @@ import {submodule} from '../src/hook.js'; import {config} from '../src/config.js'; -import {logError, logWarn, mergeDeep} from '../src/utils.js'; +import {logError, logInfo, logWarn, mergeDeep} from '../src/utils.js'; import {auctionStore} from '../libraries/weakStore/weakStore.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {emit} from '../src/events.js'; @@ -40,7 +40,10 @@ function bidIfRenderable(bid) { return bid; } -function renderPaapiHook(next, adId, override = GreedyPromise.resolve()) { +const forRenderCtx = []; + +function renderPaapiHook(next, adId, forRender = true, override = GreedyPromise.resolve()) { + forRenderCtx.push(forRender); const ids = parsePaapiAdId(adId); if (ids) { override = override.then((bid) => { @@ -54,18 +57,25 @@ function renderPaapiHook(next, adId, override = GreedyPromise.resolve()) { }); }); } - next(adId, override); + next(adId, forRender, override); } function renderOverrideHook(next, bidPm) { + const forRender = forRenderCtx.pop(); if (moduleConfig?.overrideWinner) { bidPm = bidPm.then((bid) => { if (isPaapiBid(bid)) return bid; return getPAAPIBids({adUnitCode: bid.adUnitCode}).then(res => { let paapiBid = bidIfRenderable(res[bid.adUnitCode]); - return paapiBid && paapiBid.status !== BID_STATUS.RENDERED - ? paapiBid - : bid; + if (paapiBid) { + if (!forRender) return paapiBid; + if (forRender && paapiBid.status !== BID_STATUS.RENDERED) { + paapiBid.overriddenAdId = bid.adId; + logInfo(MODULE_NAME, 'overriding contextual bid with PAAPI bid', bid, paapiBid) + return paapiBid; + } + } + return bid; }); }); } @@ -85,7 +95,12 @@ export function getRenderingDataHook(next, bid, options) { } export function markWinningBidHook(next, bid) { - isPaapiBid(bid) ? next.bail() : next(bid); + if (isPaapiBid(bid)) { + bid.status = BID_STATUS.RENDERED; + next.bail(); + } else { + next(bid); + } } function getBaseAuctionConfig() { diff --git a/src/adRendering.js b/src/adRendering.js index 6965d457911..33f7fe9252c 100644 --- a/src/adRendering.js +++ b/src/adRendering.js @@ -13,7 +13,7 @@ import {GreedyPromise} from './utils/promise.js'; const { AD_RENDER_FAILED, AD_RENDER_SUCCEEDED, STALE_RENDER, BID_WON } = EVENTS; const { EXCEPTION } = AD_RENDER_FAILED_REASON; -export const getBidToRender = hook('sync', function (adId, override = GreedyPromise.resolve()) { +export const getBidToRender = hook('sync', function (adId, forRender = true, override = GreedyPromise.resolve()) { return override .then(bid => bid ?? auctionManager.findBidByAdId(adId)) .catch(() => {}) diff --git a/src/secureCreatives.js b/src/secureCreatives.js index 189e888c32e..7bac3db37e6 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -47,6 +47,12 @@ export function getReplier(ev) { } } +function ensureAdId(adId, reply) { + return function (data, ...args) { + return reply(Object.assign(data, {adId}), ...args); + } +} + export function receiveMessage(ev) { var key = ev.message ? 'message' : 'data'; var data = {}; @@ -57,15 +63,18 @@ export function receiveMessage(ev) { } if (data && data.adId && data.message && HANDLER_MAP.hasOwnProperty(data.message)) { - return getBidToRender(data.adId).then(adObject => { - HANDLER_MAP[data.message](getReplier(ev), data, adObject); + return getBidToRender(data.adId, data.message === MESSAGES.REQUEST).then(adObject => { + HANDLER_MAP[data.message](ensureAdId(data.adId, getReplier(ev)), data, adObject); }) } } -function getResizer(bidResponse) { +function getResizer(adId, bidResponse) { + // in some situations adId !== bidResponse.adId + // the first is the one that was requested and is tied to the element + // the second is the one that is being rendered (sometimes different, e.g. in some paapi setups) return function (width, height) { - resizeRemoteCreative({...bidResponse, width, height}); + resizeRemoteCreative({...bidResponse, width, height, adId}); } } function handleRenderRequest(reply, message, bidResponse) { @@ -76,7 +85,7 @@ function handleRenderRequest(reply, message, bidResponse) { renderer: getCreativeRendererSource(bidResponse) }, adData)); }, - resizeFn: getResizer(bidResponse), + resizeFn: getResizer(message.adId, bidResponse), options: message.options, adId: message.adId, bidResponse diff --git a/test/spec/modules/topLevelPaapi_spec.js b/test/spec/modules/topLevelPaapi_spec.js index 359810e544e..946040bd8b7 100644 --- a/test/spec/modules/topLevelPaapi_spec.js +++ b/test/spec/modules/topLevelPaapi_spec.js @@ -12,7 +12,7 @@ import { getPAAPIBids, getRenderingDataHook, markWinningBidHook, parsePaapiAdId, - parsePaapiSize, + parsePaapiSize, resizeCreativeHook, topLevelPAAPI } from '/modules/topLevelPaapi.js'; import {auctionManager} from '../../../src/auctionManager.js'; @@ -311,6 +311,7 @@ describe('topLevelPaapi', () => { ]).then(([paapiBid, bidToRender]) => { if (canRender) { expect(bidToRender).to.eql(paapiBid); + expect(paapiBid.overriddenAdId).to.eql(mockContextual.adId); } else { expect(bidToRender).to.eql(mockContextual) } @@ -332,14 +333,21 @@ describe('topLevelPaapi', () => { }) }); - it('should not not override when the bid was already rendered', () => { - return getBids({adUnitCode: 'au', auctionId}).then(res => { - res.au.status = BID_STATUS.RENDERED; - return getBidToRender(mockContextual.adId) - }).then(bidToRender => { - expect(bidToRender).to.eql(mockContextual); + if (canRender) { + it('should not not override when the bid was already rendered', () => { + getBids(); + return getBidToRender(mockContextual.adId, true).then((bid) => { + expect(bid.source).to.eql('paapi'); + bid.status = BID_STATUS.RENDERED; + return getBidToRender(mockContextual.adId, false).then(bidToRender => [bid, bidToRender]) + }).then(([paapiBid, bidToRender]) => { + expect(bidToRender).to.eql(paapiBid); + return getBidToRender(mockContextual.adId); + }).then(bidToRender => { + expect(bidToRender).to.eql(mockContextual); + }); }) - }) + } }); }); @@ -463,9 +471,11 @@ describe('topLevelPaapi', () => { describe('markWinnigBidsHook', () => { it('stops paapi bids', () => { - markWinningBidHook(next, {source: 'paapi'}); + const bid = {source: 'paapi'}; + markWinningBidHook(next, bid); sinon.assert.notCalled(next); sinon.assert.called(next.bail); + expect(bid.status).to.eql(BID_STATUS.RENDERED); }); it('ignores non-paapi bids', () => { markWinningBidHook(next, {other: 'bid'}); diff --git a/test/spec/unit/adRendering_spec.js b/test/spec/unit/adRendering_spec.js index 5001e2e4ed3..4d0962a0b2c 100644 --- a/test/spec/unit/adRendering_spec.js +++ b/test/spec/unit/adRendering_spec.js @@ -29,19 +29,19 @@ describe('adRendering', () => { sandbox.stub(auctionManager, 'findBidByAdId').callsFake(() => 'auction-bid') }); it('should default to bid from auctionManager', () => { - return getBidToRender('adId', Promise.resolve(null)).then((res) => { + return getBidToRender('adId', true, Promise.resolve(null)).then((res) => { expect(res).to.eql('auction-bid'); sinon.assert.calledWith(auctionManager.findBidByAdId, 'adId'); }); }); it('should give precedence to override promise', () => { - return getBidToRender('adId', Promise.resolve('override')).then((res) => { + return getBidToRender('adId', true, Promise.resolve('override')).then((res) => { expect(res).to.eql('override'); sinon.assert.notCalled(auctionManager.findBidByAdId); }) }); it('should return undef when override rejects', () => { - return getBidToRender('adId', Promise.reject(new Error('any reason'))).then(res => { + return getBidToRender('adId', true, Promise.reject(new Error('any reason'))).then(res => { expect(res).to.not.exist; }) }) From 1f2b7529551bfd2bd0fbb38e99bbf3aed621d4a1 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 24 Apr 2024 14:49:15 -0700 Subject: [PATCH 21/25] fix tests --- src/secureCreatives.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/secureCreatives.js b/src/secureCreatives.js index 7bac3db37e6..a33f742b738 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -49,7 +49,7 @@ export function getReplier(ev) { function ensureAdId(adId, reply) { return function (data, ...args) { - return reply(Object.assign(data, {adId}), ...args); + return reply(Object.assign({}, data, {adId}), ...args); } } From d6bd7b02f4f1361886e096414210f1e66dfc498c Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 25 Apr 2024 06:57:05 -0700 Subject: [PATCH 22/25] emit BID_WON for paapi bids --- modules/topLevelPaapi.js | 1 + test/spec/modules/topLevelPaapi_spec.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/topLevelPaapi.js b/modules/topLevelPaapi.js index 0847e29a49d..08caf106fc4 100644 --- a/modules/topLevelPaapi.js +++ b/modules/topLevelPaapi.js @@ -97,6 +97,7 @@ export function getRenderingDataHook(next, bid, options) { export function markWinningBidHook(next, bid) { if (isPaapiBid(bid)) { bid.status = BID_STATUS.RENDERED; + emit(EVENTS.BID_WON, bid); next.bail(); } else { next(bid); diff --git a/test/spec/modules/topLevelPaapi_spec.js b/test/spec/modules/topLevelPaapi_spec.js index 946040bd8b7..b19fa9751b0 100644 --- a/test/spec/modules/topLevelPaapi_spec.js +++ b/test/spec/modules/topLevelPaapi_spec.js @@ -470,12 +470,16 @@ describe('topLevelPaapi', () => { }); describe('markWinnigBidsHook', () => { - it('stops paapi bids', () => { + beforeEach(() => { + sandbox.stub(events, 'emit'); + }); + it('handles paapi bids', () => { const bid = {source: 'paapi'}; markWinningBidHook(next, bid); sinon.assert.notCalled(next); sinon.assert.called(next.bail); expect(bid.status).to.eql(BID_STATUS.RENDERED); + sinon.assert.calledWith(events.emit, EVENTS.BID_WON, bid); }); it('ignores non-paapi bids', () => { markWinningBidHook(next, {other: 'bid'}); From 319345c5aa36f62ce49d9cbeb63ef4489a905569 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 25 Apr 2024 07:22:49 -0700 Subject: [PATCH 23/25] add no ad server example --- .../top-level-paapi/no_adserver.html | 113 ++++++++++++++++++ .../top-level-paapi/shared/example-setup.js | 9 +- 2 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 integrationExamples/top-level-paapi/no_adserver.html diff --git a/integrationExamples/top-level-paapi/no_adserver.html b/integrationExamples/top-level-paapi/no_adserver.html new file mode 100644 index 00000000000..0b37f80f27c --- /dev/null +++ b/integrationExamples/top-level-paapi/no_adserver.html @@ -0,0 +1,113 @@ + + + + + + + + + +

No ad server, publisher as top level PAAPI seller example

+ +

+ +

+
+ +
+
Div-1
+
+ +
+
+

Instructions

+

Start local server with:

+ gulp serve-fast --https +

Chrome flags:

+ --enable-features=CookieDeprecationFacilitatedTesting:label/treatment_1.2/force_eligible/true + --privacy-sandbox-enrollment-overrides=https://localhost:9999 +

Join interest group at https://www.optable.co/ +

+
+ + diff --git a/integrationExamples/top-level-paapi/shared/example-setup.js b/integrationExamples/top-level-paapi/shared/example-setup.js index 9c259427676..1c52abf02c9 100644 --- a/integrationExamples/top-level-paapi/shared/example-setup.js +++ b/integrationExamples/top-level-paapi/shared/example-setup.js @@ -14,14 +14,15 @@ function addExampleControls(requestBids) { ctl.innerHTML = ` Simulate contextual bid: - - + + CPM + `; ctl.style = 'margin-top: 30px'; document.body.appendChild(ctl); - ctl.addEventListener('click', function (ev) { - const cpm = ev.target?.dataset?.cpm; + ctl.querySelector('.bid').addEventListener('click', function (ev) { + const cpm = ctl.querySelector('.cpm').value; if (cpm) { setupContextualResponse(parseInt(cpm, 10)); } From 361068c856e50e0a5bf8efe311b9d63f074acae4 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 25 Apr 2024 07:51:56 -0700 Subject: [PATCH 24/25] improve bid override logic --- modules/topLevelPaapi.js | 8 ++++---- test/spec/modules/topLevelPaapi_spec.js | 11 ++++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/modules/topLevelPaapi.js b/modules/topLevelPaapi.js index 08caf106fc4..040c0125b3a 100644 --- a/modules/topLevelPaapi.js +++ b/modules/topLevelPaapi.js @@ -40,10 +40,10 @@ function bidIfRenderable(bid) { return bid; } -const forRenderCtx = []; +const forRenderStack = []; function renderPaapiHook(next, adId, forRender = true, override = GreedyPromise.resolve()) { - forRenderCtx.push(forRender); + forRenderStack.push(forRender); const ids = parsePaapiAdId(adId); if (ids) { override = override.then((bid) => { @@ -61,10 +61,10 @@ function renderPaapiHook(next, adId, forRender = true, override = GreedyPromise. } function renderOverrideHook(next, bidPm) { - const forRender = forRenderCtx.pop(); + const forRender = forRenderStack.pop(); if (moduleConfig?.overrideWinner) { bidPm = bidPm.then((bid) => { - if (isPaapiBid(bid)) return bid; + if (isPaapiBid(bid) || bid?.status === BID_STATUS.RENDERED) return bid; return getPAAPIBids({adUnitCode: bid.adUnitCode}).then(res => { let paapiBid = bidIfRenderable(res[bid.adUnitCode]); if (paapiBid) { diff --git a/test/spec/modules/topLevelPaapi_spec.js b/test/spec/modules/topLevelPaapi_spec.js index b19fa9751b0..cdc0770386c 100644 --- a/test/spec/modules/topLevelPaapi_spec.js +++ b/test/spec/modules/topLevelPaapi_spec.js @@ -336,14 +336,23 @@ describe('topLevelPaapi', () => { if (canRender) { it('should not not override when the bid was already rendered', () => { getBids(); - return getBidToRender(mockContextual.adId, true).then((bid) => { + return getBidToRender(mockContextual.adId).then((bid) => { + // first pass - paapi wins over contextual expect(bid.source).to.eql('paapi'); bid.status = BID_STATUS.RENDERED; return getBidToRender(mockContextual.adId, false).then(bidToRender => [bid, bidToRender]) }).then(([paapiBid, bidToRender]) => { + // if `forRender` = false (bit retrieved for x-domain events and such) + // the referenced bid is still paapi expect(bidToRender).to.eql(paapiBid); return getBidToRender(mockContextual.adId); }).then(bidToRender => { + // second pass, paapi has been rendered, contextual should win + expect(bidToRender).to.eql(mockContextual); + bidToRender.status = BID_STATUS.RENDERED; + return getBidToRender(mockContextual.adId, false); + }).then(bidToRender => { + // if the contextual bid has been rendered, it's the one being referenced expect(bidToRender).to.eql(mockContextual); }); }) From 07c9ff387741930b5823a119eaa515a9fdd8fbbf Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Mon, 3 Jun 2024 12:47:08 -0700 Subject: [PATCH 25/25] fix lint --- test/spec/modules/paapi_spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js index 24378ec89f1..768e2ba8853 100644 --- a/test/spec/modules/paapi_spec.js +++ b/test/spec/modules/paapi_spec.js @@ -860,7 +860,6 @@ describe('paapi module', () => { }); }); }); - }); }); });