From 92dc73da94704f4c579e65fff3a454f04ca47194 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Mon, 2 Sep 2024 21:07:52 -0700 Subject: [PATCH 01/14] WIP parallel paapi --- modules/optableBidAdapter.js | 13 +- modules/paapi.js | 188 ++++++++++++++++++++++--- src/adapters/bidderFactory.js | 2 +- test/spec/modules/paapi_spec.js | 236 +++++++++++++++++++++++++++++++- 4 files changed, 412 insertions(+), 27 deletions(-) diff --git a/modules/optableBidAdapter.js b/modules/optableBidAdapter.js index 4e639fb88ee..0ff1e7bf220 100644 --- a/modules/optableBidAdapter.js +++ b/modules/optableBidAdapter.js @@ -3,6 +3,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js' +import {deepClone} from "../src/utils.js"; const converter = ortbConverter({ context: { netRevenue: true, ttl: 300 }, imp(buildImp, bidRequest, context) { @@ -30,9 +31,17 @@ export const spec = { }, interpretResponse: function(response, request) { const bids = converter.fromORTB({ response: response.body, request: request.data }).bids - const auctionConfigs = (response.body.ext?.optable?.fledge?.auctionconfigs ?? []).map((cfg) => { + const auctionConfigs = (response.body.ext?.optable?.fledge?.auctionconfigs ?? []).flatMap((cfg) => { const { impid, ...config } = cfg; - return { bidId: impid, config } + const asnc = ['auctionSignals', 'sellerSignals', 'perBuyerSignals', 'perBuyerTimeouts', 'deprecatedRenderURLReplacements', 'directFromSellerSignals'] + const config2 = deepClone(config); + Object.keys(config) + .filter(key => asnc.includes(key)) + .forEach(key => { + config[key] = ((val) => new Promise((resolve) => setTimeout(() => resolve(val), 2000)))(config[key]); + config2[key] = ((val) => new Promise((resolve, reject) => setTimeout(() => resolve({}), 2000)))(config[key]); + }); + return [{ bidId: impid, config }, {bidId: impid, config: config2}] }) return { bids, paapi: auctionConfigs } diff --git a/modules/paapi.js b/modules/paapi.js index 9ae2c870e5d..6233fe04171 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -3,7 +3,16 @@ */ import {config} from '../src/config.js'; import {getHook, hook, module} from '../src/hook.js'; -import {deepSetValue, logInfo, logWarn, mergeDeep, sizesToSizeTuples, deepAccess, deepEqual} from '../src/utils.js'; +import { + deepAccess, + deepEqual, + deepSetValue, + logError, + 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'; @@ -11,6 +20,9 @@ import {currencyCompare} from '../libraries/currencyUtils/currency.js'; import {keyCompare, maximum, minimum} from '../src/utils/reducers.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {auctionStore} from '../libraries/weakStore/weakStore.js'; +import {adapterMetrics} from '../src/adapters/bidderFactory.js'; +import {defer} from '../src/utils/promise.js'; +import {auctionManager} from '../src/auctionManager.js'; const MODULE = 'PAAPI'; @@ -27,10 +39,18 @@ export function registerSubmodule(submod) { module('paapi', registerSubmodule); -const pendingConfigsForAuction = auctionStore(); +/* auction configs as returned by getPAAPIConfigs */ const configsForAuction = auctionStore(); + +/* auction configs returned by adapters, but waiting for end-of-auction signals before they're added to configsForAuction */ +const pendingConfigsForAuction = auctionStore(); + +/* igb returned by adapters, waiting for end-of-auction signals before they're merged into configForAuctions */ const pendingBuyersForAuction = auctionStore(); +/* for auction configs that were generated in parallel with auctions (and contain promises), their resolve/reject methods */ +const deferredConfigsForAuction = auctionStore(); + let latestAuctionForAdUnit = {}; let moduleConfig = {}; @@ -58,7 +78,16 @@ getHook('makeBidRequests').before(addPaapiData); getHook('makeBidRequests').after(markForFledge); events.on(EVENTS.AUCTION_END, onAuctionEnd); -function getSlotSignals(adUnit = {}, bidsReceived = [], bidRequests = []) { +function getStaticSignals(adUnit = {}) { + const cfg = {}; + const requestedSize = getRequestedSize(adUnit); + if (requestedSize) { + cfg.requestedSize = requestedSize; + } + return cfg; +} + +function getSlotSignals(bidsReceived = [], bidRequests = []) { let bidfloor, bidfloorcur; if (bidsReceived.length > 0) { const bestBid = bidsReceived.reduce(maximum(currencyCompare(bid => [bid.cpm, bid.currency]))); @@ -75,10 +104,6 @@ function getSlotSignals(adUnit = {}, 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; } @@ -102,32 +127,68 @@ export function buyersToAuctionConfigs(igbRequests, merge = mergeBuyers, config function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adUnits}) { const adUnitsByCode = Object.fromEntries(adUnits?.map(au => [au.code, au]) || []); const allReqs = bidderRequests?.flatMap(br => br.bids); - const paapiConfigs = {}; + const newConfigs = {}; (adUnitCodes || []).forEach(au => { - paapiConfigs[au] = null; + newConfigs[au] = null; !latestAuctionForAdUnit.hasOwnProperty(au) && (latestAuctionForAdUnit[au] = null); }); const pendingConfigs = pendingConfigsForAuction(auctionId); const pendingBuyers = pendingBuyersForAuction(auctionId); if (pendingConfigs && pendingBuyers) { Object.entries(pendingBuyers).forEach(([adUnitCode, igbRequests]) => { - buyersToAuctionConfigs(igbRequests).forEach(auctionConfig => append(pendingConfigs, adUnitCode, auctionConfig)) + buyersToAuctionConfigs(igbRequests).forEach(auctionConfig => append(pendingConfigs, adUnitCode, {config: auctionConfig})) }) } + const deferredConfigs = deferredConfigsForAuction(auctionId); + const adUnitsWithConfigs = Array.from(new Set(Object.keys(pendingConfigs).concat(Object.keys(deferredConfigs)))); + const signals = Object.fromEntries( + adUnitsWithConfigs.map(adUnitCode => { + latestAuctionForAdUnit[adUnitCode] = auctionId; + const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode; + return [adUnitCode, { + ...getStaticSignals(adUnitsByCode[adUnitCode]), + ...getSlotSignals(bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit)) + }] + }) + ) + + const configsById = {}; Object.entries(pendingConfigs || {}).forEach(([adUnitCode, auctionConfigs]) => { - const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode; - const slotSignals = getSlotSignals(adUnitsByCode[adUnitCode], bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit)); - paapiConfigs[adUnitCode] = { - ...slotSignals, - componentAuctions: auctionConfigs.map(cfg => mergeDeep({}, slotSignals, cfg)) - }; - latestAuctionForAdUnit[adUnitCode] = auctionId; + auctionConfigs.forEach(({id, config}) => append(configsById, id, {adUnitCode, config: mergeDeep({}, signals[adUnitCode], config)})); + }); + + function resolveSignals(signals, deferrals) { + Object.entries(deferrals).forEach(([signal, {resolve, default: defaultValue}]) => { + resolve(Object.assign({}, defaultValue, signals.hasOwnProperty(signal) ? signals[signal] : {})) + }) + } + + Object.entries(deferredConfigs).forEach(([adUnitCode, {top, components}]) => { + resolveSignals(signals[adUnitCode], top); + Object.entries(components).forEach(([configId, deferrals]) => { + const matchingConfigs = configsById.hasOwnProperty(configId) ? configsById[configId] : []; + if (matchingConfigs.length > 1) { + logWarn(`Received multiple PAAPI configs for the same bidder and seller (${configId}), pending PAAPI auctions will only see the first`); + } + const {config} = matchingConfigs.shift() ?? {config: {...signals[adUnitCode]}} + resolveSignals(config, deferrals); + }) }); - configsForAuction(auctionId, paapiConfigs); + + Object.entries(pendingConfigs || {}).forEach(([adUnitCode, auctionConfigs]) => { + const configsById = {}; + auctionConfigs.forEach(({id, config}) => append(configsById, id, config)); + newConfigs[adUnitCode] = { + ...signals[adUnitCode], + componentAuctions: auctionConfigs.map(({config}) => mergeDeep({}, signals[adUnitCode], config)) + } + }); + + configsForAuction(auctionId, newConfigs); submodules.forEach(submod => submod.onAuctionConfig?.( auctionId, - paapiConfigs, - (adUnitCode) => paapiConfigs[adUnitCode] != null && USED.add(paapiConfigs[adUnitCode])) + newConfigs, + (adUnitCode) => newConfigs[adUnitCode] != null && USED.add(newConfigs[adUnitCode])) ); } @@ -142,9 +203,13 @@ function setFPD(target, {ortb2, ortb2Imp}) { return target; } +function getConfigId(bidderCode, seller) { + return `${bidderCode}::${seller}`; +} + export function addPaapiConfigHook(next, request, paapiConfig) { if (getFledgeConfig(config.getCurrentBidder()).enabled) { - const {adUnitCode, auctionId} = request; + const {adUnitCode, auctionId, bidder} = request; // eslint-disable-next-line no-inner-declarations function storePendingData(store, data) { @@ -163,7 +228,7 @@ export function addPaapiConfigHook(next, request, paapiConfig) { (config.interestGroupBuyers || []).forEach(buyer => { pbs[buyer] = setFPD(pbs[buyer] ?? {}, request); }) - storePendingData(pendingConfigsForAuction, config); + storePendingData(pendingConfigsForAuction, {id: getConfigId(bidder, config.seller), config}); } if (igb && checkOrigin(igb)) { igb.pbs = setFPD(igb.pbs || {}, request); @@ -382,6 +447,85 @@ export function markForFledge(next, bidderRequests) { next(bidderRequests); } +export const ASYNC_SIGNALS = ['auctionSignals', 'sellerSignals', 'perBuyerSignals', 'perBuyerTimeouts', 'deprecatedRenderURLReplacements', 'directFromSellerSignals']; +const REQUIRED_SYNC_SIGNALS = [['seller'], ['decisionLogicURL', 'decisionLogicUrl']]; + +export function parallelPaapiProcessing(next, spec, bids, bidderRequest, ...args) { + function makeDeferrals(defaults = {}) { + let promises = {}; + const deferrals = Object.fromEntries(ASYNC_SIGNALS.map(signal => { + const def = defer({promiseFactory: (resolver) => new Promise(resolver)}); + def.default = defaults.hasOwnProperty(signal) ? defaults[signal] : {}; + promises[signal] = def.promise; + return [signal, def] + })) + return [deferrals, promises]; + } + + const {auctionId, paapi: {enabled} = {}} = bidderRequest; + const auctionConfigs = configsForAuction(auctionId); + bids.map(bid => bid.adUnitCode).forEach(adUnitCode => { + latestAuctionForAdUnit[adUnitCode] = auctionId; + auctionConfigs[adUnitCode] = null; + }); + + if (enabled && spec.buildPAAPIConfigs) { + const metrics = adapterMetrics(bidderRequest); + let partialConfigs; + metrics.measureTime('buildPAAPIConfigs', () => { + try { + partialConfigs = spec.buildPAAPIConfigs(bids, bidderRequest) + } catch (e) { + logError(`Error invoking "buildPAAPIConfigs":`, e); + } + }); + const requestsById = Object.fromEntries(bids.map(bid => [bid.bidId, bid])); + (partialConfigs ?? []).forEach(({bidId, config}) => { + const bidRequest = requestsById.hasOwnProperty(bidId) && requestsById[bidId]; + if (!bidRequest) { + logWarn(`Received partial PAAPI config for unknown bidId, config will be ignored`, {bidId, config}); + } else { + const missing = REQUIRED_SYNC_SIGNALS.find(signals => signals.every(signal => !config.hasOwnProperty(signal) || !config[signal] || typeof config[signal] !== 'string')); + if (missing) { + logError(`Partial PAAPI config is missing required property "${missing[0]}"`, partialConfigs) + } else { + const adUnitCode = bidRequest.adUnitCode; + latestAuctionForAdUnit[adUnitCode] = auctionId; + const deferredConfigs = deferredConfigsForAuction(auctionId); + if (!deferredConfigs.hasOwnProperty(adUnitCode)) { + const [deferrals, promises] = makeDeferrals(); + deferredConfigs[adUnitCode] = { + top: deferrals, + components: {} + } + auctionConfigs[adUnitCode] = { + ...getStaticSignals(auctionManager.index.getAdUnit(bidRequest)), + ...promises, + componentAuctions: [] + } + } + const configId = getConfigId(bidRequest.bidder, config.seller); + if (deferredConfigs[adUnitCode].components.hasOwnProperty(configId)) { + logWarn(`Received multiple PAAPI configs for the same bidder and seller; config will be ignored`, { + config, + bidder: bidRequest.bidder + }) + } else { + const [deferrals, promises] = makeDeferrals(config); + deferredConfigs[adUnitCode].components[configId] = deferrals; + auctionConfigs[adUnitCode].componentAuctions.push({ + ...getStaticSignals(bidRequest), + ...config, + ...promises + }) + } + } + } + }) + } + return next.call(this, spec, bids, bidderRequest, ...args); +} + export function setImpExtAe(imp, bidRequest, context) { if (!context.bidderRequest.paapi?.enabled) { delete imp.ext?.ae; diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index ac26883ae99..bb2f137558c 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -609,6 +609,6 @@ export function isValid(adUnitCode, bid, {index = auctionManager.index} = {}) { return true; } -function adapterMetrics(bidderRequest) { +export function adapterMetrics(bidderRequest) { return useMetrics(bidderRequest.metrics).renameWith(n => [`adapter.client.${n}`, `adapters.client.${bidderRequest.bidderCode}.${n}`]) } diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js index cc839307c8e..c2553cf7190 100644 --- a/test/spec/modules/paapi_spec.js +++ b/test/spec/modules/paapi_spec.js @@ -7,12 +7,12 @@ import {hook} from '../../../src/hook.js'; import 'modules/appnexusBidAdapter.js'; import 'modules/rubiconBidAdapter.js'; import { - addPaapiConfigHook, addPaapiData, + addPaapiConfigHook, addPaapiData, ASYNC_SIGNALS, buyersToAuctionConfigs, getPAAPIConfig, getPAAPISize, IGB_TO_CONFIG, - mergeBuyers, + mergeBuyers, parallelPaapiProcessing, parseExtIgi, parseExtPrebidFledge, partitionBuyers, @@ -1130,6 +1130,238 @@ describe('paapi module', () => { }); }); + describe('parallel PAAPI auctions', () => { + describe('parallellPaapiProcessing', () => { + let next, spec, bids, bidderRequest, restOfTheArgs, mockConfig, mockAuction; + beforeEach(() => { + next = sinon.stub(); + spec = { + code: 'mockBidder', + }; + bids = [{ + bidder: 'mockBidder', + bidId: 'bidId', + adUnitCode: 'au', + auctionId: 'aid', + mediaTypes: { + banner: { + sizes: [[123, 321]] + } + } + }]; + bidderRequest = {auctionId: 'aid', bidderCode: 'mockBidder', paapi: {enabled: true}, bids}; + restOfTheArgs = [{more: 'args'}]; + mockConfig = { + seller: 'mock.seller', + decisionLogicURL: 'mock.seller/decisionLogic' + } + mockAuction = {}; + sandbox.stub(auctionManager.index, 'getAuction').callsFake(() => mockAuction); + sandbox.stub(auctionManager.index, 'getAdUnit').callsFake((req) => bids.find(bid => bid.adUnitCode === req.adUnitCode)) + config.setConfig({paapi: {enabled: true}}); + }); + + afterEach(() => { + sinon.assert.calledWith(next, spec, bids, bidderRequest, ...restOfTheArgs); + config.resetConfig(); + }); + + function startParallel() { + return parallelPaapiProcessing(next, spec, bids, bidderRequest, ...restOfTheArgs); + } + + describe('should have no effect when', () => { + afterEach(() => { + expect(getPAAPIConfig({}, true)).to.eql({au: null}); + }) + it('spec has no buildPAAPIConfigs', () => { + startParallel(); + }); + Object.entries({ + 'returns no configs': () => { spec.buildPAAPIConfigs = sinon.stub().callsFake(() => []); }, + 'throws': () => { spec.buildPAAPIConfigs = sinon.stub().callsFake(() => { throw new Error() }) }, + 'returns too little config': () => { spec.buildPAAPIConfigs = sinon.stub().callsFake(() => [ {bidId: 'bidId', config: {seller: 'mock.seller'}} ]) }, + 'bidder is not paapi enabled': () => { + bidderRequest.paapi.enabled = false; + spec.buildPAAPIConfigs = sinon.stub().callsFake(() => [{config: mockConfig, bidId: 'bidId'}]) + }, + 'paapi module is not enabled': () => { + delete bidderRequest.paapi; + spec.buildPAAPIConfigs = sinon.stub().callsFake(() => [{config: mockConfig, bidId: 'bidId'}]) + }, + 'bidId points to missing bid': () => { spec.buildPAAPIConfigs = sinon.stub().callsFake(() => [{config: mockConfig, bidId: 'missing'}]) } + }).forEach(([t, setup]) => { + it(`buildPAAPIConfigs ${t}`, () => { + setup(); + startParallel(); + }); + }); + }); + + describe('when buildPAAPIConfigs returns valid config', () => { + let builtCfg; + beforeEach(() => { + builtCfg = [{bidId: 'bidId', config: mockConfig}]; + spec.buildPAAPIConfigs = sinon.stub().callsFake(() => builtCfg); + }); + + afterEach(() => { + sinon.assert.calledWith(spec.buildPAAPIConfigs, bids, bidderRequest); + }) + + it('should make async config available from getPAAPIConfig', () => { + startParallel(); + const actual = getPAAPIConfig(); + const promises = Object.fromEntries(ASYNC_SIGNALS.map(signal => [signal, sinon.match((arg) => arg instanceof Promise)])) + sinon.assert.match(actual, { + au: sinon.match({ + ...promises, + requestedSize: { + width: 123, + height: 321 + }, + componentAuctions: [ + sinon.match({ + ...mockConfig, + ...promises, + requestedSize: { + width: 123, + height: 321 + } + }) + ] + }) + }); + }); + + it('should respect requestedSize from adapter', () => { + mockConfig.requestedSize = {width: 1, height: 2}; + startParallel(); + sinon.assert.match(getPAAPIConfig().au, { + requestedSize: { + width: 123, + height: 321 + }, + componentAuctions: [sinon.match({ + requestedSize: { + width: 1, + height: 2 + } + })] + }) + }) + + it('should not accept multiple partial configs for the same bid/seller', () => { + builtCfg.push(builtCfg[0]) + startParallel(); + expect(getPAAPIConfig().au.componentAuctions.length).to.eql(1); + }); + + describe('on auction end', () => { + let bidsReceived, bidderRequests, adUnitCodes, adUnits; + beforeEach(() => { + bidsReceived = [{adUnitCode: 'au', cpm: 1}]; + adUnits = [{code: 'au'}] + adUnitCodes = ['au']; + bidderRequests = [bidderRequest]; + }); + + function endAuction() { + events.emit(EVENTS.AUCTION_END, {auctionId: 'aid', bidsReceived, bidderRequests, adUnitCodes, adUnits}) + } + + function resolveConfig(auctionConfig) { + return Promise.all( + Object.entries(auctionConfig) + .map(([key, value]) => Promise.resolve(value).then(value => [key, value])) + ).then(result => Object.fromEntries(result)) + } + + it('should resolve top level config with auction signals', async () => { + startParallel(); + let config = getPAAPIConfig().au; + endAuction(); + config = await resolveConfig(config); + sinon.assert.match(config, { + auctionSignals: { + prebid: {bidfloor: 1} + } + }) + }); + + describe('when adapter returns the rest of auction config', () => { + let configRemainder; + beforeEach(() => { + configRemainder = { + ...Object.fromEntries(ASYNC_SIGNALS.map(signal => [signal, {type: signal}])), + seller: 'mock.seller' + }; + }) + function returnRemainder() { + addPaapiConfigHook(sinon.stub(), bids[0], {config: configRemainder}); + } + it('should resolve component configs with values returned by adapters', async () => { + startParallel(); + let config = getPAAPIConfig().au.componentAuctions[0]; + returnRemainder(); + endAuction(); + config = await resolveConfig(config); + sinon.assert.match(config, configRemainder); + }); + + it('should pick first config that matches bidId/seller', async () => { + startParallel(); + let config = getPAAPIConfig().au.componentAuctions[0]; + returnRemainder(); + const expectedSignals = {...configRemainder}; + configRemainder = { + ...configRemainder, + auctionSignals: { + this: 'should be ignored' + } + } + returnRemainder(); + endAuction(); + config = await resolveConfig(config); + sinon.assert.match(config, expectedSignals); + }); + + describe('should default to values returned from buildPAAPIConfigs when interpretResponse', () => { + beforeEach(() => { + ASYNC_SIGNALS.forEach(signal => mockConfig[signal] = {default: signal}) + }); + Object.entries({ + 'returns no matching config'() { + }, + 'does not include values in response'() { + configRemainder = {}; + returnRemainder(); + } + }).forEach(([t, postResponse]) => { + it(t, async () => { + startParallel(); + let config = getPAAPIConfig().au.componentAuctions[0]; + postResponse(); + endAuction(); + config = await resolveConfig(config); + sinon.assert.match(config, mockConfig); + }); + }); + }); + it('should make extra configs available', () => { + startParallel(); + expect(true).to.be.false; + }); + it('should pass only new configs to submodules\' onAuctionConfig', () => { + startParallel(); + expect(true).to.be.false; + }) + }); + }); + }); + }); + }); + describe('ortb processors for fledge', () => { it('imp.ext.ae should be removed if fledge is not enabled', () => { const imp = {ext: {ae: 1, igs: {}}}; From bfa1b581f9000849750949eb7f48089e2a3d564f Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 3 Sep 2024 10:22:00 -0700 Subject: [PATCH 02/14] Parallel auction configs --- modules/paapi.js | 135 ++++++++++++++++++++------------ test/spec/modules/paapi_spec.js | 82 +++++++++++++------ 2 files changed, 145 insertions(+), 72 deletions(-) diff --git a/modules/paapi.js b/modules/paapi.js index 6233fe04171..660e4551b03 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -127,19 +127,25 @@ export function buyersToAuctionConfigs(igbRequests, merge = mergeBuyers, config function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adUnits}) { const adUnitsByCode = Object.fromEntries(adUnits?.map(au => [au.code, au]) || []); const allReqs = bidderRequests?.flatMap(br => br.bids); - const newConfigs = {}; + const paapiConfigs = configsForAuction(auctionId); (adUnitCodes || []).forEach(au => { - newConfigs[au] = null; + if (!paapiConfigs.hasOwnProperty(au)) { + paapiConfigs[au] = null; + } !latestAuctionForAdUnit.hasOwnProperty(au) && (latestAuctionForAdUnit[au] = null); }); + const pendingConfigs = pendingConfigsForAuction(auctionId); const pendingBuyers = pendingBuyersForAuction(auctionId); + if (pendingConfigs && pendingBuyers) { Object.entries(pendingBuyers).forEach(([adUnitCode, igbRequests]) => { buyersToAuctionConfigs(igbRequests).forEach(auctionConfig => append(pendingConfigs, adUnitCode, {config: auctionConfig})) }) } + const deferredConfigs = deferredConfigsForAuction(auctionId); + const adUnitsWithConfigs = Array.from(new Set(Object.keys(pendingConfigs).concat(Object.keys(deferredConfigs)))); const signals = Object.fromEntries( adUnitsWithConfigs.map(adUnitCode => { @@ -154,7 +160,10 @@ function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adU const configsById = {}; Object.entries(pendingConfigs || {}).forEach(([adUnitCode, auctionConfigs]) => { - auctionConfigs.forEach(({id, config}) => append(configsById, id, {adUnitCode, config: mergeDeep({}, signals[adUnitCode], config)})); + auctionConfigs.forEach(({id, config}) => append(configsById, id, { + adUnitCode, + config: mergeDeep({}, signals[adUnitCode], config) + })); }); function resolveSignals(signals, deferrals) { @@ -168,28 +177,33 @@ function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adU Object.entries(components).forEach(([configId, deferrals]) => { const matchingConfigs = configsById.hasOwnProperty(configId) ? configsById[configId] : []; if (matchingConfigs.length > 1) { - logWarn(`Received multiple PAAPI configs for the same bidder and seller (${configId}), pending PAAPI auctions will only see the first`); + logWarn(`Received multiple PAAPI configs for the same bidder and seller (${configId}), active PAAPI auctions will only see the first`); } const {config} = matchingConfigs.shift() ?? {config: {...signals[adUnitCode]}} resolveSignals(config, deferrals); }) }); - Object.entries(pendingConfigs || {}).forEach(([adUnitCode, auctionConfigs]) => { - const configsById = {}; - auctionConfigs.forEach(({id, config}) => append(configsById, id, config)); - newConfigs[adUnitCode] = { - ...signals[adUnitCode], - componentAuctions: auctionConfigs.map(({config}) => mergeDeep({}, signals[adUnitCode], config)) + const newConfigs = Object.values(configsById).flatMap(configs => configs); + const hasDeferredConfigs = Object.keys(deferredConfigs).length > 0; + + if (moduleConfig.parallel && hasDeferredConfigs && newConfigs.length > 0) { + logWarn(`Received PAAPI configs after PAAPI auctions were already started in parallel with their contextual auction`, newConfigs) + } + + newConfigs.forEach(({adUnitCode, config}) => { + if (paapiConfigs[adUnitCode] == null) { + paapiConfigs[adUnitCode] = { + ...signals[adUnitCode], + componentAuctions: [] + } } + paapiConfigs[adUnitCode].componentAuctions.push(mergeDeep({}, signals[adUnitCode], config)); }); - configsForAuction(auctionId, newConfigs); - submodules.forEach(submod => submod.onAuctionConfig?.( - auctionId, - newConfigs, - (adUnitCode) => newConfigs[adUnitCode] != null && USED.add(newConfigs[adUnitCode])) - ); + if (!moduleConfig.parallel || !hasDeferredConfigs) { + submodules.forEach(submod => submod.onAuctionConfig?.(auctionId, paapiConfigs)); + } } function append(target, key, value) { @@ -448,7 +462,33 @@ export function markForFledge(next, bidderRequests) { } export const ASYNC_SIGNALS = ['auctionSignals', 'sellerSignals', 'perBuyerSignals', 'perBuyerTimeouts', 'deprecatedRenderURLReplacements', 'directFromSellerSignals']; -const REQUIRED_SYNC_SIGNALS = [['seller'], ['decisionLogicURL', 'decisionLogicUrl']]; + + +const validatePartialConfig = (() => { + const REQUIRED_SYNC_SIGNALS = [ + { + props: ['seller'], + validate: (val) => typeof val === 'string' + }, + { + props: ['interestGroupBuyers'], + validate: (val) => Array.isArray(val) && val.length > 0 + }, + { + props: ['decisionLogicURL', 'decisionLogicUrl'], + validate: (val) => typeof val === 'string' + } + ]; + + return function (config) { + const invalid = REQUIRED_SYNC_SIGNALS.find(({props, validate}) => props.every(prop => !config.hasOwnProperty(prop) || !config[prop] || !validate(config[prop]))); + if (invalid) { + logError(`Partial PAAPI config has missing or invalid property "${invalid.props[0]}"`, config) + return false; + } + return true; + } +})() export function parallelPaapiProcessing(next, spec, bids, bidderRequest, ...args) { function makeDeferrals(defaults = {}) { @@ -484,42 +524,37 @@ export function parallelPaapiProcessing(next, spec, bids, bidderRequest, ...args const bidRequest = requestsById.hasOwnProperty(bidId) && requestsById[bidId]; if (!bidRequest) { logWarn(`Received partial PAAPI config for unknown bidId, config will be ignored`, {bidId, config}); - } else { - const missing = REQUIRED_SYNC_SIGNALS.find(signals => signals.every(signal => !config.hasOwnProperty(signal) || !config[signal] || typeof config[signal] !== 'string')); - if (missing) { - logError(`Partial PAAPI config is missing required property "${missing[0]}"`, partialConfigs) - } else { - const adUnitCode = bidRequest.adUnitCode; - latestAuctionForAdUnit[adUnitCode] = auctionId; - const deferredConfigs = deferredConfigsForAuction(auctionId); - if (!deferredConfigs.hasOwnProperty(adUnitCode)) { - const [deferrals, promises] = makeDeferrals(); - deferredConfigs[adUnitCode] = { - top: deferrals, - components: {} - } - auctionConfigs[adUnitCode] = { - ...getStaticSignals(auctionManager.index.getAdUnit(bidRequest)), - ...promises, - componentAuctions: [] - } + } else if (validatePartialConfig(config)) { + const adUnitCode = bidRequest.adUnitCode; + latestAuctionForAdUnit[adUnitCode] = auctionId; + const deferredConfigs = deferredConfigsForAuction(auctionId); + if (!deferredConfigs.hasOwnProperty(adUnitCode)) { + const [deferrals, promises] = makeDeferrals(); + deferredConfigs[adUnitCode] = { + top: deferrals, + components: {} } - const configId = getConfigId(bidRequest.bidder, config.seller); - if (deferredConfigs[adUnitCode].components.hasOwnProperty(configId)) { - logWarn(`Received multiple PAAPI configs for the same bidder and seller; config will be ignored`, { - config, - bidder: bidRequest.bidder - }) - } else { - const [deferrals, promises] = makeDeferrals(config); - deferredConfigs[adUnitCode].components[configId] = deferrals; - auctionConfigs[adUnitCode].componentAuctions.push({ - ...getStaticSignals(bidRequest), - ...config, - ...promises - }) + auctionConfigs[adUnitCode] = { + ...getStaticSignals(auctionManager.index.getAdUnit(bidRequest)), + ...promises, + componentAuctions: [] } } + const configId = getConfigId(bidRequest.bidder, config.seller); + if (deferredConfigs[adUnitCode].components.hasOwnProperty(configId)) { + logWarn(`Received multiple PAAPI configs for the same bidder and seller; config will be ignored`, { + config, + bidder: bidRequest.bidder + }) + } else { + const [deferrals, promises] = makeDeferrals(config); + deferredConfigs[adUnitCode].components[configId] = deferrals; + auctionConfigs[adUnitCode].componentAuctions.push({ + ...getStaticSignals(bidRequest), + ...config, + ...promises + }) + } } }) } diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js index c2553cf7190..90630365b77 100644 --- a/test/spec/modules/paapi_spec.js +++ b/test/spec/modules/paapi_spec.js @@ -7,12 +7,15 @@ import {hook} from '../../../src/hook.js'; import 'modules/appnexusBidAdapter.js'; import 'modules/rubiconBidAdapter.js'; import { - addPaapiConfigHook, addPaapiData, ASYNC_SIGNALS, + addPaapiConfigHook, + addPaapiData, + ASYNC_SIGNALS, buyersToAuctionConfigs, getPAAPIConfig, getPAAPISize, IGB_TO_CONFIG, - mergeBuyers, parallelPaapiProcessing, + mergeBuyers, + parallelPaapiProcessing, parseExtIgi, parseExtPrebidFledge, partitionBuyers, @@ -225,7 +228,6 @@ describe('paapi module', () => { it('should drop auction configs after end of auction', () => { events.emit(EVENTS.AUCTION_END, {auctionId}); addPaapiConfigHook(nextFnSpy, {auctionId, adUnitCode: 'au'}, paapiConfig); - events.emit(EVENTS.AUCTION_END, {auctionId}); expect(getPAAPIConfig({auctionId})).to.eql({}); }); @@ -315,19 +317,6 @@ describe('paapi module', () => { }); }); }); - it('removes configs from getPAAPIConfig if the module calls markAsUsed', () => { - submods[0].onAuctionConfig.callsFake((auctionId, configs, markAsUsed) => { - markAsUsed('au1'); - }); - addPaapiConfigHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, paapiConfig); - events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au1']}); - expect(getPAAPIConfig()).to.eql({}); - }); - it('keeps them available if they do not', () => { - addPaapiConfigHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, paapiConfig); - events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au1']}); - expect(getPAAPIConfig()).to.not.be.empty; - }); }); }); @@ -1153,7 +1142,8 @@ describe('paapi module', () => { restOfTheArgs = [{more: 'args'}]; mockConfig = { seller: 'mock.seller', - decisionLogicURL: 'mock.seller/decisionLogic' + decisionLogicURL: 'mock.seller/decisionLogic', + interestGroupBuyers: ['mock.buyer'] } mockAuction = {}; sandbox.stub(auctionManager.index, 'getAuction').callsFake(() => mockAuction); @@ -1348,13 +1338,61 @@ describe('paapi module', () => { }); }); }); - it('should make extra configs available', () => { + + it('should make extra configs available', async () => { startParallel(); - expect(true).to.be.false; + returnRemainder(); + configRemainder = {...configRemainder, seller: 'other.seller'}; + returnRemainder(); + endAuction(); + let configs = getPAAPIConfig().au.componentAuctions; + configs = [await resolveConfig(configs[0]), configs[1]]; + expect(configs.map(cfg => cfg.seller)).to.eql(['mock.seller', 'other.seller']); }); - it('should pass only new configs to submodules\' onAuctionConfig', () => { - startParallel(); - expect(true).to.be.false; + + describe('submodule\'s onAuctionConfig', () => { + let onAuctionConfig; + beforeEach(() => { + onAuctionConfig = sinon.stub(); + registerSubmodule({onAuctionConfig}) + }); + + Object.entries({ + 'parallel=true, some configs deferred': { + setup() { + config.mergeConfig({paapi: {parallel: true}}) + }, + shouldInvoke: false, + }, + 'parallel=true, no deferred configs': { + setup() { + config.mergeConfig({paapi: {parallel: true}}); + spec.buildPAAPIConfigs = sinon.stub().callsFake(() => []); + }, + shouldInvoke: true + }, + 'parallel=false, some configs deferred': { + setup() { + config.mergeConfig({paapi: {parallel: false}}) + }, + shouldInvoke: true + } + }).forEach(([t, {setup, shouldInvoke}]) => { + describe(`when ${t}`, () => { + beforeEach(setup); + + it(`should ${shouldInvoke ? '' : 'NOT '} invoke onAuctionConfig`, () => { + startParallel(); + returnRemainder(); + endAuction(); + if (shouldInvoke) { + sinon.assert.calledWith(onAuctionConfig, 'aid', sinon.match(arg => arg.au.componentAuctions[0].seller === 'mock.seller')); + } else { + sinon.assert.notCalled(onAuctionConfig); + } + }) + }) + }) }) }); }); From 32b9ef7b66ceb1e694d008a4b662f90f321cd186 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 3 Sep 2024 12:03:19 -0700 Subject: [PATCH 03/14] revert optable changes --- modules/optableBidAdapter.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/modules/optableBidAdapter.js b/modules/optableBidAdapter.js index 0ff1e7bf220..4e639fb88ee 100644 --- a/modules/optableBidAdapter.js +++ b/modules/optableBidAdapter.js @@ -3,7 +3,6 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js' -import {deepClone} from "../src/utils.js"; const converter = ortbConverter({ context: { netRevenue: true, ttl: 300 }, imp(buildImp, bidRequest, context) { @@ -31,17 +30,9 @@ export const spec = { }, interpretResponse: function(response, request) { const bids = converter.fromORTB({ response: response.body, request: request.data }).bids - const auctionConfigs = (response.body.ext?.optable?.fledge?.auctionconfigs ?? []).flatMap((cfg) => { + const auctionConfigs = (response.body.ext?.optable?.fledge?.auctionconfigs ?? []).map((cfg) => { const { impid, ...config } = cfg; - const asnc = ['auctionSignals', 'sellerSignals', 'perBuyerSignals', 'perBuyerTimeouts', 'deprecatedRenderURLReplacements', 'directFromSellerSignals'] - const config2 = deepClone(config); - Object.keys(config) - .filter(key => asnc.includes(key)) - .forEach(key => { - config[key] = ((val) => new Promise((resolve) => setTimeout(() => resolve(val), 2000)))(config[key]); - config2[key] = ((val) => new Promise((resolve, reject) => setTimeout(() => resolve({}), 2000)))(config[key]); - }); - return [{ bidId: impid, config }, {bidId: impid, config: config2}] + return { bidId: impid, config } }) return { bids, paapi: auctionConfigs } From 3aec7ca58a3d8a6248499c9c949264f81b5bb367 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 3 Sep 2024 12:03:40 -0700 Subject: [PATCH 04/14] trigger onAuctionConfigs depending on parallel settings --- modules/paapi.js | 38 +++++++++++++++++++++++++++----- src/auction.js | 5 ++++- test/spec/auctionmanager_spec.js | 7 ++++++ test/spec/modules/paapi_spec.js | 35 +++++++++++++++++++---------- 4 files changed, 67 insertions(+), 18 deletions(-) diff --git a/modules/paapi.js b/modules/paapi.js index 660e4551b03..eff74574733 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -65,18 +65,37 @@ export function reset() { export function init(cfg) { if (cfg && cfg.enabled === true) { + if (!moduleConfig.enabled) { + attachHandlers(); + } moduleConfig = cfg; logInfo(`${MODULE} enabled (browser ${isFledgeSupported() ? 'supports' : 'does NOT support'} runAdAuction)`, cfg); } else { + if (moduleConfig.enabled) { + detachHandlers(); + } moduleConfig = {}; logInfo(`${MODULE} disabled`, cfg); } } -getHook('addPaapiConfig').before(addPaapiConfigHook); -getHook('makeBidRequests').before(addPaapiData); -getHook('makeBidRequests').after(markForFledge); -events.on(EVENTS.AUCTION_END, onAuctionEnd); +function attachHandlers() { + getHook('addPaapiConfig').before(addPaapiConfigHook); + getHook('makeBidRequests').before(addPaapiData); + getHook('makeBidRequests').after(markForFledge); +//getHook('processBidderRequests').before(parallelPaapiProcessing); +//events.on(EVENTS.AUCTION_INIT, onAuctionInit); + events.on(EVENTS.AUCTION_END, onAuctionEnd); +} + +function detachHandlers() { + getHook('addPaapiConfig').getHooks({hook: addPaapiConfigHook}).remove(); + getHook('makeBidRequests').getHooks({hook: addPaapiData}).remove(); + getHook('makeBidRequests').getHooks({hook: markForFledge}).remove(); + //getHook('processBidderRequests').before({hook: parallelPaapiProcessing}).remove(); + events.off(EVENTS.AUCTION_END, onAuctionEnd); + //events.off(EVENTS.AUCTION_INIT, onAuctionInit); +} function getStaticSignals(adUnit = {}) { const cfg = {}; @@ -463,7 +482,6 @@ export function markForFledge(next, bidderRequests) { export const ASYNC_SIGNALS = ['auctionSignals', 'sellerSignals', 'perBuyerSignals', 'perBuyerTimeouts', 'deprecatedRenderURLReplacements', 'directFromSellerSignals']; - const validatePartialConfig = (() => { const REQUIRED_SYNC_SIGNALS = [ { @@ -561,6 +579,16 @@ export function parallelPaapiProcessing(next, spec, bids, bidderRequest, ...args return next.call(this, spec, bids, bidderRequest, ...args); } +export function onAuctionInit({auctionId}) { + if (moduleConfig.parallel) { + auctionManager.index.getAuction({auctionId}).requestsDone.then(() => { + if (Object.keys(deferredConfigsForAuction(auctionId)).length > 0) { + submodules.forEach(submod => submod.onAuctionConfig?.(auctionId, configsForAuction(auctionId))); + } + }) + } +} + export function setImpExtAe(imp, bidRequest, context) { if (!context.bidderRequest.paapi?.enabled) { delete imp.ext?.ae; diff --git a/src/auction.js b/src/auction.js index b422ffa7333..7e0bc08ad93 100644 --- a/src/auction.js +++ b/src/auction.js @@ -150,6 +150,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a const _timeout = cbTimeout; const _timelyRequests = new Set(); const done = defer(); + const requestsDone = defer(); let _bidsRejected = []; let _callback = callback; let _bidderRequests = []; @@ -320,6 +321,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a } } }, _timeout, onTimelyResponse, ortb2Fragments); + requestsDone.resolve(); } }; @@ -408,7 +410,8 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a getNonBids: () => _nonBids, getFPD: () => ortb2Fragments, getMetrics: () => metrics, - end: done.promise + end: done.promise, + requestsDone: requestsDone.promise }; } diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index ab00ac86d98..9b30044e1b3 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -841,6 +841,13 @@ describe('auctionmanager.js', function () { expect(auction.getNonBids()[0]).to.equal('test'); }); + it('resolves .requestsDone', async () => { + const auction = auctionManager.createAuction({adUnits}); + stubCallAdapters.reset(); + auction.callBids(); + await auction.requestsDone; + }) + describe('stale auctions', () => { let clock, auction; beforeEach(() => { diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js index 90630365b77..28617602989 100644 --- a/test/spec/modules/paapi_spec.js +++ b/test/spec/modules/paapi_spec.js @@ -14,7 +14,7 @@ import { getPAAPIConfig, getPAAPISize, IGB_TO_CONFIG, - mergeBuyers, + mergeBuyers, onAuctionInit, parallelPaapiProcessing, parseExtIgi, parseExtPrebidFledge, @@ -1157,7 +1157,8 @@ describe('paapi module', () => { }); function startParallel() { - return parallelPaapiProcessing(next, spec, bids, bidderRequest, ...restOfTheArgs); + parallelPaapiProcessing(next, spec, bids, bidderRequest, ...restOfTheArgs); + onAuctionInit({auctionId: 'aid'}) } describe('should have no effect when', () => { @@ -1362,34 +1363,44 @@ describe('paapi module', () => { setup() { config.mergeConfig({paapi: {parallel: true}}) }, - shouldInvoke: false, + delayed: false, }, 'parallel=true, no deferred configs': { setup() { config.mergeConfig({paapi: {parallel: true}}); spec.buildPAAPIConfigs = sinon.stub().callsFake(() => []); }, - shouldInvoke: true + delayed: true }, 'parallel=false, some configs deferred': { setup() { config.mergeConfig({paapi: {parallel: false}}) }, - shouldInvoke: true + delayed: true } - }).forEach(([t, {setup, shouldInvoke}]) => { + }).forEach(([t, {setup, delayed}]) => { describe(`when ${t}`, () => { - beforeEach(setup); + beforeEach(() => { + mockAuction.requestsDone = Promise.resolve(); + setup(); + }); - it(`should ${shouldInvoke ? '' : 'NOT '} invoke onAuctionConfig`, () => { - startParallel(); - returnRemainder(); - endAuction(); - if (shouldInvoke) { + function expectInvoked(shouldBeInvoked) { + if (shouldBeInvoked) { sinon.assert.calledWith(onAuctionConfig, 'aid', sinon.match(arg => arg.au.componentAuctions[0].seller === 'mock.seller')); } else { sinon.assert.notCalled(onAuctionConfig); } + } + + it(`should invoke onAuctionConfig when ${delayed ? 'auction ends' : 'auction requests have started'}`, async () => { + startParallel(); + await mockAuction.requestsDone; + expectInvoked(!delayed); + onAuctionConfig.reset(); + returnRemainder(); + endAuction(); + expectInvoked(delayed); }) }) }) From 1a34b50a49bff56b19e93481b87b3d8d38e0df85 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 3 Sep 2024 12:12:05 -0700 Subject: [PATCH 05/14] attach parallel paapi processing handlers --- modules/paapi.js | 8 ++++---- src/adapters/bidderFactory.js | 12 ++++++------ test/spec/modules/priceFloors_spec.js | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/modules/paapi.js b/modules/paapi.js index eff74574733..40e5083775b 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -83,8 +83,8 @@ function attachHandlers() { getHook('addPaapiConfig').before(addPaapiConfigHook); getHook('makeBidRequests').before(addPaapiData); getHook('makeBidRequests').after(markForFledge); -//getHook('processBidderRequests').before(parallelPaapiProcessing); -//events.on(EVENTS.AUCTION_INIT, onAuctionInit); + getHook('processBidderRequests').before(parallelPaapiProcessing); + events.on(EVENTS.AUCTION_INIT, onAuctionInit); events.on(EVENTS.AUCTION_END, onAuctionEnd); } @@ -92,9 +92,9 @@ function detachHandlers() { getHook('addPaapiConfig').getHooks({hook: addPaapiConfigHook}).remove(); getHook('makeBidRequests').getHooks({hook: addPaapiData}).remove(); getHook('makeBidRequests').getHooks({hook: markForFledge}).remove(); - //getHook('processBidderRequests').before({hook: parallelPaapiProcessing}).remove(); + getHook('processBidderRequests').getHooks({hook: parallelPaapiProcessing}).remove(); + events.off(EVENTS.AUCTION_INIT, onAuctionInit); events.off(EVENTS.AUCTION_END, onAuctionEnd); - //events.off(EVENTS.AUCTION_INIT, onAuctionInit); } function getStaticSignals(adUnit = {}) { diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index bb2f137558c..8ab34a04d8d 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -190,7 +190,7 @@ export function registerBidder(spec) { } } -export function guardTids(bidderCode) { +export const guardTids = memoize(({bidderCode}) => { if (isActivityAllowed(ACTIVITY_TRANSMIT_TID, activityParams(MODULE_TYPE_BIDDER, bidderCode))) { return { bidRequest: (br) => br, @@ -228,7 +228,7 @@ export function guardTids(bidderCode) { } }) } -} +}); /** * Make a new bidder from the given spec. This is exported mainly for testing. @@ -246,7 +246,7 @@ export function newBidder(spec) { if (!Array.isArray(bidderRequest.bids)) { return; } - const tidGuard = guardTids(bidderRequest.bidderCode); + const tidGuard = guardTids(bidderRequest); const adUnitCodesHandled = {}; function addBidWithCode(adUnitCode, bid) { @@ -287,7 +287,7 @@ export function newBidder(spec) { } }); - processBidderRequests(spec, validBidRequests.map(tidGuard.bidRequest), tidGuard.bidderRequest(bidderRequest), ajax, configEnabledCallback, { + processBidderRequests(spec, validBidRequests, bidderRequest, ajax, configEnabledCallback, { onRequest: requestObject => events.emit(EVENTS.BEFORE_BIDDER_HTTP, bidderRequest, requestObject), onResponse: (resp) => { onTimelyResponse(spec.code); @@ -381,8 +381,8 @@ const RESPONSE_PROPS = ['bids', 'paapi'] export const processBidderRequests = hook('sync', function (spec, bids, bidderRequest, ajax, wrapCallback, {onRequest, onResponse, onPaapi, onError, onBid, onCompletion}) { const metrics = adapterMetrics(bidderRequest); onCompletion = metrics.startTiming('total').stopBefore(onCompletion); - - let requests = metrics.measureTime('buildRequests', () => spec.buildRequests(bids, bidderRequest)); + const tidGuard = guardTids(bidderRequest); + let requests = metrics.measureTime('buildRequests', () => spec.buildRequests(bids.map(tidGuard.bidRequest), tidGuard.bidderRequest(bidderRequest))); if (!requests || requests.length === 0) { onCompletion(); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index 7223940bc45..ceb3446b570 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -1751,7 +1751,7 @@ describe('the price floors module', function () { const req = utils.deepClone(bidRequest); _floorDataForAuction[req.auctionId] = utils.deepClone(basicFloorConfig); - expect(guardTids('mock-bidder').bidRequest(req).getFloor({})).to.deep.equal({ + expect(guardTids({bidderCode: 'mock-bidder'}).bidRequest(req).getFloor({})).to.deep.equal({ currency: 'USD', floor: 1.0 }); From 3aa2688a3000dda58b6489cf428f0b23efb65992 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 4 Sep 2024 09:04:08 -0700 Subject: [PATCH 06/14] handle TIDs for buildPAAPIConfigs --- modules/paapi.js | 14 ++++++++++---- test/spec/modules/paapi_spec.js | 31 +++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/modules/paapi.js b/modules/paapi.js index 40e5083775b..24fc83a0d7c 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -20,7 +20,7 @@ import {currencyCompare} from '../libraries/currencyUtils/currency.js'; import {keyCompare, maximum, minimum} from '../src/utils/reducers.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {auctionStore} from '../libraries/weakStore/weakStore.js'; -import {adapterMetrics} from '../src/adapters/bidderFactory.js'; +import {adapterMetrics, guardTids} from '../src/adapters/bidderFactory.js'; import {defer} from '../src/utils/promise.js'; import {auctionManager} from '../src/auctionManager.js'; @@ -480,7 +480,10 @@ export function markForFledge(next, bidderRequests) { next(bidderRequests); } -export const ASYNC_SIGNALS = ['auctionSignals', 'sellerSignals', 'perBuyerSignals', 'perBuyerTimeouts', 'deprecatedRenderURLReplacements', 'directFromSellerSignals']; +// NOTE: according to https://github.com/WICG/turtledove/blob/main/FLEDGE.md#211-providing-signals-asynchronously, +// `directFromSellerSignals` can also be async, but unlike the others there doesn't seem to be a "safe" default +// to use when the adapter fails to provide a value +export const ASYNC_SIGNALS = ['auctionSignals', 'sellerSignals', 'perBuyerSignals', 'perBuyerTimeouts', 'deprecatedRenderURLReplacements']; const validatePartialConfig = (() => { const REQUIRED_SYNC_SIGNALS = [ @@ -524,7 +527,9 @@ export function parallelPaapiProcessing(next, spec, bids, bidderRequest, ...args const auctionConfigs = configsForAuction(auctionId); bids.map(bid => bid.adUnitCode).forEach(adUnitCode => { latestAuctionForAdUnit[adUnitCode] = auctionId; - auctionConfigs[adUnitCode] = null; + if (!auctionConfigs.hasOwnProperty(adUnitCode)) { + auctionConfigs[adUnitCode] = null; + } }); if (enabled && spec.buildPAAPIConfigs) { @@ -532,7 +537,8 @@ export function parallelPaapiProcessing(next, spec, bids, bidderRequest, ...args let partialConfigs; metrics.measureTime('buildPAAPIConfigs', () => { try { - partialConfigs = spec.buildPAAPIConfigs(bids, bidderRequest) + const tidGuard = guardTids(bidderRequest); + partialConfigs = spec.buildPAAPIConfigs(bids.map(tidGuard.bidRequest), tidGuard.bidderRequest(bidderRequest)) } catch (e) { logError(`Error invoking "buildPAAPIConfigs":`, e); } diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js index 28617602989..67165b0891e 100644 --- a/test/spec/modules/paapi_spec.js +++ b/test/spec/modules/paapi_spec.js @@ -1196,10 +1196,6 @@ describe('paapi module', () => { spec.buildPAAPIConfigs = sinon.stub().callsFake(() => builtCfg); }); - afterEach(() => { - sinon.assert.calledWith(spec.buildPAAPIConfigs, bids, bidderRequest); - }) - it('should make async config available from getPAAPIConfig', () => { startParallel(); const actual = getPAAPIConfig(); @@ -1225,6 +1221,33 @@ describe('paapi module', () => { }); }); + it('should work when called multiple times for the same auction', () => { + startParallel(); + spec.buildPAAPIConfigs = sinon.stub().callsFake(() => []); + startParallel(); + expect(getPAAPIConfig().au.componentAuctions.length).to.eql(1); + }); + + it('should hide TIDs from buildPAAPIConfigs', () => { + config.setConfig({enableTIDs: false}); + startParallel(); + sinon.assert.calledWith( + spec.buildPAAPIConfigs, + sinon.match(bidRequests => bidRequests.every(req => req.auctionId == null)), + sinon.match(bidderRequest => bidderRequest.auctionId == null) + ); + }); + + it('should show TIDs when enabled', () => { + config.setConfig({enableTIDs: true}); + startParallel(); + sinon.assert.calledWith( + spec.buildPAAPIConfigs, + sinon.match(bidRequests => bidRequests.every(req => req.auctionId === 'aid')), + sinon.match(bidderRequest => bidderRequest.auctionId === 'aid') + ) + }) + it('should respect requestedSize from adapter', () => { mockConfig.requestedSize = {width: 1, height: 2}; startParallel(); From 0fa2a2c7691bf0de013dceb0cb36f379ab504210 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 4 Sep 2024 09:07:13 -0700 Subject: [PATCH 07/14] turn on parallel flag in integ example --- integrationExamples/top-level-paapi/no_adserver.html | 1 + 1 file changed, 1 insertion(+) diff --git a/integrationExamples/top-level-paapi/no_adserver.html b/integrationExamples/top-level-paapi/no_adserver.html index 0b37f80f27c..dd363e53485 100644 --- a/integrationExamples/top-level-paapi/no_adserver.html +++ b/integrationExamples/top-level-paapi/no_adserver.html @@ -43,6 +43,7 @@ debug: true, paapi: { enabled: true, + parallel: true, gpt: { autoconfig: false }, From b9bff062e181e992d5e5fe0daa3fbaa64e3cf7d5 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 4 Sep 2024 11:41:06 -0700 Subject: [PATCH 08/14] Support parallel igb --- modules/paapi.js | 113 +++++--- test/spec/modules/paapi_spec.js | 449 ++++++++++++++++++++++---------- 2 files changed, 394 insertions(+), 168 deletions(-) diff --git a/modules/paapi.js b/modules/paapi.js index 24fc83a0d7c..dc0b379773e 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -11,7 +11,8 @@ import { logInfo, logWarn, mergeDeep, - sizesToSizeTuples + sizesToSizeTuples, + deepClone } from '../src/utils.js'; import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js'; import * as events from '../src/events.js'; @@ -139,7 +140,7 @@ export function buyersToAuctionConfigs(igbRequests, merge = mergeBuyers, config .map(([request, igbs]) => { const auctionConfig = mergeDeep(merge(igbs), config.auctionConfig); auctionConfig.auctionSignals = setFPD(auctionConfig.auctionSignals || {}, request); - return auctionConfig; + return [request, auctionConfig]; }); } @@ -159,7 +160,7 @@ function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adU if (pendingConfigs && pendingBuyers) { Object.entries(pendingBuyers).forEach(([adUnitCode, igbRequests]) => { - buyersToAuctionConfigs(igbRequests).forEach(auctionConfig => append(pendingConfigs, adUnitCode, {config: auctionConfig})) + buyersToAuctionConfigs(igbRequests).forEach(([{bidder}, auctionConfig]) => append(pendingConfigs, adUnitCode, {id: getComponentSellerConfigId(bidder), config: auctionConfig})) }) } @@ -193,7 +194,7 @@ function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adU Object.entries(deferredConfigs).forEach(([adUnitCode, {top, components}]) => { resolveSignals(signals[adUnitCode], top); - Object.entries(components).forEach(([configId, deferrals]) => { + Object.entries(components).forEach(([configId, {deferrals}]) => { const matchingConfigs = configsById.hasOwnProperty(configId) ? configsById[configId] : []; if (matchingConfigs.length > 1) { logWarn(`Received multiple PAAPI configs for the same bidder and seller (${configId}), active PAAPI auctions will only see the first`); @@ -240,6 +241,10 @@ function getConfigId(bidderCode, seller) { return `${bidderCode}::${seller}`; } +function getComponentSellerConfigId(bidderCode) { + return moduleConfig.componentSeller.separateAuctions ? `igb::${bidderCode}` : 'igb'; +} + export function addPaapiConfigHook(next, request, paapiConfig) { if (getFledgeConfig(config.getCurrentBidder()).enabled) { const {adUnitCode, auctionId, bidder} = request; @@ -511,6 +516,11 @@ const validatePartialConfig = (() => { } })() +/* + * Adapters can provide a `spec.buildPAAPIConfigs` to be included in PAAPI auctions + * that can be started in parallel to contextual auctions. + * + */ export function parallelPaapiProcessing(next, spec, bids, bidderRequest, ...args) { function makeDeferrals(defaults = {}) { let promises = {}; @@ -523,7 +533,7 @@ export function parallelPaapiProcessing(next, spec, bids, bidderRequest, ...args return [deferrals, promises]; } - const {auctionId, paapi: {enabled} = {}} = bidderRequest; + const {auctionId, paapi: {enabled, componentSeller} = {}} = bidderRequest; const auctionConfigs = configsForAuction(auctionId); bids.map(bid => bid.adUnitCode).forEach(adUnitCode => { latestAuctionForAdUnit[adUnitCode] = auctionId; @@ -544,40 +554,81 @@ export function parallelPaapiProcessing(next, spec, bids, bidderRequest, ...args } }); const requestsById = Object.fromEntries(bids.map(bid => [bid.bidId, bid])); - (partialConfigs ?? []).forEach(({bidId, config}) => { + (partialConfigs ?? []).forEach(({bidId, config, igb}) => { const bidRequest = requestsById.hasOwnProperty(bidId) && requestsById[bidId]; if (!bidRequest) { - logWarn(`Received partial PAAPI config for unknown bidId, config will be ignored`, {bidId, config}); - } else if (validatePartialConfig(config)) { + logError(`Received partial PAAPI config for unknown bidId`, {bidId, config}); + } else { const adUnitCode = bidRequest.adUnitCode; latestAuctionForAdUnit[adUnitCode] = auctionId; const deferredConfigs = deferredConfigsForAuction(auctionId); - if (!deferredConfigs.hasOwnProperty(adUnitCode)) { - const [deferrals, promises] = makeDeferrals(); - deferredConfigs[adUnitCode] = { - top: deferrals, - components: {} + + // eslint-disable-next-line no-inner-declarations + function getDeferredConfig() { + if (!deferredConfigs.hasOwnProperty(adUnitCode)) { + const [deferrals, promises] = makeDeferrals(); + auctionConfigs[adUnitCode] = { + ...getStaticSignals(auctionManager.index.getAdUnit(bidRequest)), + ...promises, + componentAuctions: [] + } + deferredConfigs[adUnitCode] = { + top: deferrals, + components: {}, + auctionConfig: auctionConfigs[adUnitCode] + } } - auctionConfigs[adUnitCode] = { - ...getStaticSignals(auctionManager.index.getAdUnit(bidRequest)), - ...promises, - componentAuctions: [] + return deferredConfigs[adUnitCode]; + } + + if (config && validatePartialConfig(config)) { + const configId = getConfigId(bidRequest.bidder, config.seller); + const deferredConfig = getDeferredConfig(); + if (deferredConfig.components.hasOwnProperty(configId)) { + logWarn(`Received multiple PAAPI configs for the same bidder and seller; config will be ignored`, { + config, + bidder: bidRequest.bidder + }) + } else { + const [deferrals, promises] = makeDeferrals(config); + const auctionConfig = { + ...getStaticSignals(bidRequest), + ...config, + ...promises + } + deferredConfig.auctionConfig.componentAuctions.push(auctionConfig) + deferredConfig.components[configId] = {auctionConfig, deferrals}; } } - const configId = getConfigId(bidRequest.bidder, config.seller); - if (deferredConfigs[adUnitCode].components.hasOwnProperty(configId)) { - logWarn(`Received multiple PAAPI configs for the same bidder and seller; config will be ignored`, { - config, - bidder: bidRequest.bidder - }) - } else { - const [deferrals, promises] = makeDeferrals(config); - deferredConfigs[adUnitCode].components[configId] = deferrals; - auctionConfigs[adUnitCode].componentAuctions.push({ - ...getStaticSignals(bidRequest), - ...config, - ...promises - }) + if (componentSeller && igb && checkOrigin(igb)) { + const configId = getComponentSellerConfigId(spec.code); + const deferredConfig = getDeferredConfig(); + const partialConfig = buyersToAuctionConfigs([[bidRequest, igb]])[0][1]; + if (deferredConfig.components.hasOwnProperty(configId)) { + const {auctionConfig, deferrals} = deferredConfig.components[configId]; + if (!auctionConfig.interestGroupBuyers.includes(igb.origin)) { + const immediate = {}; + Object.entries(partialConfig).forEach(([key, value]) => { + if (deferrals.hasOwnProperty(key)) { + mergeDeep(deferrals[key], {default: value}); + } else { + immediate[key] = value; + } + }) + mergeDeep(auctionConfig, immediate); + } else { + logWarn(`Received the same PAAPI buyer multiple times for the same PAAPI auction. Consider setting paapi.componentSeller.separateAuctions: true`, igb) + } + } else { + const [deferrals, promises] = makeDeferrals(partialConfig); + const auctionConfig = { + ...partialConfig, + ...getStaticSignals(bidRequest), + ...promises, + } + deferredConfig.components[configId] = {auctionConfig, deferrals}; + deferredConfig.auctionConfig.componentAuctions.push(auctionConfig); + } } } }) diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js index 67165b0891e..c26af192f64 100644 --- a/test/spec/modules/paapi_spec.js +++ b/test/spec/modules/paapi_spec.js @@ -1051,11 +1051,11 @@ describe('paapi module', () => { it('uses compact partitions by default, and returns an auction config for each one', () => { partitioners.compact.returns([[{}, 1], [{}, 2]]); const [cf1, cf2] = toAuctionConfig(); - sinon.assert.match(cf1, { + sinon.assert.match(cf1[1], { ...config.auctionConfig, config: 0 }); - sinon.assert.match(cf2, { + sinon.assert.match(cf2[1], { ...config.auctionConfig, config: 1 }); @@ -1083,8 +1083,8 @@ describe('paapi module', () => { }; partitioners.compact.returns([[{}], [fpd]]); const [cf1, cf2] = toAuctionConfig(); - expect(cf1.auctionSignals?.prebid).to.not.exist; - expect(cf2.auctionSignals.prebid).to.eql(fpd); + expect(cf1[1].auctionSignals?.prebid).to.not.exist; + expect(cf2[1].auctionSignals.prebid).to.eql(fpd); }); }); }); @@ -1121,7 +1121,8 @@ describe('paapi module', () => { describe('parallel PAAPI auctions', () => { describe('parallellPaapiProcessing', () => { - let next, spec, bids, bidderRequest, restOfTheArgs, mockConfig, mockAuction; + let next, spec, bids, bidderRequest, restOfTheArgs, mockConfig, mockAuction, bidsReceived, bidderRequests, adUnitCodes, adUnits; + beforeEach(() => { next = sinon.stub(); spec = { @@ -1146,6 +1147,10 @@ describe('paapi module', () => { interestGroupBuyers: ['mock.buyer'] } mockAuction = {}; + bidsReceived = [{adUnitCode: 'au', cpm: 1}]; + adUnits = [{code: 'au'}] + adUnitCodes = ['au']; + bidderRequests = [bidderRequest]; sandbox.stub(auctionManager.index, 'getAuction').callsFake(() => mockAuction); sandbox.stub(auctionManager.index, 'getAdUnit').callsFake((req) => bids.find(bid => bid.adUnitCode === req.adUnitCode)) config.setConfig({paapi: {enabled: true}}); @@ -1161,6 +1166,10 @@ describe('paapi module', () => { onAuctionInit({auctionId: 'aid'}) } + function endAuction() { + events.emit(EVENTS.AUCTION_END, {auctionId: 'aid', bidsReceived, bidderRequests, adUnitCodes, adUnits}) + } + describe('should have no effect when', () => { afterEach(() => { expect(getPAAPIConfig({}, true)).to.eql({au: null}); @@ -1189,6 +1198,13 @@ describe('paapi module', () => { }); }); + function resolveConfig(auctionConfig) { + return Promise.all( + Object.entries(auctionConfig) + .map(([key, value]) => Promise.resolve(value).then(value => [key, value])) + ).then(result => Object.fromEntries(result)) + } + describe('when buildPAAPIConfigs returns valid config', () => { let builtCfg; beforeEach(() => { @@ -1270,167 +1286,326 @@ describe('paapi module', () => { startParallel(); expect(getPAAPIConfig().au.componentAuctions.length).to.eql(1); }); + it('should resolve top level config with auction signals', async () => { + startParallel(); + let config = getPAAPIConfig().au; + endAuction(); + config = await resolveConfig(config); + sinon.assert.match(config, { + auctionSignals: { + prebid: {bidfloor: 1} + } + }) + }); - describe('on auction end', () => { - let bidsReceived, bidderRequests, adUnitCodes, adUnits; + describe('when adapter returns the rest of auction config', () => { + let configRemainder; beforeEach(() => { - bidsReceived = [{adUnitCode: 'au', cpm: 1}]; - adUnits = [{code: 'au'}] - adUnitCodes = ['au']; - bidderRequests = [bidderRequest]; - }); - - function endAuction() { - events.emit(EVENTS.AUCTION_END, {auctionId: 'aid', bidsReceived, bidderRequests, adUnitCodes, adUnits}) - } - - function resolveConfig(auctionConfig) { - return Promise.all( - Object.entries(auctionConfig) - .map(([key, value]) => Promise.resolve(value).then(value => [key, value])) - ).then(result => Object.fromEntries(result)) + configRemainder = { + ...Object.fromEntries(ASYNC_SIGNALS.map(signal => [signal, {type: signal}])), + seller: 'mock.seller' + }; + }) + function returnRemainder() { + addPaapiConfigHook(sinon.stub(), bids[0], {config: configRemainder}); } - - it('should resolve top level config with auction signals', async () => { + it('should resolve component configs with values returned by adapters', async () => { startParallel(); - let config = getPAAPIConfig().au; + let config = getPAAPIConfig().au.componentAuctions[0]; + returnRemainder(); endAuction(); config = await resolveConfig(config); - sinon.assert.match(config, { + sinon.assert.match(config, configRemainder); + }); + + it('should pick first config that matches bidId/seller', async () => { + startParallel(); + let config = getPAAPIConfig().au.componentAuctions[0]; + returnRemainder(); + const expectedSignals = {...configRemainder}; + configRemainder = { + ...configRemainder, auctionSignals: { - prebid: {bidfloor: 1} + this: 'should be ignored' } - }) + } + returnRemainder(); + endAuction(); + config = await resolveConfig(config); + sinon.assert.match(config, expectedSignals); }); - describe('when adapter returns the rest of auction config', () => { - let configRemainder; + describe('should default to values returned from buildPAAPIConfigs when interpretResponse', () => { beforeEach(() => { - configRemainder = { - ...Object.fromEntries(ASYNC_SIGNALS.map(signal => [signal, {type: signal}])), - seller: 'mock.seller' - }; - }) - function returnRemainder() { - addPaapiConfigHook(sinon.stub(), bids[0], {config: configRemainder}); - } - it('should resolve component configs with values returned by adapters', async () => { - startParallel(); - let config = getPAAPIConfig().au.componentAuctions[0]; - returnRemainder(); - endAuction(); - config = await resolveConfig(config); - sinon.assert.match(config, configRemainder); + ASYNC_SIGNALS.forEach(signal => mockConfig[signal] = {default: signal}) }); - - it('should pick first config that matches bidId/seller', async () => { - startParallel(); - let config = getPAAPIConfig().au.componentAuctions[0]; - returnRemainder(); - const expectedSignals = {...configRemainder}; - configRemainder = { - ...configRemainder, - auctionSignals: { - this: 'should be ignored' - } + Object.entries({ + 'returns no matching config'() { + }, + 'does not include values in response'() { + configRemainder = {}; + returnRemainder(); } - returnRemainder(); - endAuction(); - config = await resolveConfig(config); - sinon.assert.match(config, expectedSignals); + }).forEach(([t, postResponse]) => { + it(t, async () => { + startParallel(); + let config = getPAAPIConfig().au.componentAuctions[0]; + postResponse(); + endAuction(); + config = await resolveConfig(config); + sinon.assert.match(config, mockConfig); + }); }); + }); - describe('should default to values returned from buildPAAPIConfigs when interpretResponse', () => { - beforeEach(() => { - ASYNC_SIGNALS.forEach(signal => mockConfig[signal] = {default: signal}) - }); - Object.entries({ - 'returns no matching config'() { + it('should make extra configs available', async () => { + startParallel(); + returnRemainder(); + configRemainder = {...configRemainder, seller: 'other.seller'}; + returnRemainder(); + endAuction(); + let configs = getPAAPIConfig().au.componentAuctions; + configs = [await resolveConfig(configs[0]), configs[1]]; + expect(configs.map(cfg => cfg.seller)).to.eql(['mock.seller', 'other.seller']); + }); + + describe('submodule\'s onAuctionConfig', () => { + let onAuctionConfig; + beforeEach(() => { + onAuctionConfig = sinon.stub(); + registerSubmodule({onAuctionConfig}) + }); + + Object.entries({ + 'parallel=true, some configs deferred': { + setup() { + config.mergeConfig({paapi: {parallel: true}}) }, - 'does not include values in response'() { - configRemainder = {}; - returnRemainder(); + delayed: false, + }, + 'parallel=true, no deferred configs': { + setup() { + config.mergeConfig({paapi: {parallel: true}}); + spec.buildPAAPIConfigs = sinon.stub().callsFake(() => []); + }, + delayed: true + }, + 'parallel=false, some configs deferred': { + setup() { + config.mergeConfig({paapi: {parallel: false}}) + }, + delayed: true + } + }).forEach(([t, {setup, delayed}]) => { + describe(`when ${t}`, () => { + beforeEach(() => { + mockAuction.requestsDone = Promise.resolve(); + setup(); + }); + + function expectInvoked(shouldBeInvoked) { + if (shouldBeInvoked) { + sinon.assert.calledWith(onAuctionConfig, 'aid', sinon.match(arg => arg.au.componentAuctions[0].seller === 'mock.seller')); + } else { + sinon.assert.notCalled(onAuctionConfig); + } } - }).forEach(([t, postResponse]) => { - it(t, async () => { + + it(`should invoke onAuctionConfig when ${delayed ? 'auction ends' : 'auction requests have started'}`, async () => { startParallel(); - let config = getPAAPIConfig().au.componentAuctions[0]; - postResponse(); + await mockAuction.requestsDone; + expectInvoked(!delayed); + onAuctionConfig.reset(); + returnRemainder(); endAuction(); - config = await resolveConfig(config); - sinon.assert.match(config, mockConfig); - }); - }); + expectInvoked(delayed); + }) + }) + }) + }) + }); + }); + describe('when buildPAAPIConfigs returns igb', () => { + let builtCfg, igb, auctionConfig; + beforeEach(() => { + igb = {origin: 'mock.buyer'} + builtCfg = [{bidId: 'bidId', igb}]; + spec.buildPAAPIConfigs = sinon.stub().callsFake(() => builtCfg); + auctionConfig = { + seller: 'mock.seller', + decisionLogicUrl: 'mock.seller/decisionLogic' + } + config.mergeConfig({ + paapi: { + componentSeller: { + auctionConfig + } + } + }) + bidderRequest.paapi.componentSeller = true; + }); + Object.entries({ + 'componentSeller not configured'() { + bidderRequest.paapi.componentSeller = false; + }, + 'buildPAAPIconfig returns nothing'() { + builtCfg = [] + }, + 'returned igb is not valid'() { + builtCfg = [{bidId: 'bidId', igb: {}}]; + } + }).forEach(([t, setup]) => { + it(`should have no effect when ${t}`, () => { + setup(); + startParallel(); + expect(getPAAPIConfig()).to.eql({}); + }) + }) + + describe('when component seller is set up', () => { + it('should generate a deferred auctionConfig', () => { + startParallel(); + sinon.assert.match(getPAAPIConfig().au.componentAuctions[0], { + ...auctionConfig, + interestGroupBuyers: ['mock.buyer'], + }) + }); + + it('should use signal values from componentSeller.auctionConfig', async () => { + auctionConfig.auctionSignals = {test: 'signal'}; + config.mergeConfig({ + paapi: {componentSeller: {auctionConfig}} + }) + startParallel(); + endAuction(); + const cfg = await resolveConfig(getPAAPIConfig().au.componentAuctions[0]); + sinon.assert.match(cfg.auctionSignals, auctionConfig.auctionSignals); + }) + + it('should collate buyers', () => { + startParallel(); + startParallel(); + sinon.assert.match(getPAAPIConfig().au.componentAuctions[0], { + interestGroupBuyers: ['mock.buyer'] }); + }); - it('should make extra configs available', async () => { - startParallel(); - returnRemainder(); - configRemainder = {...configRemainder, seller: 'other.seller'}; - returnRemainder(); - endAuction(); - let configs = getPAAPIConfig().au.componentAuctions; - configs = [await resolveConfig(configs[0]), configs[1]]; - expect(configs.map(cfg => cfg.seller)).to.eql(['mock.seller', 'other.seller']); + function returnIgb(igb) { + addPaapiConfigHook(sinon.stub(), bids[0], {igb}); + } + + it('should resolve to values from interpretResponse as well as buildPAAPIConfigs', async () => { + igb.cur = 'cur'; + igb.pbs = {over: 'ridden'} + startParallel(); + let cfg = getPAAPIConfig().au.componentAuctions[0]; + returnIgb({ + origin: 'mock.buyer', + pbs: {some: 'signal'} }); + endAuction(); + cfg = await resolveConfig(cfg); + sinon.assert.match(cfg, { + perBuyerSignals: { + [igb.origin]: {some: 'signal'}, + }, + perBuyerCurrencies: { + [igb.origin]: 'cur' + } + }) + }); - describe('submodule\'s onAuctionConfig', () => { - let onAuctionConfig; - beforeEach(() => { - onAuctionConfig = sinon.stub(); - registerSubmodule({onAuctionConfig}) - }); + it('should not overwrite config once resolved', () => { + startParallel(); + returnIgb({ + origin: 'mock.buyer', + }); + endAuction(); + const cfg = getPAAPIConfig().au; + sinon.assert.match(cfg, Object.fromEntries(ASYNC_SIGNALS.map(signal => [signal, sinon.match(arg => arg instanceof Promise)]))) + }) - Object.entries({ - 'parallel=true, some configs deferred': { - setup() { - config.mergeConfig({paapi: {parallel: true}}) - }, - delayed: false, - }, - 'parallel=true, no deferred configs': { - setup() { - config.mergeConfig({paapi: {parallel: true}}); - spec.buildPAAPIConfigs = sinon.stub().callsFake(() => []); - }, - delayed: true - }, - 'parallel=false, some configs deferred': { - setup() { - config.mergeConfig({paapi: {parallel: false}}) - }, - delayed: true - } - }).forEach(([t, {setup, delayed}]) => { - describe(`when ${t}`, () => { - beforeEach(() => { - mockAuction.requestsDone = Promise.resolve(); - setup(); - }); + it('can resolve multiple igbs', async () => { + igb.cur = 'cur1'; + startParallel(); + spec.code = 'other'; + igb.origin = 'other.buyer' + igb.cur = 'cur2' + startParallel(); + let cfg = getPAAPIConfig().au.componentAuctions[0]; + returnIgb({ + origin: 'mock.buyer', + pbs: {signal: 1} + }); + returnIgb({ + origin: 'other.buyer', + pbs: {signal: 2} + }); + endAuction(); + cfg = await resolveConfig(cfg); + sinon.assert.match(cfg, { + perBuyerSignals: { + 'mock.buyer': {signal: 1}, + 'other.buyer': {signal: 2} + }, + perBuyerCurrencies: { + 'mock.buyer': 'cur1', + 'other.buyer': 'cur2' + } + }) + }) - function expectInvoked(shouldBeInvoked) { - if (shouldBeInvoked) { - sinon.assert.calledWith(onAuctionConfig, 'aid', sinon.match(arg => arg.au.componentAuctions[0].seller === 'mock.seller')); - } else { - sinon.assert.notCalled(onAuctionConfig); - } + function startMultiple() { + startParallel(); + spec.code = 'other'; + igb.origin = 'other.buyer' + startParallel(); + } + + + describe('when using separateAuctions=false', () => { + beforeEach(() => { + config.mergeConfig({ + paapi: { + componentSeller: { + separateAuctions: false } + } + }) + }); - it(`should invoke onAuctionConfig when ${delayed ? 'auction ends' : 'auction requests have started'}`, async () => { - startParallel(); - await mockAuction.requestsDone; - expectInvoked(!delayed); - onAuctionConfig.reset(); - returnRemainder(); - endAuction(); - expectInvoked(delayed); - }) - }) + it('should merge igb from different specs into a single auction config', () => { + startMultiple(); + sinon.assert.match(getPAAPIConfig().au.componentAuctions[0], { + interestGroupBuyers: ['mock.buyer', 'other.buyer'] + }); + }); + }) + + describe('when using separateAuctions=true', () => { + beforeEach(() => { + config.mergeConfig({ + paapi: { + componentSeller: { + separateAuctions: true + } + } + }) + }); + it('should generate an auction config for each bidder', () => { + startMultiple(); + const components = getPAAPIConfig().au.componentAuctions; + sinon.assert.match(components[0], { + interestGroupBuyers: ['mock.buyer'] + }) + sinon.assert.match(components[1], { + interestGroupBuyers: ['other.buyer'] }) }) - }); - }); - }); + }) + + }) + }) }); }); From 4c783543b0d7b43c8f2ad9a8429eea8bb9566b1d Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 4 Sep 2024 13:28:49 -0700 Subject: [PATCH 09/14] improve comment --- modules/paapi.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/modules/paapi.js b/modules/paapi.js index dc0b379773e..6796ebbfc1c 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -517,9 +517,24 @@ const validatePartialConfig = (() => { })() /* - * Adapters can provide a `spec.buildPAAPIConfigs` to be included in PAAPI auctions + * Adapters can provide a `spec.buildPAAPIConfigs(validBidRequests, bidderRequest)` to be included in PAAPI auctions * that can be started in parallel to contextual auctions. * + * If PAAPI is enabled, and an adapter provides `buildPAAPIConfigs`, it is invoked just before `buildRequests`, and takes + * the same arguments. It should return an array of PAAPI configuration objects withe same format as in `interpretResponse`, + * {bidId, config?, igb?}. + * + * Everything returned by `buildPAAPIConfigs` is treated in the same way as if it was returned by `interpetResponse` - + * except for signals that can be provided asynchronously (cfr. `ASYNC_SIGNALS`), which are tentatively replaced by promises. + * + * When the (contextual) auction ends, the promises are resolved. + * If during the auction the adapter's `interpretResponse` returned matching configurations (same `bidId`, and a `config` with the same `seller`, + * or an `igb` with the same `origin`), the promises resolve to their contents. Otherwise, they resolve to the values + * provided by `buildPAAPIConfigs`, or an empty object if no value was provided. + * + * Promisified auction configs are available from `getPAAPIConfig` immediately after a contextual auction is started. + * If the `paapi.parallel` config flag is set, PAAPI submodules are also triggered at the same time (instead of + * when the auction ends). */ export function parallelPaapiProcessing(next, spec, bids, bidderRequest, ...args) { function makeDeferrals(defaults = {}) { From 79f2be1cae7306d36ec6a6529d32a8f265cf3ec4 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 4 Sep 2024 13:31:07 -0700 Subject: [PATCH 10/14] fix lint --- modules/paapi.js | 3 +-- test/spec/modules/paapi_spec.js | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/modules/paapi.js b/modules/paapi.js index 6796ebbfc1c..1b7fabfc51e 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -11,8 +11,7 @@ import { logInfo, logWarn, mergeDeep, - sizesToSizeTuples, - deepClone + sizesToSizeTuples } from '../src/utils.js'; import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js'; import * as events from '../src/events.js'; diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js index c26af192f64..44d7681e619 100644 --- a/test/spec/modules/paapi_spec.js +++ b/test/spec/modules/paapi_spec.js @@ -1562,7 +1562,6 @@ describe('paapi module', () => { startParallel(); } - describe('when using separateAuctions=false', () => { beforeEach(() => { config.mergeConfig({ @@ -1603,7 +1602,6 @@ describe('paapi module', () => { }) }) }) - }) }) }); From d296a674798ba0f7e9508019513a26f915f7eea4 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 4 Sep 2024 16:03:36 -0700 Subject: [PATCH 11/14] surrender to the Linter --- modules/paapi.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/paapi.js b/modules/paapi.js index 1b7fabfc51e..a5260c3bcdc 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -577,8 +577,7 @@ export function parallelPaapiProcessing(next, spec, bids, bidderRequest, ...args latestAuctionForAdUnit[adUnitCode] = auctionId; const deferredConfigs = deferredConfigsForAuction(auctionId); - // eslint-disable-next-line no-inner-declarations - function getDeferredConfig() { + const getDeferredConfig = () => { if (!deferredConfigs.hasOwnProperty(adUnitCode)) { const [deferrals, promises] = makeDeferrals(); auctionConfigs[adUnitCode] = { From 2ac56db9207a326d42a8c7bad69ccab5c3081841 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 5 Sep 2024 08:48:22 -0700 Subject: [PATCH 12/14] convert optable --- modules/optableBidAdapter.js | 33 ++++++++++++++++++--- modules/paapi.js | 30 +++++++++---------- test/spec/modules/optableBidAdapter_spec.js | 27 +++++++++++++++++ 3 files changed, 71 insertions(+), 19 deletions(-) diff --git a/modules/optableBidAdapter.js b/modules/optableBidAdapter.js index 4e639fb88ee..d2dae252e6c 100644 --- a/modules/optableBidAdapter.js +++ b/modules/optableBidAdapter.js @@ -17,17 +17,42 @@ const BIDDER_CODE = 'optable'; const DEFAULT_REGION = 'ca' const DEFAULT_ORIGIN = 'https://ads.optable.co' +function getOrigin() { + return config.getConfig('optable.origin') ?? DEFAULT_ORIGIN; +} + +function getBaseUrl() { + const region = config.getConfig('optable.region') ?? DEFAULT_REGION; + return `${getOrigin()}/${region}` +} + export const spec = { code: BIDDER_CODE, isBidRequestValid: function(bid) { return !!bid.params?.site }, buildRequests: function(bidRequests, bidderRequest) { - const region = config.getConfig('optable.region') ?? DEFAULT_REGION - const origin = config.getConfig('optable.origin') ?? DEFAULT_ORIGIN - const requestURL = `${origin}/${region}/ortb2/v1/ssp/bid` + const requestURL = `${getBaseUrl()}/ortb2/v1/ssp/bid` const data = converter.toORTB({ bidRequests, bidderRequest, context: { mediaType: BANNER } }); - return { method: 'POST', url: requestURL, data } }, + buildPAAPIConfigs: function(bidRequests) { + const origin = getOrigin(); + return bidRequests + .filter(req => req.ortb2Imp?.ext?.ae) + .map(bid => ({ + bidId: bid.bidId, + config: { + seller: origin, + decisionLogicURL: `${getBaseUrl()}/paapi/v1/ssp/decision-logic.js?origin=${bid.params.site}`, + interestGroupBuyers: [origin], + perBuyerMultiBidLimits: { + [origin]: 100 + }, + perBuyerCurrencies: { + [origin]: 'USD' + } + } + })) + }, interpretResponse: function(response, request) { const bids = converter.fromORTB({ response: response.body, request: request.data }).bids const auctionConfigs = (response.body.ext?.optable?.fledge?.auctionconfigs ?? []).map((cfg) => { diff --git a/modules/paapi.js b/modules/paapi.js index a5260c3bcdc..c84519be59a 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -515,25 +515,25 @@ const validatePartialConfig = (() => { } })() -/* +/** * Adapters can provide a `spec.buildPAAPIConfigs(validBidRequests, bidderRequest)` to be included in PAAPI auctions - * that can be started in parallel to contextual auctions. - * - * If PAAPI is enabled, and an adapter provides `buildPAAPIConfigs`, it is invoked just before `buildRequests`, and takes - * the same arguments. It should return an array of PAAPI configuration objects withe same format as in `interpretResponse`, - * {bidId, config?, igb?}. + * that can be started in parallel with contextual auctions. * - * Everything returned by `buildPAAPIConfigs` is treated in the same way as if it was returned by `interpetResponse` - - * except for signals that can be provided asynchronously (cfr. `ASYNC_SIGNALS`), which are tentatively replaced by promises. + * If PAAPI is enabled, and an adapter provides `buildPAAPIConfigs`, it is invoked just before `buildRequests`, + * and takes the same arguments. It should return an array of PAAPI configuration objects with the same format + * as in `interpretResponse` (`{bidId, config?, igb?}`). * + * Everything returned by `buildPAAPIConfigs` is treated in the same way as if it was returned by `interpretResponse` - + * except for signals that can be provided asynchronously (cfr. `ASYNC_SIGNALS`), which are replaced by promises. * When the (contextual) auction ends, the promises are resolved. - * If during the auction the adapter's `interpretResponse` returned matching configurations (same `bidId`, and a `config` with the same `seller`, - * or an `igb` with the same `origin`), the promises resolve to their contents. Otherwise, they resolve to the values - * provided by `buildPAAPIConfigs`, or an empty object if no value was provided. * - * Promisified auction configs are available from `getPAAPIConfig` immediately after a contextual auction is started. - * If the `paapi.parallel` config flag is set, PAAPI submodules are also triggered at the same time (instead of - * when the auction ends). + * If during the auction the adapter's `interpretResponse` returned matching configurations (same `bidId`, + * and a `config` with the same `seller`, or an `igb` with the same `origin`), the promises resolve to their contents. + * Otherwise, they resolve to the values provided by `buildPAAPIConfigs`, or an empty object if no value was provided. + * + * Promisified auction configs are available from `getPAAPIConfig` immediately after `requestBids`. + * If the `paapi.parallel` config flag is set, PAAPI submodules are also triggered at the same time + * (instead of when the auction ends). */ export function parallelPaapiProcessing(next, spec, bids, bidderRequest, ...args) { function makeDeferrals(defaults = {}) { @@ -558,10 +558,10 @@ export function parallelPaapiProcessing(next, spec, bids, bidderRequest, ...args if (enabled && spec.buildPAAPIConfigs) { const metrics = adapterMetrics(bidderRequest); + const tidGuard = guardTids(bidderRequest); let partialConfigs; metrics.measureTime('buildPAAPIConfigs', () => { try { - const tidGuard = guardTids(bidderRequest); partialConfigs = spec.buildPAAPIConfigs(bids.map(tidGuard.bidRequest), tidGuard.bidderRequest(bidderRequest)) } catch (e) { logError(`Error invoking "buildPAAPIConfigs":`, e); diff --git a/test/spec/modules/optableBidAdapter_spec.js b/test/spec/modules/optableBidAdapter_spec.js index ef04474c270..b7cf2e3b44d 100644 --- a/test/spec/modules/optableBidAdapter_spec.js +++ b/test/spec/modules/optableBidAdapter_spec.js @@ -47,6 +47,33 @@ describe('optableBidAdapter', function() { }); }); + describe('buildPAAPIConfigs', () => { + function makeRequest({bidId, site = 'mockSite', ae = 1}) { + return { + bidId, + params: { + site + }, + ortb2Imp: { + ext: {ae} + } + } + } + it('should generate auction configs for ae requests', () => { + const configs = spec.buildPAAPIConfigs([ + makeRequest({bidId: 'bid1', ae: 1}), + makeRequest({bidId: 'bid2', ae: 0}), + makeRequest({bidId: 'bid3', ae: 1}), + ]); + expect(configs.map(cfg => cfg.bidId)).to.eql(['bid1', 'bid3']); + configs.forEach(cfg => sinon.assert.match(cfg.config, { + seller: 'https://ads.optable.co', + decisionLogicURL: `https://ads.optable.co/ca/paapi/v1/ssp/decision-logic.js?origin=mockSite`, + interestGroupBuyers: ['https://ads.optable.co'] + })) + }) + }) + describe('interpretResponse', function() { const validBid = { bidder: 'optable', From 0ddc1a289ecf35e245cb04f42511a60a8bfc0723 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 5 Sep 2024 09:26:21 -0700 Subject: [PATCH 13/14] improve signal handling --- .../top-level-paapi/gam-contextual.html | 1 + modules/paapi.js | 17 ++++--- test/spec/modules/paapi_spec.js | 44 ++++++++++++++++++- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/integrationExamples/top-level-paapi/gam-contextual.html b/integrationExamples/top-level-paapi/gam-contextual.html index b51b512e0ca..47bf9607680 100644 --- a/integrationExamples/top-level-paapi/gam-contextual.html +++ b/integrationExamples/top-level-paapi/gam-contextual.html @@ -52,6 +52,7 @@ debug: true, paapi: { enabled: true, + parallel: true, gpt: { autoconfig: false }, diff --git a/modules/paapi.js b/modules/paapi.js index c84519be59a..2078147df18 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -187,7 +187,15 @@ function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adU function resolveSignals(signals, deferrals) { Object.entries(deferrals).forEach(([signal, {resolve, default: defaultValue}]) => { - resolve(Object.assign({}, defaultValue, signals.hasOwnProperty(signal) ? signals[signal] : {})) + let value = signals.hasOwnProperty(signal) ? signals[signal] : null; + if (value == null && defaultValue == null) { + value = undefined; + } else if (typeof defaultValue === 'object' && typeof value === 'object') { + value = mergeDeep({}, defaultValue, value); + } else { + value = value ?? defaultValue + } + resolve(value); }) } @@ -484,10 +492,7 @@ export function markForFledge(next, bidderRequests) { next(bidderRequests); } -// NOTE: according to https://github.com/WICG/turtledove/blob/main/FLEDGE.md#211-providing-signals-asynchronously, -// `directFromSellerSignals` can also be async, but unlike the others there doesn't seem to be a "safe" default -// to use when the adapter fails to provide a value -export const ASYNC_SIGNALS = ['auctionSignals', 'sellerSignals', 'perBuyerSignals', 'perBuyerTimeouts', 'deprecatedRenderURLReplacements']; +export const ASYNC_SIGNALS = ['auctionSignals', 'sellerSignals', 'perBuyerSignals', 'perBuyerTimeouts', 'deprecatedRenderURLReplacements', 'directFromSellerSignals']; const validatePartialConfig = (() => { const REQUIRED_SYNC_SIGNALS = [ @@ -540,7 +545,7 @@ export function parallelPaapiProcessing(next, spec, bids, bidderRequest, ...args let promises = {}; const deferrals = Object.fromEntries(ASYNC_SIGNALS.map(signal => { const def = defer({promiseFactory: (resolver) => new Promise(resolver)}); - def.default = defaults.hasOwnProperty(signal) ? defaults[signal] : {}; + def.default = defaults.hasOwnProperty(signal) ? defaults[signal] : null; promises[signal] = def.promise; return [signal, def] })) diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js index 44d7681e619..c1ce7f86c2e 100644 --- a/test/spec/modules/paapi_spec.js +++ b/test/spec/modules/paapi_spec.js @@ -14,7 +14,8 @@ import { getPAAPIConfig, getPAAPISize, IGB_TO_CONFIG, - mergeBuyers, onAuctionInit, + mergeBuyers, + onAuctionInit, parallelPaapiProcessing, parseExtIgi, parseExtPrebidFledge, @@ -1335,7 +1336,7 @@ describe('paapi module', () => { sinon.assert.match(config, expectedSignals); }); - describe('should default to values returned from buildPAAPIConfigs when interpretResponse', () => { + describe('should default to values returned from buildPAAPIConfigs when interpretResponse does not return', () => { beforeEach(() => { ASYNC_SIGNALS.forEach(signal => mockConfig[signal] = {default: signal}) }); @@ -1358,6 +1359,45 @@ describe('paapi module', () => { }); }); + it('should resolve to undefined when no value is available', async () => { + startParallel(); + let config = getPAAPIConfig().au.componentAuctions[0]; + delete configRemainder.sellerSignals; + returnRemainder(); + endAuction(); + config = await resolveConfig(config); + expect(config.sellerSignals).to.be.undefined; + }); + + [ + { + start: {t: 'scalar', value: 'str'}, + end: {t: 'array', value: ['abc']}, + should: {t: 'array', value: ['abc']} + }, + { + start: {t: 'object', value: {a: 'b'}}, + end: {t: 'scalar', value: 'abc'}, + should: {t: 'scalar', value: 'abc'} + }, + { + start: {t: 'object', value: {outer: {inner: 'val'}}}, + end: {t: 'object', value: {outer: {other: 'val'}}}, + should: {t: 'merge', value: {outer: {inner: 'val', other: 'val'}}} + } + ].forEach(({start, end, should}) => { + it(`when buildPAAPIConfigs returns ${start.t}, interpretResponse return ${end.t}, promise should resolve to ${should.t}`, async () => { + mockConfig.sellerSignals = start.value + startParallel(); + let config = getPAAPIConfig().au.componentAuctions[0]; + configRemainder.sellerSignals = end.value; + returnRemainder(); + endAuction(); + config = await resolveConfig(config); + expect(config.sellerSignals).to.eql(should.value); + }) + }) + it('should make extra configs available', async () => { startParallel(); returnRemainder(); From 4fe18523b50aee18676da874c5a17d96d0f2ffd0 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Mon, 7 Oct 2024 09:17:47 -0700 Subject: [PATCH 14/14] Do not provide deprecatedRenderURLReplacements in parallel auctions --- modules/paapi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/paapi.js b/modules/paapi.js index 2078147df18..eb558b9024b 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -492,7 +492,7 @@ export function markForFledge(next, bidderRequests) { next(bidderRequests); } -export const ASYNC_SIGNALS = ['auctionSignals', 'sellerSignals', 'perBuyerSignals', 'perBuyerTimeouts', 'deprecatedRenderURLReplacements', 'directFromSellerSignals']; +export const ASYNC_SIGNALS = ['auctionSignals', 'sellerSignals', 'perBuyerSignals', 'perBuyerTimeouts', 'directFromSellerSignals']; const validatePartialConfig = (() => { const REQUIRED_SYNC_SIGNALS = [