Skip to content

Commit

Permalink
Prebid core: return a promise from requestBids (prebid#9106)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgirardi committed Oct 24, 2022
1 parent 0a267dd commit 64aff9b
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 110 deletions.
139 changes: 72 additions & 67 deletions src/prebid.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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));

Expand All @@ -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) {
Expand Down
139 changes: 96 additions & 43 deletions test/spec/unit/pbjs_api_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -1467,7 +1468,6 @@ describe('Unit: Prebid Module', function () {
after(function () {
clock.restore();
});
let bidsBackHandlerStub = sinon.stub();

const BIDDER_CODE = 'sampleBidder';
let bids = [{
Expand Down Expand Up @@ -1504,57 +1504,66 @@ 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: {
banner: {
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
};
Expand All @@ -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({
Expand All @@ -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,
Expand All @@ -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 () {
Expand Down

0 comments on commit 64aff9b

Please sign in to comment.