diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index edb0b4c5f4a..a0a5f3cbfd8 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -481,6 +481,15 @@ const OPEN_RTB_PROTOCOL = { const imp = { id: adUnit.code, ext, secure: _s2sConfig.secure }; + /** + * Prebid AdSlot + * @type {(string|undefined)} + */ + const pbAdSlot = utils.deepAccess(adUnit, 'fpd.context.pbAdSlot'); + if (typeof pbAdSlot === 'string' && pbAdSlot) { + utils.deepSetValue(imp, 'ext.context.data.adslot', pbAdSlot); + } + Object.assign(imp, mediaTypes); if (imp.banner || imp.video || imp.native) { diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index f42ed0be5b6..b76ca9e375d 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -267,6 +267,15 @@ export const spec = { utils.deepSetValue(data, 'source.ext.schain', bidRequest.schain); } + /** + * Prebid AdSlot + * @type {(string|undefined)} + */ + const pbAdSlot = utils.deepAccess(bidRequest, 'fpd.context.pbAdSlot'); + if (typeof pbAdSlot === 'string' && pbAdSlot) { + utils.deepSetValue(data.imp[0].ext, 'context.data.adslot', pbAdSlot); + } + return { method: 'POST', url: VIDEO_ENDPOINT, @@ -489,6 +498,15 @@ export const spec = { }); } + /** + * Prebid AdSlot + * @type {(string|undefined)} + */ + const pbAdSlot = utils.deepAccess(bidRequest, 'fpd.context.pbAdSlot'); + if (typeof pbAdSlot === 'string' && pbAdSlot) { + data['tg_i.dfp_ad_unit_code'] = pbAdSlot.replace(/^\/+/, ''); + } + // digitrust properties const digitrustParams = _getDigiTrustQueryParams(bidRequest, 'FASTLANE'); Object.assign(data, digitrustParams); diff --git a/src/adapterManager.js b/src/adapterManager.js index f93d6cf5072..c9802e0f903 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -66,6 +66,7 @@ function getBids({bidderCode, auctionId, bidderRequestId, adUnits, labels, src}) } bid = Object.assign({}, bid, getDefinedParams(adUnit, [ + 'fpd', 'mediaType', 'renderer' ])); @@ -174,6 +175,12 @@ export let uspDataHandler = { }; adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, auctionId, cbTimeout, labels) { + /** + * emit and pass adunits for external modification + * @see {@link https://github.com/prebid/Prebid.js/issues/4149|Issue} + */ + events.emit(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, adUnits); + let bidRequests = []; let bidderCodes = getBidderCodes(adUnits); diff --git a/src/constants.json b/src/constants.json index d993a6531ad..0b6b144a336 100644 --- a/src/constants.json +++ b/src/constants.json @@ -33,6 +33,7 @@ "BID_WON": "bidWon", "BIDDER_DONE": "bidderDone", "SET_TARGETING": "setTargeting", + "BEFORE_REQUEST_BIDS": "beforeRequestBids", "REQUEST_BIDS": "requestBids", "ADD_AD_UNITS": "addAdUnits", "AD_RENDER_FAILED" : "adRenderFailed" diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 5faabd120b2..538e1da9aeb 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -1288,6 +1288,80 @@ describe('S2S Adapter', function () { const parsedRequestBody = JSON.parse(server.requests[0].requestBody); expect(parsedRequestBody.source.ext.schain).to.deep.equal(schainObject); }) + + describe('pbAdSlot config', function () { + it('should not send \"imp.ext.context.data.adslot\" if \"fpd.context\" is undefined', function () { + const ortb2Config = utils.deepClone(CONFIG); + ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; + const consentConfig = { s2sConfig: ortb2Config }; + config.setConfig(consentConfig); + const bidRequest = utils.deepClone(REQUEST); + + adapter.callBids(bidRequest, BID_REQUESTS, addBidResponse, done, ajax); + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + + expect(parsedRequestBody.imp).to.be.a('array'); + expect(parsedRequestBody.imp[0]).to.be.a('object'); + expect(parsedRequestBody.imp[0]).to.not.have.deep.nested.property('ext.context.data.adslot'); + }); + + it('should not send \"imp.ext.context.data.adslot\" if \"fpd.context.pbAdSlot\" is undefined', function () { + const ortb2Config = utils.deepClone(CONFIG); + ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; + const consentConfig = { s2sConfig: ortb2Config }; + config.setConfig(consentConfig); + const bidRequest = utils.deepClone(REQUEST); + bidRequest.ad_units[0].fpd = {}; + + adapter.callBids(bidRequest, BID_REQUESTS, addBidResponse, done, ajax); + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + + expect(parsedRequestBody.imp).to.be.a('array'); + expect(parsedRequestBody.imp[0]).to.be.a('object'); + expect(parsedRequestBody.imp[0]).to.not.have.deep.nested.property('ext.context.data.adslot'); + }); + + it('should not send \"imp.ext.context.data.adslot\" if \"fpd.context.pbAdSlot\" is empty string', function () { + const ortb2Config = utils.deepClone(CONFIG); + ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; + const consentConfig = { s2sConfig: ortb2Config }; + config.setConfig(consentConfig); + const bidRequest = utils.deepClone(REQUEST); + bidRequest.ad_units[0].fpd = { + context: { + pbAdSlot: '' + } + }; + + adapter.callBids(bidRequest, BID_REQUESTS, addBidResponse, done, ajax); + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + + expect(parsedRequestBody.imp).to.be.a('array'); + expect(parsedRequestBody.imp[0]).to.be.a('object'); + expect(parsedRequestBody.imp[0]).to.not.have.deep.nested.property('ext.context.data.adslot'); + }); + + it('should send \"imp.ext.context.data.adslot\" if \"fpd.context.pbAdSlot\" value is a non-empty string', function () { + const ortb2Config = utils.deepClone(CONFIG); + ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; + const consentConfig = { s2sConfig: ortb2Config }; + config.setConfig(consentConfig); + const bidRequest = utils.deepClone(REQUEST); + bidRequest.ad_units[0].fpd = { + context: { + pbAdSlot: '/a/b/c' + } + }; + + adapter.callBids(bidRequest, BID_REQUESTS, addBidResponse, done, ajax); + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + + expect(parsedRequestBody.imp).to.be.a('array'); + expect(parsedRequestBody.imp[0]).to.be.a('object'); + expect(parsedRequestBody.imp[0]).to.have.deep.nested.property('ext.context.data.adslot'); + expect(parsedRequestBody.imp[0].ext.context.data.adslot).to.equal('/a/b/c'); + }); + }); }); describe('response handler', function () { diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index d0c0773ff45..5a9e2968309 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -1260,6 +1260,77 @@ describe('the rubicon adapter', function () { }); }); }) + + describe('Prebid AdSlot', function () { + beforeEach(function () { + // enforce that the bid at 0 does not have a 'context' property + if (bidderRequest.bids[0].hasOwnProperty('fpd')) { + delete bidderRequest.bids[0].fpd; + } + }); + + it('should not send \"tg_i.dfp_ad_unit_code\" if \"fpd.context\" object is not valid', function () { + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + const data = parseQuery(request.data); + + expect(data).to.be.an('Object'); + expect(data).to.not.have.property('tg_i.dfp_ad_unit_code'); + }); + + it('should not send \"tg_i.dfp_ad_unit_code\" if \"fpd.context.pbAdSlot\" is undefined', function () { + bidderRequest.bids[0].fpd = {}; + + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + const data = parseQuery(request.data); + + expect(data).to.be.an('Object'); + expect(data).to.not.have.property('tg_i.dfp_ad_unit_code'); + }); + + it('should not send \"tg_i.dfp_ad_unit_code\" if \"fpd.context.pbAdSlot\" value is an empty string', function () { + bidderRequest.bids[0].fpd = { + context: { + pbAdSlot: '' + } + }; + + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + const data = parseQuery(request.data); + + expect(data).to.be.an('Object'); + expect(data).to.not.have.property('tg_i.dfp_ad_unit_code'); + }); + + it('should send \"tg_i.dfp_ad_unit_code\" if \"fpd.context.pbAdSlot\" value is a valid string', function () { + bidderRequest.bids[0].fpd = { + context: { + pbAdSlot: 'abc' + } + } + + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + const data = parseQuery(request.data); + + expect(data).to.be.an('Object'); + expect(data).to.have.property('tg_i.dfp_ad_unit_code'); + expect(data['tg_i.dfp_ad_unit_code']).to.equal('abc'); + }); + + it('should send \"tg_i.dfp_ad_unit_code\" if \"fpd.context.pbAdSlot\" value is a valid string, but all leading slash characters should be removed', function () { + bidderRequest.bids[0].fpd = { + context: { + pbAdSlot: '/a/b/c' + } + }; + + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + const data = parseQuery(request.data); + + expect(data).to.be.an('Object'); + expect(data).to.have.property('tg_i.dfp_ad_unit_code'); + expect(data['tg_i.dfp_ad_unit_code']).to.equal('a/b/c'); + }); + }); }); describe('for video requests', function () { @@ -1568,6 +1639,38 @@ describe('the rubicon adapter', function () { const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(request.data.regs.coppa).to.equal(1); }); + + it('should include pbAdSlot in bid request', function () { + createVideoBidderRequest(); + bidderRequest.bids[0].fpd = { + context: { + pbAdSlot: '1234567890' + } + }; + + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); + + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].ext.context.data.adslot).to.equal('1234567890'); + }); + }); + + it('should include pbAdSlot in bid request', function () { + createVideoBidderRequest(); + bidderRequest.bids[0].fpd = { + context: { + pbAdSlot: '1234567890' + } + }; + + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); + + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].ext.context.data.adslot).to.equal('1234567890'); }); describe('combineSlotUrlParams', function () { diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 094220ea521..7a048c3f967 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -1966,6 +1966,228 @@ describe('Unit: Prebid Module', function () { assert.ok(spyEventsOn.calledWith('bidWon', Function)); events.on.restore(); }); + + describe('beforeRequestBids', function () { + let bidRequestedHandler; + let beforeRequestBidsHandler; + let bidsBackHandler = function bidsBackHandler() {}; + + let bidsBackSpy; + let bidRequestedSpy; + let beforeRequestBidsSpy; + + beforeEach(function () { + resetAuction(); + bidsBackSpy = sinon.spy(bidsBackHandler); + googletag.pubads().setSlots(createSlotArrayScenario2()); + }); + + afterEach(function () { + bidsBackSpy.resetHistory(); + + if (bidRequestedSpy) { + $$PREBID_GLOBAL$$.offEvent('bidRequested', bidRequestedSpy); + bidRequestedSpy.resetHistory(); + } + + if (beforeRequestBidsSpy) { + $$PREBID_GLOBAL$$.offEvent('beforeRequestBids', beforeRequestBidsSpy); + beforeRequestBidsSpy.resetHistory(); + } + }); + + it('should allow creation of a fpd.context.pbAdSlot property on adUnits from inside the event handler', function () { + // verify adUnits passed to handler then alter the adUnits + beforeRequestBidsHandler = function beforeRequestBidsHandler(beforeRequestBidsAdUnits) { + expect(beforeRequestBidsAdUnits).to.be.a('array'); + expect(beforeRequestBidsAdUnits).to.have.lengthOf(1); + expect(beforeRequestBidsAdUnits[0]).to.be.a('object'); + // adUnit should not contain a context property yet + expect(beforeRequestBidsAdUnits[0]).to.not.have.property('fpd') + // alter the adUnit by adding the property for context.pbAdSlot + beforeRequestBidsAdUnits[0].fpd = { + context: { + pbAdSlot: '/19968336/header-bid-tag-pbadslot-0' + } + }; + }; + beforeRequestBidsSpy = sinon.spy(beforeRequestBidsHandler); + + // use this handler to verify if the adUnits alterations were applied successfully by the beforeRequestBids handler + bidRequestedHandler = function bidRequestedHandler(bidRequest) { + expect(bidRequest).to.be.a('object'); + expect(bidRequest).to.have.property('bids'); + expect(bidRequest.bids).to.be.a('array'); + expect(bidRequest.bids).to.have.lengthOf(1); + const bid = bidRequest['bids'][0]; + expect(bid).to.be.a('object'); + expect(bid).to.have.property('fpd'); + expect(bid.fpd).to.be.a('object'); + expect(bid.fpd).to.have.property('context'); + expect(bid.fpd.context).to.be.a('object'); + expect(bid.fpd.context).to.have.property('pbAdSlot'); + expect(bid.fpd.context.pbAdSlot).to.equal('/19968336/header-bid-tag-pbadslot-0'); + }; + bidRequestedSpy = sinon.spy(bidRequestedHandler); + + $$PREBID_GLOBAL$$.onEvent('beforeRequestBids', beforeRequestBidsSpy); + $$PREBID_GLOBAL$$.onEvent('bidRequested', bidRequestedSpy); + $$PREBID_GLOBAL$$.requestBids({ + adUnits: [{ + code: '/19968336/header-bid-tag-0', + mediaTypes: { + banner: { + sizes: [[750, 350]] + } + }, + bids: [{ + bidder: 'appnexus', + params: { + placementId: 13122370 + } + }] + }], + bidsBackHandler: bidsBackSpy + }); + + sinon.assert.calledOnce(beforeRequestBidsSpy); + sinon.assert.calledOnce(bidRequestedSpy); + }); + + it('should allow creation of a fpd.context.pbAdSlot property on adUnits from inside the event handler', function () { + // verify adUnits passed to handler then alter the adUnits + beforeRequestBidsHandler = function beforeRequestBidsHandler(beforeRequestBidsAdUnits) { + expect(beforeRequestBidsAdUnits).to.be.a('array'); + expect(beforeRequestBidsAdUnits).to.have.lengthOf(2); + expect(beforeRequestBidsAdUnits[0]).to.be.a('object'); + expect(beforeRequestBidsAdUnits[1]).to.be.a('object'); + // adUnit should not contain a context property yet + expect(beforeRequestBidsAdUnits[0]).to.not.have.property('fpd'); + expect(beforeRequestBidsAdUnits[1]).to.not.have.property('fpd'); + // alter the adUnit by adding the property for context.pbAdSlot + beforeRequestBidsAdUnits[0].fpd = { + context: { + pbAdSlot: '/19968336/header-bid-tag-pbadslot-0' + } + }; + beforeRequestBidsAdUnits[1].fpd = { + context: { + pbAdSlot: '/19968336/header-bid-tag-pbadslot-1' + } + }; + }; + beforeRequestBidsSpy = sinon.spy(beforeRequestBidsHandler); + + // use this handler to verify if the adUnits alterations were applied successfully by the beforeRequestBids handler + bidRequestedHandler = function bidRequestedHandler(bidRequest) { + expect(bidRequest).to.be.a('object'); + expect(bidRequest).to.have.property('bids'); + expect(bidRequest.bids).to.be.a('array'); + expect(bidRequest.bids).to.have.lengthOf(2); + const bid0 = bidRequest['bids'][0]; + expect(bid0).to.be.a('object'); + expect(bid0).to.have.property('fpd'); + expect(bid0.fpd).to.be.a('object'); + expect(bid0.fpd).to.have.property('context'); + expect(bid0.fpd.context).to.be.a('object'); + expect(bid0.fpd.context).to.have.property('pbAdSlot'); + expect(bid0.fpd.context.pbAdSlot).to.equal('/19968336/header-bid-tag-pbadslot-0'); + + const bid1 = bidRequest['bids'][1]; + expect(bid1).to.be.a('object'); + expect(bid1).to.have.property('fpd'); + expect(bid1.fpd).to.be.a('object'); + expect(bid1.fpd).to.have.property('context'); + expect(bid1.fpd.context).to.be.a('object'); + expect(bid1.fpd.context).to.have.property('pbAdSlot'); + expect(bid1.fpd.context.pbAdSlot).to.equal('/19968336/header-bid-tag-pbadslot-1'); + }; + bidRequestedSpy = sinon.spy(bidRequestedHandler); + + $$PREBID_GLOBAL$$.onEvent('beforeRequestBids', beforeRequestBidsSpy); + $$PREBID_GLOBAL$$.onEvent('bidRequested', bidRequestedSpy); + $$PREBID_GLOBAL$$.requestBids({ + adUnits: [{ + code: '/19968336/header-bid-tag-0', + mediaTypes: { + banner: { + sizes: [[750, 350]] + } + }, + bids: [{ + bidder: 'appnexus', + params: { + placementId: 13122370 + } + }] + }, { + code: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [[750, 350]] + } + }, + bids: [{ + bidder: 'appnexus', + params: { + placementId: 14122380 + } + }] + }], + bidsBackHandler: bidsBackSpy + }); + + sinon.assert.calledOnce(beforeRequestBidsSpy); + sinon.assert.calledOnce(bidRequestedSpy); + }); + + it('should not create a context property on adUnits if not added by handler', function () { + // verify adUnits passed to handler then alter the adUnits + beforeRequestBidsHandler = function beforeRequestBidsHandler(beforeRequestBidsAdUnits) { + expect(beforeRequestBidsAdUnits).to.be.a('array'); + expect(beforeRequestBidsAdUnits).to.have.lengthOf(1); + expect(beforeRequestBidsAdUnits[0]).to.be.a('object'); + // adUnit should not contain a context property yet + expect(beforeRequestBidsAdUnits[0]).to.not.have.property('context') + }; + beforeRequestBidsSpy = sinon.spy(beforeRequestBidsHandler); + + // use this handler to verify if the adUnits alterations were applied successfully by the beforeRequestBids handler + bidRequestedHandler = function bidRequestedHandler(bidRequest) { + expect(bidRequest).to.be.a('object'); + expect(bidRequest).to.have.property('bids'); + expect(bidRequest.bids).to.be.a('array'); + expect(bidRequest.bids).to.have.lengthOf(1); + const bid = bidRequest['bids'][0]; + expect(bid).to.be.a('object'); + expect(bid).to.not.have.property('context'); + }; + bidRequestedSpy = sinon.spy(bidRequestedHandler); + + $$PREBID_GLOBAL$$.onEvent('beforeRequestBids', beforeRequestBidsSpy); + $$PREBID_GLOBAL$$.onEvent('bidRequested', bidRequestedSpy); + $$PREBID_GLOBAL$$.requestBids({ + adUnits: [{ + code: '/19968336/header-bid-tag-0', + mediaTypes: { + banner: { + sizes: [[750, 350]] + } + }, + bids: [{ + bidder: 'appnexus', + params: { + placementId: 13122370 + } + }] + }], + bidsBackHandler: bidsBackSpy + }); + + sinon.assert.calledOnce(beforeRequestBidsSpy); + sinon.assert.calledOnce(bidRequestedSpy); + }); + }); }); describe('offEvent', function () {