diff --git a/modules/paapi.js b/modules/paapi.js index 8ddd1912c29..9ae2c870e5d 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -54,6 +54,7 @@ export function init(cfg) { } getHook('addPaapiConfig').before(addPaapiConfigHook); +getHook('makeBidRequests').before(addPaapiData); getHook('makeBidRequests').after(markForFledge); events.on(EVENTS.AUCTION_END, onAuctionEnd); @@ -332,38 +333,50 @@ function getRequestedSize(adUnit) { })(); } +export function addPaapiData(next, adUnits, ...args) { + if (isFledgeSupported() && moduleConfig.enabled) { + adUnits.forEach(adUnit => { + // https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/Protected%20Audience%20Support.md + const igsAe = adUnit.ortb2Imp?.ext?.igs != null + ? adUnit.ortb2Imp.ext.igs.ae || 1 + : null; + const extAe = adUnit.ortb2Imp?.ext?.ae; + if (igsAe !== extAe && igsAe != null && extAe != null) { + logWarn(MODULE, `Ad unit defines conflicting ortb2Imp.ext.ae and ortb2Imp.ext.igs, using the latter`, adUnit); + } + const ae = igsAe ?? extAe ?? moduleConfig.defaultForSlots; + if (ae) { + deepSetValue(adUnit, 'ortb2Imp.ext.ae', ae); + adUnit.ortb2Imp.ext.igs = Object.assign({ + ae: ae, + biddable: 1 + }, adUnit.ortb2Imp.ext.igs); + const requestedSize = getRequestedSize(adUnit); + if (requestedSize) { + deepSetValue(adUnit, 'ortb2Imp.ext.paapi.requestedSize', requestedSize); + } + adUnit.bids.forEach(bidReq => { + if (!getFledgeConfig(bidReq.bidder).enabled) { + deepSetValue(bidReq, 'ortb2Imp.ext.ae', 0); + bidReq.ortb2Imp.ext.igs = {ae: 0, biddable: 0}; + } + }) + } + }) + } + next(adUnits, ...args); +} + export function markForFledge(next, bidderRequests) { if (isFledgeSupported()) { bidderRequests.forEach((bidderReq) => { - const {enabled, ae} = getFledgeConfig(bidderReq.bidderCode); + const {enabled} = getFledgeConfig(bidderReq.bidderCode); Object.assign(bidderReq, { paapi: { enabled, componentSeller: !!moduleConfig.componentSeller?.auctionConfig } }); - bidderReq.bids.forEach(bidReq => { - // https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/Protected%20Audience%20Support.md - const igsAe = bidReq.ortb2Imp?.ext?.igs != null - ? bidReq.ortb2Imp.ext.igs.ae || 1 - : null; - const extAe = bidReq.ortb2Imp?.ext?.ae; - if (igsAe !== extAe && igsAe != null && extAe != null) { - logWarn(MODULE, `Bid request defines conflicting ortb2Imp.ext.ae and ortb2Imp.ext.igs, using the latter`, bidReq); - } - const bidAe = igsAe ?? extAe ?? ae; - if (bidAe) { - deepSetValue(bidReq, 'ortb2Imp.ext.ae', bidAe); - bidReq.ortb2Imp.ext.igs = Object.assign({ - ae: bidAe, - biddable: 1 - }, bidReq.ortb2Imp.ext.igs); - const requestedSize = getRequestedSize(bidReq); - if (requestedSize) { - deepSetValue(bidReq, 'ortb2Imp.ext.paapi.requestedSize', requestedSize); - } - } - }); }); } next(bidderRequests); @@ -378,18 +391,6 @@ export function setImpExtAe(imp, bidRequest, context) { registerOrtbProcessor({type: IMP, name: 'impExtAe', fn: setImpExtAe}); -function paapiResponseParser(configs, response, context) { - configs.forEach((config) => { - const impCtx = context.impContext[config.impid]; - if (!impCtx?.imp?.ext?.ae) { - logWarn(MODULE, 'Received auction configuration for an impression that was not in the request or did not ask for it', config, impCtx?.imp); - } else { - impCtx.paapiConfigs = impCtx.paapiConfigs || []; - impCtx.paapiConfigs.push(config); - } - }); -} - export function parseExtIgi(response, ortbResponse, context) { paapiResponseParser( (ortbResponse.ext?.igi || []).flatMap(igi => { @@ -411,6 +412,18 @@ export function parseExtIgi(response, ortbResponse, context) { ) } +function paapiResponseParser(configs, response, context) { + configs.forEach((config) => { + const impCtx = context.impContext[config.impid]; + if (!impCtx?.imp?.ext?.ae) { + logWarn(MODULE, 'Received auction configuration for an impression that was not in the request or did not ask for it', config, impCtx?.imp); + } else { + impCtx.paapiConfigs = impCtx.paapiConfigs || []; + impCtx.paapiConfigs.push(config); + } + }); +} + // to make it easier to share code between the PBS adapter and adapters whose backend is PBS, break up // fledge response processing in two steps: first aggregate all the auction configs by their imp... diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index c0da66c031b..edae21e97a7 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -515,7 +515,9 @@ export function PrebidServer() { } }, onFledge: (params) => { - addPaapiConfig({auctionId: bidRequests[0].auctionId, ...params}, {config: params.config}); + config.runWithBidder(params.bidder, () => { + addPaapiConfig({auctionId: bidRequests[0].auctionId, ...params}, {config: params.config}); + }) } }) } diff --git a/modules/prebidServerBidAdapter/ortbConverter.js b/modules/prebidServerBidAdapter/ortbConverter.js index bb033271b3c..242c65c7dfa 100644 --- a/modules/prebidServerBidAdapter/ortbConverter.js +++ b/modules/prebidServerBidAdapter/ortbConverter.js @@ -241,6 +241,7 @@ const PBS_CONVERTER = ortbConverter({ adUnitCode: impCtx.adUnit.code, ortb2: bidderReq?.ortb2, ortb2Imp: bidReq?.ortb2Imp, + bidder: cfg.bidder, config: cfg.config }; })); diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js index 77c8a6d7dda..cc839307c8e 100644 --- a/test/spec/modules/paapi_spec.js +++ b/test/spec/modules/paapi_spec.js @@ -7,7 +7,7 @@ import {hook} from '../../../src/hook.js'; import 'modules/appnexusBidAdapter.js'; import 'modules/rubiconBidAdapter.js'; import { - addPaapiConfigHook, + addPaapiConfigHook, addPaapiData, buyersToAuctionConfigs, getPAAPIConfig, getPAAPISize, @@ -652,110 +652,171 @@ describe('paapi module', () => { config.resetConfig(); }); - function mark() { - return Object.fromEntries( - adapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() { - }, - [] - ).map(b => [b.bidderCode, b]) - ); - } - - function expectFledgeFlags(...enableFlags) { - const bidRequests = mark(); - expect(bidRequests.appnexus.paapi?.enabled).to.eql(enableFlags[0].enabled); - bidRequests.appnexus.bids.forEach(bid => expect(bid.ortb2Imp.ext.ae).to.eql(enableFlags[0].ae)); + describe('makeBidRequests', () => { + function mark() { + return Object.fromEntries( + adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() { + }, + [] + ).map(b => [b.bidderCode, b]) + ); + } - expect(bidRequests.rubicon.paapi?.enabled).to.eql(enableFlags[1].enabled); - bidRequests.rubicon.bids.forEach(bid => expect(bid.ortb2Imp?.ext?.ae).to.eql(enableFlags[1].ae)); + function expectFledgeFlags(...enableFlags) { + const bidRequests = mark(); + expect(bidRequests.appnexus.paapi?.enabled).to.eql(enableFlags[0].enabled); + bidRequests.appnexus.bids.forEach(bid => expect(bid.ortb2Imp.ext.ae).to.eql(enableFlags[0].ae)); - Object.values(bidRequests).flatMap(req => req.bids).forEach(bid => { - if (bid.ortb2Imp?.ext?.ae) { - sinon.assert.match(bid.ortb2Imp.ext.igs, { - ae: bid.ortb2Imp.ext.ae, - biddable: 1 - }); - } - }); - } + expect(bidRequests.rubicon.paapi?.enabled).to.eql(enableFlags[1].enabled); + bidRequests.rubicon.bids.forEach(bid => expect(bid.ortb2Imp?.ext?.ae).to.eql(enableFlags[1].ae)); - describe('with setConfig()', () => { - it('should set paapi.enabled correctly per bidder', function () { - config.setConfig({ - bidderSequence: 'fixed', - paapi: { - enabled: true, - bidders: ['appnexus'], - defaultForSlots: 1, + Object.values(bidRequests).flatMap(req => req.bids).forEach(bid => { + if (bid.ortb2Imp?.ext?.ae) { + sinon.assert.match(bid.ortb2Imp.ext.igs, { + ae: bid.ortb2Imp.ext.ae, + biddable: 1 + }); } }); - expectFledgeFlags({enabled: true, ae: 1}, {enabled: false, ae: undefined}); - }); + } - it('should set paapi.enabled correctly for all bidders', function () { - config.setConfig({ - bidderSequence: 'fixed', - paapi: { - enabled: true, - defaultForSlots: 1, - } + describe('with setConfig()', () => { + it('should set paapi.enabled correctly per bidder', function () { + config.setConfig({ + bidderSequence: 'fixed', + paapi: { + enabled: true, + bidders: ['appnexus'], + defaultForSlots: 1, + } + }); + expectFledgeFlags({enabled: true, ae: 1}, {enabled: false, ae: 0}); }); - expectFledgeFlags({enabled: true, ae: 1}, {enabled: true, ae: 1}); - }); - Object.entries({ - 'not set': { - cfg: {}, - componentSeller: false - }, - 'set': { - cfg: { - componentSeller: { - auctionConfig: { - decisionLogicURL: 'publisher.example' - } - } - }, - componentSeller: true - } - }).forEach(([t, {cfg, componentSeller}]) => { - it(`should set request paapi.componentSeller = ${componentSeller} when config componentSeller is ${t}`, () => { + it('should set paapi.enabled correctly for all bidders', function () { config.setConfig({ + bidderSequence: 'fixed', paapi: { enabled: true, defaultForSlots: 1, - ...cfg } }); - Object.values(mark()).forEach(br => expect(br.paapi?.componentSeller).to.eql(componentSeller)); + expectFledgeFlags({enabled: true, ae: 1}, {enabled: true, ae: 1}); + }); + + Object.entries({ + 'not set': { + cfg: {}, + componentSeller: false + }, + 'set': { + cfg: { + componentSeller: { + auctionConfig: { + decisionLogicURL: 'publisher.example' + } + } + }, + componentSeller: true + } + }).forEach(([t, {cfg, componentSeller}]) => { + it(`should set request paapi.componentSeller = ${componentSeller} when config componentSeller is ${t}`, () => { + config.setConfig({ + paapi: { + enabled: true, + defaultForSlots: 1, + ...cfg + } + }); + Object.values(mark()).forEach(br => expect(br.paapi?.componentSeller).to.eql(componentSeller)); + }); }); }); + }); + describe('addPaapiData', () => { + function getEnrichedAdUnits() { + const next = sinon.stub(); + addPaapiData(next, adUnits); + sinon.assert.calledWith(next, adUnits); + return adUnits; + } + + function getImpExt() { + const next = sinon.stub(); + addPaapiData(next, adUnits); + sinon.assert.calledWith(next, adUnits); + return { + global: adUnits[0].ortb2Imp?.ext, + ...Object.fromEntries(adUnits[0].bids.map(bid => [bid.bidder, bid.ortb2Imp?.ext])) + } + } it('should not override pub-defined ext.ae', () => { config.setConfig({ - bidderSequence: 'fixed', paapi: { enabled: true, defaultForSlots: 1, } }); Object.assign(adUnits[0], {ortb2Imp: {ext: {ae: 0}}}); - expectFledgeFlags({enabled: true, ae: 0}, {enabled: true, ae: 0}); + sinon.assert.match(getImpExt(), { + global: { + ae: 0, + }, + rubicon: undefined, + appnexus: undefined + }); }); + it('should override per-bidder when excluded via paapi.bidders', () => { + config.setConfig({ + paapi: { + enabled: true, + defaultForSlots: 1, + bidders: ['rubicon'] + } + }) + sinon.assert.match(getImpExt(), { + global: { + ae: 1, + igs: { + ae: 1, + biddable: 1 + } + }, + rubicon: undefined, + appnexus: { + ae: 0, + igs: { + ae: 0, + biddable: 0 + } + } + }) + }) + it('should populate ext.igs when request has ext.ae', () => { config.setConfig({ - bidderSequence: 'fixed', paapi: { enabled: true } }); Object.assign(adUnits[0], {ortb2Imp: {ext: {ae: 3}}}); - expectFledgeFlags({enabled: true, ae: 3}, {enabled: true, ae: 3}); + sinon.assert.match(getImpExt(), { + global: { + ae: 3, + igs: { + ae: 3, + biddable: 1 + } + }, + rubicon: undefined, + appnexus: undefined, + }); }); it('should not override pub-defined ext.igs', () => { @@ -765,16 +826,17 @@ describe('paapi module', () => { } }); Object.assign(adUnits[0], {ortb2Imp: {ext: {ae: 1, igs: {biddable: 0}}}}); - const bidReqs = mark(); - Object.values(bidReqs).flatMap(req => req.bids).forEach(bid => { - sinon.assert.match(bid.ortb2Imp.ext, { + sinon.assert.match(getImpExt(), { + global: { ae: 1, igs: { ae: 1, biddable: 0 } - }); - }); + }, + rubicon: undefined, + appnexus: undefined + }) }); it('should fill ext.ae from ext.igs, if defined', () => { @@ -784,60 +846,64 @@ describe('paapi module', () => { } }); Object.assign(adUnits[0], {ortb2Imp: {ext: {igs: {}}}}); - expectFledgeFlags({enabled: true, ae: 1}, {enabled: true, ae: 1}); + sinon.assert.match(getImpExt(), { + global: { + ae: 1, + igs: { + ae: 1, + biddable: 1 + } + }, + appnexus: undefined, + rubicon: undefined + }) }); - }); - describe('ortb2Imp.ext.paapi.requestedSize', () => { - beforeEach(() => { - config.setConfig({ - paapi: { - enabled: true, - defaultForSlots: 1, - } + describe('ortb2Imp.ext.paapi.requestedSize', () => { + beforeEach(() => { + config.setConfig({ + paapi: { + 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, { + it('should default to value returned by getPAAPISize', () => { + getPAAPISizeStub.returns([123, 321]); + expect(getImpExt().global.paapi).to.eql({ 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' + 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, { + }; + expect(getImpExt().global.paapi).to.eql({ requestedSize: { width: '123px', height: '321px' } - }); + }) + sinon.assert.notCalled(getPAAPISizeStub); }); - 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; + it('should not be set if adUnit has no banner sizes', () => { + adUnits[0].mediaTypes = { + video: {} + }; + expect(getImpExt().global?.paapi?.requestedSize).to.not.exist; }); }); }); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index d44a67d2acc..a6d91a6309b 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -825,7 +825,7 @@ describe('S2S Adapter', function () { }) }) }) - + it('should set customHeaders correctly when publisher has provided it', () => { let configWithCustomHeaders = utils.deepClone(CONFIG); configWithCustomHeaders.customHeaders = { customHeader1: 'customHeader1Value' }; @@ -3578,18 +3578,32 @@ describe('S2S Adapter', function () { sinon.assert.calledWith(fledgeStub, sinon.match({auctionId, adUnitCode: AU, ortb2: undefined, ortb2Imp: undefined}), sinon.match({config: {id: 2}})) } - it('calls addComponentAuction alongside addBidResponse', function () { + it('calls addPaapiConfig alongside addBidResponse', function () { adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(mergeDeep({}, RESPONSE_OPENRTB, FLEDGE_RESP))); expect(addBidResponse.called).to.be.true; expectFledgeCalls(); }); - it('calls addComponentAuction when there is no bid in the response', () => { + it('calls addPaapiConfig when there is no bid in the response', () => { adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(FLEDGE_RESP)); expect(addBidResponse.called).to.be.false; expectFledgeCalls(); + }); + + it('wraps call in runWithBidder', () => { + let fail = false; + fledgeStub.callsFake(({bidder}) => { + try { + expect(bidder).to.exist.and.to.eql(config.getCurrentBidder()); + } catch (e) { + fail = true; + } + }); + adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(FLEDGE_RESP)); + expect(fail).to.be.false; }) }); });