From 552c393257ea3609eb8fda9c9a22b44507cc75f3 Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Fri, 9 Nov 2018 14:39:47 -0700 Subject: [PATCH] new no bid event and no bids available from auction --- src/AnalyticsAdapter.js | 2 ++ src/adaptermanager.js | 14 +++++--- src/adapters/bidderFactory.js | 2 +- src/auction.js | 57 +++++++++++++++++++++----------- src/auctionManager.js | 9 +++-- src/constants.json | 1 + src/prebid.js | 33 +++++++++++++----- test/spec/api_spec.js | 4 +++ test/spec/auctionmanager_spec.js | 44 ++++++++++-------------- 9 files changed, 104 insertions(+), 62 deletions(-) diff --git a/src/AnalyticsAdapter.js b/src/AnalyticsAdapter.js index 3cb64f1b911..5565ba2ed18 100644 --- a/src/AnalyticsAdapter.js +++ b/src/AnalyticsAdapter.js @@ -11,6 +11,7 @@ const { BID_REQUESTED, BID_TIMEOUT, BID_RESPONSE, + NO_BID, BID_WON, BID_ADJUSTMENT, BIDDER_DONE, @@ -100,6 +101,7 @@ export default function AnalyticsAdapter({ url, analyticsType, global, handler } _handlers = { [BID_REQUESTED]: args => this.enqueue({ eventType: BID_REQUESTED, args }), [BID_RESPONSE]: args => this.enqueue({ eventType: BID_RESPONSE, args }), + [NO_BID]: args => this.enqueue({ eventType: NO_BID, args }), [BID_TIMEOUT]: args => this.enqueue({ eventType: BID_TIMEOUT, args }), [BID_WON]: args => this.enqueue({ eventType: BID_WON, args }), [BID_ADJUSTMENT]: args => this.enqueue({ eventType: BID_ADJUSTMENT, args }), diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 55aab710741..3242115f024 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -1,6 +1,6 @@ /** @module adaptermanger */ -import { flatten, getBidderCodes, getDefinedParams, shuffle, timestamp } from './utils'; +import { flatten, getBidderCodes, getDefinedParams, shuffle, timestamp, getBidderRequest } from './utils'; import { getLabels, resolveStatus } from './sizeMapping'; import { processNativeAdUnitParams, nativeAdapters } from './native'; import { newBidder } from './adapters/bidderFactory'; @@ -340,7 +340,7 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb, requestCallbac if (s2sBidRequest.ad_units.length) { let doneCbs = serverBidRequests.map(bidRequest => { bidRequest.start = timestamp(); - return doneCb; + return doneCb.bind(bidRequest); }); // only log adapters that actually have adUnit bids @@ -360,7 +360,12 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb, requestCallbac s2sAdapter.callBids( s2sBidRequest, serverBidRequests, - addBidResponse, + function(adUnitCode, bid) { + let bidderRequest = getBidderRequest(serverBidRequests, bid.bidderCode, adUnitCode); + if (bidderRequest) { + addBidResponse.call(bidderRequest, adUnitCode, bid) + } + }, () => doneCbs.forEach(done => done()), s2sAjax ); @@ -375,12 +380,11 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb, requestCallbac const adapter = _bidderRegistry[bidRequest.bidderCode]; utils.logMessage(`CALLING BIDDER ======= ${bidRequest.bidderCode}`); events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); - bidRequest.doneCbCallCount = 0; let ajax = ajaxBuilder(requestBidsTimeout, requestCallbacks ? { request: requestCallbacks.request.bind(null, bidRequest.bidderCode), done: requestCallbacks.done } : undefined); - adapter.callBids(bidRequest, addBidResponse, doneCb, ajax); + adapter.callBids(bidRequest, addBidResponse.bind(bidRequest), doneCb.bind(bidRequest), ajax); }); } diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index d5ccf57e394..e25c041527e 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -176,7 +176,7 @@ export function newBidder(spec) { // After all the responses have come back, call done() and // register any required usersync pixels. const responses = []; - function afterAllResponses(bids) { + function afterAllResponses() { done(); events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest); registerSyncs(responses, bidderRequest.gdprConsent); diff --git a/src/auction.js b/src/auction.js index 37b2c6896f2..a8affd40ca6 100644 --- a/src/auction.js +++ b/src/auction.js @@ -48,7 +48,7 @@ * @property {function(): void} callBids - sends requests to all adapters for bids */ -import { uniques, flatten, timestamp, adUnitsFilter, getBidderRequest, deepAccess, delayExecution, getBidRequest } from './utils'; +import { uniques, flatten, timestamp, adUnitsFilter, deepAccess, getBidRequest } from './utils'; import { getPriceBucketString } from './cpmBucketManager'; import { getNativeTargeting } from './native'; import { getCacheUrl, store } from './videoCache'; @@ -95,6 +95,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels}) let _adUnitCodes = adUnitCodes; let _bidderRequests = []; let _bidsReceived = []; + let _noBids = []; let _auctionStart; let _auctionEnd; let _auctionId = utils.generateUUID(); @@ -106,6 +107,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels}) function addBidRequests(bidderRequests) { _bidderRequests = _bidderRequests.concat(bidderRequests) }; function addBidReceived(bidsReceived) { _bidsReceived = _bidsReceived.concat(bidsReceived); } + function addNoBid(noBid) { _noBids = _noBids.concat(noBid); } function getProperties() { return { @@ -117,6 +119,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels}) adUnitCodes: _adUnitCodes, labels: _labels, bidderRequests: _bidderRequests, + noBids: _noBids, bidsReceived: _bidsReceived, winningBids: _winningBids, timeout: _timeout @@ -175,7 +178,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels}) } } - function auctionDone(bidderCount) { + function auctionDone() { // when all bidders have called done callback atleast once it means auction is complete utils.logInfo(`Bids Received for Auction with id: ${_auctionId}`, _bidsReceived); _auctionStatus = AUCTION_COMPLETED; @@ -208,10 +211,12 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels}) events.emit(CONSTANTS.EVENTS.AUCTION_INIT, getProperties()); let callbacks = auctionCallbacks(auctionDone, this); - let boundObj = { - auctionAddBidResponse: callbacks.addBidResponse - }; - adaptermanager.callBids(_adUnits, bidRequests, addBidResponse.bind(boundObj), callbacks.adapterDone, { + 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); @@ -296,6 +301,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels}) return { addBidReceived, + addNoBid, executeCallback, callBids, addWinningBid, @@ -308,20 +314,19 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels}) getAdUnitCodes: () => _adUnitCodes, getBidRequests: () => _bidderRequests, getBidsReceived: () => _bidsReceived, + getNoBids: () => _noBids } } export const addBidResponse = createHook('asyncSeries', function(adUnitCode, bid) { - this.auctionAddBidResponse(adUnitCode, bid); + this.dispatch.call(this.bidderRequest, adUnitCode, bid); }, 'addBidResponse'); export function auctionCallbacks(auctionDone, auctionInstance) { let outstandingBidsAdded = 0; let allAdapterCalledDone = false; - - let onAllAdapterDone = delayExecution(() => { - allAdapterCalledDone = true; - }, auctionInstance.getBidRequests().length); + let bidderRequestsDone = new Set(); + let bidResponseMap = {}; function afterBidAdded() { outstandingBidsAdded--; @@ -331,15 +336,17 @@ export function auctionCallbacks(auctionDone, auctionInstance) { } function addBidResponse(adUnitCode, bid) { + let bidderRequest = this; + + bidResponseMap[bid.requestId] = true; + outstandingBidsAdded++; - let bidRequests = auctionInstance.getBidRequests(); let auctionId = auctionInstance.getAuctionId(); - let bidRequest = getBidderRequest(bidRequests, bid.bidderCode, adUnitCode); - let bidResponse = getPreparedBidForAuction({adUnitCode, bid, bidRequest, auctionId}); + let bidResponse = getPreparedBidForAuction({adUnitCode, bid, bidderRequest, auctionId}); if (bidResponse.mediaType === 'video') { - tryAddVideoBid(auctionInstance, bidResponse, bidRequest, afterBidAdded); + tryAddVideoBid(auctionInstance, bidResponse, bidderRequest, afterBidAdded); } else { addBidToAuction(auctionInstance, bidResponse); afterBidAdded(); @@ -347,7 +354,19 @@ export function auctionCallbacks(auctionDone, auctionInstance) { } function adapterDone() { - onAllAdapterDone(); + let bidderRequest = this; + + bidderRequestsDone.add(bidderRequest); + allAdapterCalledDone = auctionInstance.getBidRequests() + .every(bidderRequest => bidderRequestsDone.has(bidderRequest)); + + bidderRequest.bids.forEach(bid => { + if (!bidResponseMap[bid.bidId]) { + auctionInstance.addNoBid(bid); + events.emit(CONSTANTS.EVENTS.NO_BID, bid); + } + }); + if (allAdapterCalledDone && outstandingBidsAdded === 0) { auctionDone(); } @@ -412,8 +431,8 @@ function tryAddVideoBid(auctionInstance, bidResponse, bidRequests, afterBidAdded // Postprocess the bids so that all the universal properties exist, no matter which bidder they came from. // This should be called before addBidToAuction(). -function getPreparedBidForAuction({adUnitCode, bid, bidRequest, auctionId}) { - const start = bidRequest.start; +function getPreparedBidForAuction({adUnitCode, bid, bidderRequest, auctionId}) { + const start = bidderRequest.start; let bidObject = Object.assign({}, bid, { auctionId, @@ -433,7 +452,7 @@ function getPreparedBidForAuction({adUnitCode, bid, bidRequest, auctionId}) { events.emit(CONSTANTS.EVENTS.BID_ADJUSTMENT, bidObject); // a publisher-defined renderer can be used to render bids - const bidReq = bidRequest.bids && find(bidRequest.bids, bid => bid.adUnitCode == adUnitCode); + const bidReq = bidderRequest.bids && find(bidderRequest.bids, bid => bid.adUnitCode == adUnitCode); const adUnitRenderer = bidReq && bidReq.renderer; if (adUnitRenderer && adUnitRenderer.url) { diff --git a/src/auctionManager.js b/src/auctionManager.js index 389cac31fe3..3f28062ecfd 100644 --- a/src/auctionManager.js +++ b/src/auctionManager.js @@ -39,18 +39,23 @@ export function newAuctionManager() { } else { utils.logWarn(`Auction not found when adding winning bid`); } - } + }; auctionManager.getAllWinningBids = function() { return _auctions.map(auction => auction.getWinningBids()) .reduce(flatten, []); - } + }; auctionManager.getBidsRequested = function() { return _auctions.map(auction => auction.getBidRequests()) .reduce(flatten, []); }; + auctionManager.getNoBids = function() { + return _auctions.map(auction => auction.getNoBids()) + .reduce(flatten, []); + }; + auctionManager.getBidsReceived = function() { // As of now, an old bid which is not used in auction 1 can be used in auction n. // To prevent this, bid.ttl (time to live) will be added to this logic and bid pool will also be added diff --git a/src/constants.json b/src/constants.json index 7c06db48469..9f571b9234c 100644 --- a/src/constants.json +++ b/src/constants.json @@ -30,6 +30,7 @@ "BID_TIMEOUT": "bidTimeout", "BID_REQUESTED": "bidRequested", "BID_RESPONSE": "bidResponse", + "NO_BID": "noBid", "BID_WON": "bidWon", "BIDDER_DONE": "bidderDone", "SET_TARGETING": "setTargeting", diff --git a/src/prebid.js b/src/prebid.js index 0460b99bbb0..ed4398feb6f 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -114,15 +114,8 @@ $$PREBID_GLOBAL$$.getAdserverTargeting = function (adUnitCode) { return targeting.getAllTargeting(adUnitCode); }; -/** - * This function returns the bid responses at the given moment. - * @alias module:pbjs.getBidResponses - * @return {Object} map | object that contains the bidResponses - */ - -$$PREBID_GLOBAL$$.getBidResponses = function () { - utils.logInfo('Invoking $$PREBID_GLOBAL$$.getBidResponses', arguments); - const responses = auctionManager.getBidsReceived() +function getBids(type) { + const responses = auctionManager[type]() .filter(adUnitsFilter.bind(this, auctionManager.getAdUnitCodes())); // find the last auction id to get responses for most recent auction only @@ -139,6 +132,28 @@ $$PREBID_GLOBAL$$.getBidResponses = function () { }; }) .reduce((a, b) => Object.assign(a, b), {}); +} + +/** + * This function returns the bids requests involved in an auction but not bid on + * @alias module:pbjs.getNoBids + * @return {Object} map | object that contains the bidRequests + */ + +$$PREBID_GLOBAL$$.getNoBids = function () { + utils.logInfo('Invoking $$PREBID_GLOBAL$$.getNoBids', arguments); + return getBids('getNoBids'); +}; + +/** + * This function returns the bid responses at the given moment. + * @alias module:pbjs.getBidResponses + * @return {Object} map | object that contains the bidResponses + */ + +$$PREBID_GLOBAL$$.getBidResponses = function () { + utils.logInfo('Invoking $$PREBID_GLOBAL$$.getBidResponses', arguments); + return getBids('getBidsReceived'); }; /** diff --git a/test/spec/api_spec.js b/test/spec/api_spec.js index 41fafb080ad..a8987d3ed07 100755 --- a/test/spec/api_spec.js +++ b/test/spec/api_spec.js @@ -39,6 +39,10 @@ describe('Publisher API', function () { assert.isFunction($$PREBID_GLOBAL$$.getBidResponses); }); + it('should have function $$PREBID_GLOBAL$$.getBidResponses', function () { + assert.isFunction($$PREBID_GLOBAL$$.getNoBids); + }); + it('should have function $$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode', function () { assert.isFunction($$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode); }); diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index cd0c8586d04..85e37ca512d 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -1,9 +1,8 @@ -import { auctionManager, newAuctionManager } from 'src/auctionManager'; import { getKeyValueTargetingPairs, auctionCallbacks } from 'src/auction'; import CONSTANTS from 'src/constants.json'; import { adjustBids } from 'src/auction'; import * as auctionModule from 'src/auction'; -import { newBidder, registerBidder } from 'src/adapters/bidderFactory'; +import { registerBidder } from 'src/adapters/bidderFactory'; import { config } from 'src/config'; import * as store from 'src/videoCache'; import * as ajaxLib from 'src/ajax'; @@ -20,10 +19,6 @@ var fixtures = require('../fixtures/fixtures'); var adaptermanager = require('src/adaptermanager'); var events = require('src/events'); -function timestamp() { - return new Date().getTime(); -} - const BIDDER_CODE = 'sampleBidder'; const BIDDER_CODE1 = 'sampleBidder1'; @@ -667,7 +662,6 @@ describe('auctionmanager.js', function () { describe('when auction timeout is 20', function () { let eventsEmitSpy; - let getBidderRequestStub; before(function () { bids = [mockBid(), mockBid({ bidderCode: BIDDER_CODE1 })]; @@ -700,18 +694,16 @@ describe('auctionmanager.js', function () { eventsEmitSpy = sinon.spy(events, 'emit'); - let origGBR = utils.getBidderRequest; - getBidderRequestStub = sinon.stub(utils, 'getBidderRequest'); - getBidderRequestStub.callsFake((bidRequests, bidder, adUnitCode) => { - let req = origGBR(bidRequests, bidder, adUnitCode); - req.start = 1000; - return req; + // make all timestamp calls 5 minutes apart + let callCount = 0; + sinon.stub(utils, 'timestamp').callsFake(function() { + return new Date().getTime() + (callCount++ * 1000 * 60 * 5); }); }); afterEach(function () { auctionModule.newAuction.restore(); events.emit.restore(); - getBidderRequestStub.restore(); + utils.timestamp.restore(); }); it('should emit BID_TIMEOUT for timed out bids', function () { auction.callBids(); @@ -885,12 +877,12 @@ describe('auctionmanager.js', function () { mockBidRequest(bids2[0], { adUnitCode: ADUNIT_CODE2 }) ]; let cbs = auctionCallbacks(doneSpy, auction); - cbs.addBidResponse(ADUNIT_CODE, bids[0]); - cbs.adapterDone(); - cbs.addBidResponse(ADUNIT_CODE1, bids1[0]); - cbs.adapterDone(); - cbs.addBidResponse(ADUNIT_CODE2, bids2[0]); - cbs.adapterDone(); + cbs.addBidResponse.call(bidRequests[0], ADUNIT_CODE, bids[0]); + cbs.adapterDone.call(bidRequests[0]); + cbs.addBidResponse.call(bidRequests[1], ADUNIT_CODE1, bids1[0]); + cbs.adapterDone.call(bidRequests[1]); + cbs.addBidResponse.call(bidRequests[2], ADUNIT_CODE2, bids2[0]); + cbs.adapterDone.call(bidRequests[2]); assert.equal(doneSpy.callCount, 1); }); @@ -905,17 +897,17 @@ describe('auctionmanager.js', function () { playerSize: [640, 480], }, } - } + }; bidRequests = [ mockBidRequest(bids[0], opts), mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }), - ] + ]; let cbs = auctionCallbacks(doneSpy, auction); - cbs.addBidResponse(ADUNIT_CODE, bids[0]); - cbs.adapterDone(); - cbs.addBidResponse(ADUNIT_CODE1, bids1[0]); - cbs.adapterDone(); + cbs.addBidResponse.call(bidRequests[0], ADUNIT_CODE, bids[0]); + cbs.adapterDone.call(bidRequests[0]); + cbs.addBidResponse.call(bidRequests[1], ADUNIT_CODE1, bids1[0]); + cbs.adapterDone.call(bidRequests[1]); assert.equal(doneSpy.callCount, 0); const uuid = 'c488b101-af3e-4a99-b538-00423e5a3371'; const responseBody = `{"responses":[{"uuid":"${uuid}"}]}`;