diff --git a/src/prebid.js b/src/prebid.js index 0378f8c27ed..7b1cfd258ec 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -622,7 +622,7 @@ $$PREBID_GLOBAL$$.removeAdUnit = function (adUnitCode) { * @alias module:pbjs.requestBids */ $$PREBID_GLOBAL$$.requestBids = (function() { - const delegate = hook('async', function ({ bidsBackHandler, timeout, adUnits, adUnitCodes, labels, auctionId, ortb2, metrics } = {}) { + const delegate = hook('sync', function ({ bidsBackHandler, timeout, adUnits, adUnitCodes, labels, auctionId, ortb2, metrics } = {}) { events.emit(REQUEST_BIDS); const cbTimeout = timeout || config.getConfig('bidderTimeout'); logInfo('Invoking $$PREBID_GLOBAL$$.requestBids', arguments); @@ -646,7 +646,7 @@ $$PREBID_GLOBAL$$.requestBids = (function() { }); })(); -export const startAuction = hook('async', function ({ bidsBackHandler, timeout: cbTimeout, adUnits, adUnitCodes, labels, auctionId, ortb2Fragments, metrics } = {}) { +export const startAuction = hook('sync', function ({ bidsBackHandler, timeout: cbTimeout, adUnits, adUnitCodes, labels, auctionId, ortb2Fragments, metrics } = {}) { const s2sBidders = getS2SBidderSet(config.getConfig('s2sConfig') || []); adUnits = useMetrics(metrics).measureTime('requestBids.validate', () => checkAdUnitSetup(adUnits)); @@ -658,77 +658,82 @@ export const startAuction = hook('async', function ({ bidsBackHandler, timeout: adUnitCodes = adUnits && adUnits.map(unit => unit.code); } - /* - * for a given adunit which supports a set of mediaTypes - * and a given bidder which supports a set of mediaTypes - * a bidder is eligible to participate on the adunit - * if it supports at least one of the mediaTypes on the adunit - */ - adUnits.forEach(adUnit => { - // get the adunit's mediaTypes, defaulting to banner if mediaTypes isn't present - const adUnitMediaTypes = Object.keys(adUnit.mediaTypes || { 'banner': 'banner' }); - - // get the bidder's mediaTypes - const allBidders = adUnit.bids.map(bid => bid.bidder); - const bidderRegistry = adapterManager.bidderRegistry; - - const bidders = allBidders.filter(bidder => !s2sBidders.has(bidder)); - - const tid = adUnit.ortb2Imp?.ext?.tid || generateUUID(); - adUnit.transactionId = tid; - // Populate ortb2Imp.ext.tid with transactionId. Specifying a transaction ID per item in the ortb impression array, lets multiple transaction IDs be transmitted in a single bid request. - deepSetValue(adUnit, 'ortb2Imp.ext.tid', tid); - - bidders.forEach(bidder => { - const adapter = bidderRegistry[bidder]; - const spec = adapter && adapter.getSpec && adapter.getSpec(); - // banner is default if not specified in spec - const bidderMediaTypes = (spec && spec.supportedMediaTypes) || ['banner']; - - // check if the bidder's mediaTypes are not in the adUnit's mediaTypes - const bidderEligible = adUnitMediaTypes.some(type => includes(bidderMediaTypes, type)); - if (!bidderEligible) { - // drop the bidder from the ad unit if it's not compatible - logWarn(unsupportedBidderMessage(adUnit, bidder)); - adUnit.bids = adUnit.bids.filter(bid => bid.bidder !== bidder); - } else { - adunitCounter.incrementBidderRequestsCounter(adUnit.code, bidder); + return new Promise((resolve) => { + function auctionDone(bids, timedOut, auctionId) { + if (typeof bidsBackHandler === 'function') { + try { + bidsBackHandler(bids, timedOut, auctionId); + } catch (e) { + logError('Error executing bidsBackHandler', null, e); + } } + resolve({bids, timedOut, auctionId}); + } + + /* + * for a given adunit which supports a set of mediaTypes + * and a given bidder which supports a set of mediaTypes + * a bidder is eligible to participate on the adunit + * if it supports at least one of the mediaTypes on the adunit + */ + adUnits.forEach(adUnit => { + // get the adunit's mediaTypes, defaulting to banner if mediaTypes isn't present + const adUnitMediaTypes = Object.keys(adUnit.mediaTypes || { 'banner': 'banner' }); + + // get the bidder's mediaTypes + const allBidders = adUnit.bids.map(bid => bid.bidder); + const bidderRegistry = adapterManager.bidderRegistry; + + const bidders = allBidders.filter(bidder => !s2sBidders.has(bidder)); + + const tid = adUnit.ortb2Imp?.ext?.tid || generateUUID(); + adUnit.transactionId = tid; + // Populate ortb2Imp.ext.tid with transactionId. Specifying a transaction ID per item in the ortb impression array, lets multiple transaction IDs be transmitted in a single bid request. + deepSetValue(adUnit, 'ortb2Imp.ext.tid', tid); + + bidders.forEach(bidder => { + const adapter = bidderRegistry[bidder]; + const spec = adapter && adapter.getSpec && adapter.getSpec(); + // banner is default if not specified in spec + const bidderMediaTypes = (spec && spec.supportedMediaTypes) || ['banner']; + + // check if the bidder's mediaTypes are not in the adUnit's mediaTypes + const bidderEligible = adUnitMediaTypes.some(type => includes(bidderMediaTypes, type)); + if (!bidderEligible) { + // drop the bidder from the ad unit if it's not compatible + logWarn(unsupportedBidderMessage(adUnit, bidder)); + adUnit.bids = adUnit.bids.filter(bid => bid.bidder !== bidder); + } else { + adunitCounter.incrementBidderRequestsCounter(adUnit.code, bidder); + } + }); + adunitCounter.incrementRequestsCounter(adUnit.code); }); - adunitCounter.incrementRequestsCounter(adUnit.code); - }); - if (!adUnits || adUnits.length === 0) { - logMessage('No adUnits configured. No bids requested.'); - if (typeof bidsBackHandler === 'function') { - // executeCallback, this will only be called in case of first request - try { - bidsBackHandler(); - } catch (e) { - logError('Error executing bidsBackHandler', null, e); + if (!adUnits || adUnits.length === 0) { + logMessage('No adUnits configured. No bids requested.'); + auctionDone(); + } else { + const auction = auctionManager.createAuction({ + adUnits, + adUnitCodes, + callback: auctionDone, + cbTimeout, + labels, + auctionId, + ortb2Fragments, + metrics, + }); + + let adUnitsLen = adUnits.length; + if (adUnitsLen > 15) { + logInfo(`Current auction ${auction.getAuctionId()} contains ${adUnitsLen} adUnits.`, adUnits); } - } - return; - } - const auction = auctionManager.createAuction({ - adUnits, - adUnitCodes, - callback: bidsBackHandler, - cbTimeout, - labels, - auctionId, - ortb2Fragments, - metrics, + adUnitCodes.forEach(code => targeting.setLatestAuctionForAdUnit(code, auction.getAuctionId())); + auction.callBids(); + } }); - - let adUnitsLen = adUnits.length; - if (adUnitsLen > 15) { - logInfo(`Current auction ${auction.getAuctionId()} contains ${adUnitsLen} adUnits.`, adUnits); - } - - adUnitCodes.forEach(code => targeting.setLatestAuctionForAdUnit(code, auction.getAuctionId())); - auction.callBids(); }, 'startAuction'); export function executeCallbacks(fn, reqBidsConfigObj) { diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 4c1f5c3b455..e83608325d8 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -21,7 +21,8 @@ import {hook} from '../../../src/hook.js'; import {reset as resetDebugging} from '../../../src/debugging.js'; import $$PREBID_GLOBAL$$ from 'src/prebid.js'; import {resetAuctionState} from 'src/auction.js'; - +import {stubAuctionIndex} from '../../helpers/indexStub.js'; +import {createBid} from '../../../src/bidfactory.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -1467,7 +1468,6 @@ describe('Unit: Prebid Module', function () { after(function () { clock.restore(); }); - let bidsBackHandlerStub = sinon.stub(); const BIDDER_CODE = 'sampleBidder'; let bids = [{ @@ -1504,11 +1504,12 @@ describe('Unit: Prebid Module', function () { 'start': 1000 }]; + let spec, indexStub, auction, completeAuction; + beforeEach(function () { logMessageSpy = sinon.spy(utils, 'logMessage'); makeRequestsStub = sinon.stub(adapterManager, 'makeBidRequests'); makeRequestsStub.returns(bidRequests); - adUnits = [{ code: 'adUnit-code', mediaTypes: { @@ -1516,45 +1517,53 @@ describe('Unit: Prebid Module', function () { sizes: [[300, 250]] } }, + transactionId: 'mock-tid', bids: [ {bidder: BIDDER_CODE, params: {placementId: 'id'}}, ] }]; - let adUnitCodes = ['adUnit-code']; - let auction = auctionModule.newAuction({ - adUnits, - adUnitCodes, - callback: bidsBackHandlerStub, - cbTimeout: 2000 - }); - let createAuctionStub = sinon.stub(auctionModule, 'newAuction'); - createAuctionStub.returns(auction); - }); - - afterEach(function () { - clock.restore(); - adapterManager.makeBidRequests.restore(); - auctionModule.newAuction.restore(); - utils.logMessage.restore(); - }); - - it('should execute callback after timeout', function () { - let spec = { + indexStub = sinon.stub(auctionManager, 'index'); + indexStub.get(() => stubAuctionIndex({adUnits, bidRequests})) + sinon.stub(adapterManager, 'callBids').callsFake((_, bidrequests, addBidResponse, adapterDone) => { + completeAuction = (bidsReceived) => { + bidsReceived.forEach((bid) => addBidResponse(bid.adUnitCode, Object.assign(createBid(), bid))); + bidRequests.forEach((req) => adapterDone.call(req)); + } + }) + const origNewAuction = auctionModule.newAuction; + sinon.stub(auctionModule, 'newAuction').callsFake(function (opts) { + auction = origNewAuction(opts); + return auction; + }) + spec = { code: BIDDER_CODE, isBidRequestValid: sinon.stub(), buildRequests: sinon.stub(), interpretResponse: sinon.stub(), getUserSyncs: sinon.stub(), - onTimeout: sinon.stub() + onTimeout: sinon.stub(), + onSetTargeting: sinon.stub(), }; registerBidder(spec); spec.buildRequests.returns([{'id': 123, 'method': 'POST'}]); spec.isBidRequestValid.returns(true); spec.interpretResponse.returns(bids); + }); + + afterEach(function () { + clock.restore(); + adapterManager.makeBidRequests.restore(); + adapterManager.callBids.restore(); + indexStub.restore(); + auction.getBidsReceived = () => []; + auctionModule.newAuction.restore(); + utils.logMessage.restore(); + }); + it('should execute callback after timeout', function () { let requestObj = { - bidsBackHandler: null, // does not need to be defined because of newAuction mock in beforeEach + bidsBackHandler: sinon.stub(), timeout: 2000, adUnits: adUnits }; @@ -1567,26 +1576,13 @@ describe('Unit: Prebid Module', function () { clock.tick(1); assert.ok(logMessageSpy.calledWith(sinon.match(re)), 'executeCallback called'); - expect(bidsBackHandlerStub.getCall(0).args[1]).to.equal(true, + expect(requestObj.bidsBackHandler.getCall(0).args[1]).to.equal(true, 'bidsBackHandler should be called with timedOut=true'); sinon.assert.called(spec.onTimeout); }); - it('should execute callback after setTargeting', function () { - let spec = { - code: BIDDER_CODE, - isBidRequestValid: sinon.stub(), - buildRequests: sinon.stub(), - interpretResponse: sinon.stub(), - onSetTargeting: sinon.stub() - }; - - registerBidder(spec); - spec.buildRequests.returns([{'id': 123, 'method': 'POST'}]); - spec.isBidRequestValid.returns(true); - spec.interpretResponse.returns(bids); - + it('should execute `onSetTargeting` after setTargetingForGPTAsync', function () { const bidId = 1; const auctionId = 1; let adResponse = Object.assign({ @@ -1595,6 +1591,7 @@ describe('Unit: Prebid Module', function () { width: 300, height: 250, adUnitCode: bidRequests[0].bids[0].adUnitCode, + transactionId: 'mock-tid', adserverTargeting: { 'hb_bidder': BIDDER_CODE, 'hb_adid': bidId, @@ -1603,20 +1600,76 @@ describe('Unit: Prebid Module', function () { }, bidder: bids[0].bidderCode, }, bids[0]); - auction.getBidsReceived = function() { return [adResponse]; } - auction.getAuctionId = () => auctionId; let requestObj = { - bidsBackHandler: null, // does not need to be defined because of newAuction mock in beforeEach + bidsBackHandler: null, timeout: 2000, adUnits: adUnits }; $$PREBID_GLOBAL$$.requestBids(requestObj); + completeAuction([adResponse]); $$PREBID_GLOBAL$$.setTargetingForGPTAsync(); sinon.assert.called(spec.onSetTargeting); }); + + describe('returns a promise that resolves', () => { + Object.entries({ + 'immediately, without bidsBackHandler': (req) => $$PREBID_GLOBAL$$.requestBids(req), + 'after bidsBackHandler': (() => { + const bidsBackHandler = sinon.stub(); + return function (req) { + return $$PREBID_GLOBAL$$.requestBids({...req, bidsBackHandler}).then(({bids, timedOut, auctionId}) => { + sinon.assert.calledWith(bidsBackHandler, bids, timedOut, auctionId); + return {bids, timedOut, auctionId}; + }) + } + })(), + 'after a bidsBackHandler that throws': (req) => $$PREBID_GLOBAL$$.requestBids({...req, bidsBackHandler: () => { throw new Error() }}) + }).forEach(([t, requestBids]) => { + describe(t, () => { + it('with no args, when no adUnits are defined', () => { + return requestBids({}).then((res) => { + expect(res).to.eql({ + bids: undefined, + timedOut: undefined, + auctionId: undefined + }); + }); + }); + + it('on timeout', (done) => { + requestBids({ + auctionId: 'mock-auctionId', + adUnits, + timeout: 10 + }).then(({timedOut, bids, auctionId}) => { + expect(timedOut).to.be.true; + expect(bids).to.eql({}); + expect(auctionId).to.eql('mock-auctionId'); + done(); + }); + clock.tick(12); + }); + + it('with auction result', (done) => { + const bid = { + bidder: 'mock-bidder', + adUnitCode: adUnits[0].code, + transactionId: adUnits[0].transactionId + } + requestBids({ + adUnits, + }).then(({bids}) => { + sinon.assert.match(bids[bid.adUnitCode].bids[0], bid) + done(); + }); + completeAuction([bid]); + }) + }) + }) + }) }) describe('requestBids', function () {