diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index a0a5f3cbfd8..8c32ab634d9 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -11,6 +11,7 @@ import events from '../../src/events'; import includes from 'core-js/library/fn/array/includes'; import { S2S_VENDORS } from './config.js'; import { ajax } from '../../src/ajax'; +import find from 'core-js/library/fn/array/find'; const getConfig = config.getConfig; @@ -492,6 +493,12 @@ const OPEN_RTB_PROTOCOL = { Object.assign(imp, mediaTypes); + // if storedAuctionResponse has been set, pass SRID + const storedAuctionResponseBid = find(bidRequests[0].bids, bid => (bid.adUnitCode === adUnit.code && typeof bid.storedAuctionResponse === 'number')); + if (storedAuctionResponseBid) { + utils.deepSetValue(imp, 'ext.prebid.storedauctionresponse.id', storedAuctionResponseBid.storedAuctionResponse.toString()); + } + if (imp.banner || imp.video || imp.native) { imps.push(imp); } diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index b76ca9e375d..4b53cf78e0c 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -276,6 +276,11 @@ export const spec = { utils.deepSetValue(data.imp[0].ext, 'context.data.adslot', pbAdSlot); } + // if storedAuctionResponse has been set, pass SRID + if (typeof bidRequest.storedAuctionResponse === 'number') { + utils.deepSetValue(data.imp[0], 'ext.prebid.storedauctionresponse.id', bidRequest.storedAuctionResponse.toString()); + } + return { method: 'POST', url: VIDEO_ENDPOINT, diff --git a/src/adapterManager.js b/src/adapterManager.js index c9802e0f903..dfed67eaed4 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -68,7 +68,8 @@ function getBids({bidderCode, auctionId, bidderRequestId, adUnits, labels, src}) bid = Object.assign({}, bid, getDefinedParams(adUnit, [ 'fpd', 'mediaType', - 'renderer' + 'renderer', + 'storedAuctionResponse' ])); let { diff --git a/src/auction.js b/src/auction.js index fe1b70085e9..43f517fd84c 100644 --- a/src/auction.js +++ b/src/auction.js @@ -120,7 +120,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a let _winningBids = []; let _timelyBidders = new Set(); - function addBidRequests(bidderRequests) { _bidderRequests = _bidderRequests.concat(bidderRequests) }; + function addBidRequests(bidderRequests) { _bidderRequests = _bidderRequests.concat(bidderRequests); } function addBidReceived(bidsReceived) { _bidsReceived = _bidsReceived.concat(bidsReceived); } function addNoBid(noBid) { _noBids = _noBids.concat(noBid); } @@ -213,62 +213,73 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a let bidRequests = adapterManager.makeBidRequests(_adUnits, _auctionStart, _auctionId, _timeout, _labels); utils.logInfo(`Bids Requested for Auction with id: ${_auctionId}`, bidRequests); - bidRequests.forEach(bidRequest => { - addBidRequests(bidRequest); - }); - - let requests = {}; if (bidRequests.length < 1) { utils.logWarn('No valid bid requests returned for auction'); auctionDone(); } else { - let call = { - bidRequests, - run: () => { - startAuctionTimer(); - - _auctionStatus = AUCTION_IN_PROGRESS; - - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, getProperties()); - - let callbacks = auctionCallbacks(auctionDone, this); - adapterManager.callBids(_adUnits, bidRequests, function(...args) { - addBidResponse.apply({ - dispatch: callbacks.addBidResponse, - bidderRequest: this - }, args) - }, callbacks.adapterDone, { - request(source, origin) { - increment(outstandingRequests, origin); - increment(requests, source); - - if (!sourceInfo[source]) { - sourceInfo[source] = { - SRA: true, - origin - }; - } - if (requests[source] > 1) { - sourceInfo[source].SRA = false; - } - }, - done(origin) { - outstandingRequests[origin]--; - if (queuedCalls[0]) { - if (runIfOriginHasCapacity(queuedCalls[0])) { - queuedCalls.shift(); - } + addBidderRequests.call({ + dispatch: addBidderRequestsCallback, + context: this + }, bidRequests); + } + } + + /** + * callback executed after addBidderRequests completes + * @param {BidRequest[]} bidRequests + */ + function addBidderRequestsCallback(bidRequests) { + bidRequests.forEach(bidRequest => { + addBidRequests(bidRequest); + }); + + let requests = {}; + let call = { + bidRequests, + run: () => { + startAuctionTimer(); + + _auctionStatus = AUCTION_IN_PROGRESS; + + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, getProperties()); + + let callbacks = auctionCallbacks(auctionDone, this); + adapterManager.callBids(_adUnits, bidRequests, function(...args) { + addBidResponse.apply({ + dispatch: callbacks.addBidResponse, + bidderRequest: this + }, args) + }, callbacks.adapterDone, { + request(source, origin) { + increment(outstandingRequests, origin); + increment(requests, source); + + if (!sourceInfo[source]) { + sourceInfo[source] = { + SRA: true, + origin + }; + } + if (requests[source] > 1) { + sourceInfo[source].SRA = false; + } + }, + done(origin) { + outstandingRequests[origin]--; + if (queuedCalls[0]) { + if (runIfOriginHasCapacity(queuedCalls[0])) { + queuedCalls.shift(); } } - }, _timeout, onTimelyResponse); - } - }; - - if (!runIfOriginHasCapacity(call)) { - utils.logWarn('queueing auction due to limited endpoint capacity'); - queuedCalls.push(call); + } + }, _timeout, onTimelyResponse); } + }; + + if (!runIfOriginHasCapacity(call)) { + utils.logWarn('queueing auction due to limited endpoint capacity'); + queuedCalls.push(call); } function runIfOriginHasCapacity(call) { @@ -344,6 +355,10 @@ export const addBidResponse = hook('async', function(adUnitCode, bid) { this.dispatch.call(this.bidderRequest, adUnitCode, bid); }, 'addBidResponse'); +export const addBidderRequests = hook('sync', function(bidderRequests) { + this.dispatch.call(this.context, bidderRequests); +}, 'addBidderRequests'); + export const bidsBackCallback = hook('async', function (adUnits, callback) { if (callback) { callback(); diff --git a/src/debugging.js b/src/debugging.js index 98ff67f4707..786ede6fe4d 100644 --- a/src/debugging.js +++ b/src/debugging.js @@ -1,11 +1,12 @@ import { config } from './config'; import { logMessage as utilsLogMessage, logWarn as utilsLogWarn } from './utils'; -import { addBidResponse } from './auction'; +import { addBidderRequests, addBidResponse } from './auction'; const OVERRIDE_KEY = '$$PREBID_GLOBAL$$:debugging'; -export let boundHook; +export let addBidResponseBound; +export let addBidderRequestsBound; function logMessage(msg) { utilsLogMessage('DEBUG: ' + msg); @@ -15,52 +16,113 @@ function logWarn(msg) { utilsLogWarn('DEBUG: ' + msg); } -function removeHook() { - addBidResponse.getHooks({hook: boundHook}).remove() +function addHooks(overrides) { + addBidResponseBound = addBidResponseHook.bind(overrides); + addBidResponse.before(addBidResponseBound, 5); + + addBidderRequestsBound = addBidderRequestsHook.bind(overrides); + addBidderRequests.before(addBidderRequestsBound, 5); +} + +function removeHooks() { + addBidResponse.getHooks({hook: addBidResponseBound}).remove(); + addBidderRequests.getHooks({hook: addBidderRequestsBound}).remove(); } -function enableOverrides(overrides, fromSession = false) { +export function enableOverrides(overrides, fromSession = false) { config.setConfig({'debug': true}); + removeHooks(); + addHooks(overrides); logMessage(`bidder overrides enabled${fromSession ? ' from session' : ''}`); - - removeHook(); - - boundHook = addBidResponseHook.bind(overrides); - addBidResponse.before(boundHook, 5); } export function disableOverrides() { - removeHook(); + removeHooks(); logMessage('bidder overrides disabled'); } +/** + * @param {{bidder:string, adUnitCode:string}} overrideObj + * @param {string} bidderCode + * @param {string} adUnitCode + * @returns {boolean} + */ +export function bidExcluded(overrideObj, bidderCode, adUnitCode) { + if (overrideObj.bidder && overrideObj.bidder !== bidderCode) { + return true; + } + if (overrideObj.adUnitCode && overrideObj.adUnitCode !== adUnitCode) { + return true; + } + return false; +} + +/** + * @param {string[]} bidders + * @param {string} bidderCode + * @returns {boolean} + */ +export function bidderExcluded(bidders, bidderCode) { + return (Array.isArray(bidders) && bidders.indexOf(bidderCode) === -1); +} + +/** + * @param {Object} overrideObj + * @param {Object} bidObj + * @param {Object} bidType + * @returns {Object} bidObj with overridden properties + */ +export function applyBidOverrides(overrideObj, bidObj, bidType) { + return Object.keys(overrideObj).filter(key => (['adUnitCode', 'bidder'].indexOf(key) === -1)).reduce(function(result, key) { + logMessage(`bidder overrides changed '${result.adUnitCode}/${result.bidderCode}' ${bidType}.${key} from '${result[key]}' to '${overrideObj[key]}'`); + result[key] = overrideObj[key]; + return result; + }, bidObj); +} + export function addBidResponseHook(next, adUnitCode, bid) { - let overrides = this; - if (Array.isArray(overrides.bidders) && overrides.bidders.indexOf(bid.bidderCode) === -1) { + const overrides = this; + + if (bidderExcluded(overrides.bidders, bid.bidderCode)) { logWarn(`bidder '${bid.bidderCode}' excluded from auction by bidder overrides`); return; } if (Array.isArray(overrides.bids)) { - overrides.bids.forEach(overrideBid => { - if (overrideBid.bidder && overrideBid.bidder !== bid.bidderCode) { - return; - } - if (overrideBid.adUnitCode && overrideBid.adUnitCode !== adUnitCode) { - return; + overrides.bids.forEach(function(overrideBid) { + if (!bidExcluded(overrideBid, bid.bidderCode, adUnitCode)) { + applyBidOverrides(overrideBid, bid, 'bidder'); } + }); + } - bid = Object.assign({}, bid); + next(adUnitCode, bid); +} - Object.keys(overrideBid).filter(key => ['bidder', 'adUnitCode'].indexOf(key) === -1).forEach((key) => { - let value = overrideBid[key]; - logMessage(`bidder overrides changed '${adUnitCode}/${bid.bidderCode}' bid.${key} from '${bid[key]}' to '${value}'`); - bid[key] = value; +export function addBidderRequestsHook(next, bidderRequests) { + const overrides = this; + + const includedBidderRequests = bidderRequests.filter(function(bidderRequest) { + if (bidderExcluded(overrides.bidders, bidderRequest.bidderCode)) { + logWarn(`bidRequest '${bidderRequest.bidderCode}' excluded from auction by bidder overrides`); + return false; + } + return true; + }); + + if (Array.isArray(overrides.bidRequests)) { + includedBidderRequests.forEach(function(bidderRequest) { + overrides.bidRequests.forEach(function(overrideBid) { + bidderRequest.bids.forEach(function(bid) { + if (!bidExcluded(overrideBid, bidderRequest.bidderCode, bid.adUnitCode)) { + applyBidOverrides(overrideBid, bid, 'bidRequest'); + } + }); }); }); } - next(adUnitCode, bid); + next(includedBidderRequests); } export function getConfig(debugging) { diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index 2693daa62b3..657afa1cba9 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -982,6 +982,87 @@ describe('auctionmanager.js', function () { }); }); + describe('addBidRequests', function () { + let createAuctionStub; + let adUnits; + let adUnitCodes; + let spec; + let spec1; + let auction; + let ajaxStub; + + let bids = TEST_BIDS; + let bids1 = [mockBid({ bidderCode: BIDDER_CODE1 })]; + + before(function () { + let bidRequests = [ + mockBidRequest(bids[0]), + mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }) + ]; + let makeRequestsStub = sinon.stub(adapterManager, 'makeBidRequests'); + makeRequestsStub.returns(bidRequests); + + ajaxStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(mockAjaxBuilder); + }); + + after(function () { + ajaxStub.restore(); + adapterManager.makeBidRequests.restore(); + }); + + beforeEach(function () { + config.setConfig({ + debugging: { + enabled: true, + bidRequests: [{ + bidderCode: BIDDER_CODE, + adUnitCode: ADUNIT_CODE, + storedAuctionResponse: '11111' + }] + } + }); + + adUnits = [{ + code: ADUNIT_CODE, + bids: [ + {bidder: BIDDER_CODE, params: {placementId: 'id'}}, + ] + }, { + code: ADUNIT_CODE1, + bids: [ + {bidder: BIDDER_CODE1, params: {placementId: 'id'}}, + ] + }]; + adUnitCodes = adUnits.map(({ code }) => code); + auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: 3000}); + createAuctionStub = sinon.stub(auctionModule, 'newAuction'); + createAuctionStub.returns(auction); + + spec = mockBidder(BIDDER_CODE, bids); + spec1 = mockBidder(BIDDER_CODE1, bids1); + + registerBidder(spec); + registerBidder(spec1); + }); + + afterEach(function () { + auctionModule.newAuction.restore(); + config.resetConfig(); + }); + + it('should override bidRequest properties when config debugging has a matching bidRequest defined', function () { + auction.callBids(); + const auctionBidRequests = auction.getBidRequests(); + assert.equal(auctionBidRequests.length > 0, true); + assert.equal(Array.isArray(auctionBidRequests[0].bids), true); + + const bid = find(auctionBidRequests[0].bids, bid => bid.adUnitCode === ADUNIT_CODE); + assert.equal(typeof bid !== 'undefined', true); + assert.equal(bid.hasOwnProperty('storedAuctionResponse'), true); + assert.equal(bid.storedAuctionResponse, '11111'); + }); + }); + describe('getMediaTypeGranularity', function () { it('video', function () { let bidReq = { diff --git a/test/spec/debugging_spec.js b/test/spec/debugging_spec.js index ba9702b0324..b71d0141790 100644 --- a/test/spec/debugging_spec.js +++ b/test/spec/debugging_spec.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; -import { sessionLoader, addBidResponseHook, getConfig, disableOverrides, boundHook } from 'src/debugging'; -import { addBidResponse } from 'src/auction'; +import { sessionLoader, addBidResponseHook, addBidderRequestsHook, getConfig, disableOverrides, addBidResponseBound, addBidderRequestsBound } from 'src/debugging'; +import { addBidResponse, addBidderRequests } from 'src/auction'; import { config } from 'src/config'; describe('bid overrides', function () { @@ -31,14 +31,16 @@ describe('bid overrides', function () { enabled: true }); - expect(addBidResponse.getHooks().some(hook => hook.hook === boundHook)).to.equal(true); + expect(addBidResponse.getHooks().some(hook => hook.hook === addBidResponseBound)).to.equal(true); + expect(addBidderRequests.getHooks().some(hook => hook.hook === addBidderRequestsBound)).to.equal(true); }); it('should happen when configuration found in sessionStorage', function () { sessionLoader({ getItem: () => ('{"enabled": true}') }); - expect(addBidResponse.getHooks().some(hook => hook.hook === boundHook)).to.equal(true); + expect(addBidResponse.getHooks().some(hook => hook.hook === addBidResponseBound)).to.equal(true); + expect(addBidderRequests.getHooks().some(hook => hook.hook === addBidderRequestsBound)).to.equal(true); }); it('should not throw if sessionStorage is inaccessible', function () { @@ -52,7 +54,7 @@ describe('bid overrides', function () { }); }); - describe('hook', function () { + describe('bidResponse hook', function () { let mockBids; let bids; @@ -84,7 +86,7 @@ describe('bid overrides', function () { let next = (adUnitCode, bid) => { bids.push(bid); }; - addBidResponseHook.bind(overrides)(next, bid.adUnitCode, bid) + addBidResponseHook.bind(overrides)(next, bid.adUnitCode, bid); }); } @@ -141,4 +143,51 @@ describe('bid overrides', function () { expect(bids[1].cpm).to.equal(2); }); }); + + describe('bidRequests hook', function () { + let mockBidRequests; + let bidderRequests; + + beforeEach(function () { + let baseBidderRequest = { + 'bidderCode': 'rubicon', + 'bids': [{ + 'width': 970, + 'height': 250, + 'statusMessage': 'Bid available', + 'mediaType': 'banner', + 'source': 'client', + 'currency': 'USD', + 'cpm': 0.5, + 'ttl': 300, + 'netRevenue': false, + 'adUnitCode': '/19968336/header-bid-tag-0' + }] + }; + mockBidRequests = []; + mockBidRequests.push(baseBidderRequest); + mockBidRequests.push(Object.assign({}, baseBidderRequest, { + bidderCode: 'appnexus' + })); + + bidderRequests = []; + }); + + function run(overrides) { + let next = (b) => { + bidderRequests = b; + }; + addBidderRequestsHook.bind(overrides)(next, mockBidRequests); + } + + it('should allow us to exclude bidders', function () { + run({ + enabled: true, + bidders: ['appnexus'] + }); + + expect(bidderRequests.length).to.equal(1); + expect(bidderRequests[0].bidderCode).to.equal('appnexus'); + }); + }); }); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 538e1da9aeb..1253098a399 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -460,7 +460,8 @@ describe('S2S Adapter', function () { 'sizes': [300, 250], 'bidId': '123', 'bidderRequestId': '3d1063078dfcc8', - 'auctionId': '173afb6d132ba3' + 'auctionId': '173afb6d132ba3', + 'storedAuctionResponse': 11111 } ], 'auctionStart': 1510852447530, @@ -787,6 +788,27 @@ describe('S2S Adapter', function () { }); }); + it('adds debugging value from storedAuctionResponse to OpenRTB', function () { + const s2sConfig = Object.assign({}, CONFIG, { + endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + }); + const _config = { + s2sConfig: s2sConfig, + device: { ifa: '6D92078A-8246-4BA4-AE5B-76104861E7DC' }, + app: { bundle: 'com.test.app' } + }; + + config.setConfig(_config); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const requestBid = JSON.parse(server.requests[0].requestBody); + expect(requestBid.imp).to.exist.and.to.be.a('array'); + expect(requestBid.imp).to.have.lengthOf(1); + expect(requestBid.imp[0].ext).to.exist.and.to.be.a('object'); + expect(requestBid.imp[0].ext.prebid).to.exist.and.to.be.a('object'); + expect(requestBid.imp[0].ext.prebid.storedauctionresponse).to.exist.and.to.be.a('object'); + expect(requestBid.imp[0].ext.prebid.storedauctionresponse.id).to.equal('11111'); + }); + it('adds device.w and device.h even if the config lacks a device object', function () { const s2sConfig = Object.assign({}, CONFIG, { endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 5a9e2968309..2413f2703f9 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -225,7 +225,8 @@ describe('the rubicon adapter', function () { lipbid: '0000-1111-2222-3333', segments: ['segA', 'segB'] } - } + }; + bid.storedAuctionResponse = 11111; } function createVideoBidderRequestNoVideo() { @@ -1640,6 +1641,22 @@ describe('the rubicon adapter', function () { expect(request.data.regs.coppa).to.equal(1); }); + it('should include storedAuctionResponse in video bid request', function () { + createVideoBidderRequest(); + + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); + + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp).to.exist.and.to.be.a('array'); + expect(request.data.imp).to.have.lengthOf(1); + expect(request.data.imp[0].ext).to.exist.and.to.be.a('object'); + expect(request.data.imp[0].ext.prebid).to.exist.and.to.be.a('object'); + expect(request.data.imp[0].ext.prebid.storedauctionresponse).to.exist.and.to.be.a('object'); + expect(request.data.imp[0].ext.prebid.storedauctionresponse.id).to.equal('11111'); + }); + it('should include pbAdSlot in bid request', function () { createVideoBidderRequest(); bidderRequest.bids[0].fpd = {