forked from prebid/Prebid.js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathadpod.js
430 lines (391 loc) · 18.9 KB
/
adpod.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
/**
* This module houses the functionality to evaluate and process adpod adunits/bids. Specifically there are several hooked functions,
* that either supplement the base function (ie to check something additional or unique to adpod objects) or to replace the base funtion
* entirely when appropriate.
*
* Brief outline of each hook:
* - `callPrebidCacheHook` - for any adpod bids, this function will temporarily hold them in a queue in order to send the bids to Prebid Cache in bulk
* - `checkAdUnitSetupHook` - evaluates the adUnits to ensure that required fields for adpod adUnits are present. Invalid adpod adUntis are removed from the array.
* - `checkVideoBidSetupHook` - evaluates the adpod bid returned from an adaptor/bidder to ensure required fields are populated; also initializes duration bucket field.
*
* To initialize the module, there is an `initAdpodHooks()` function that should be imported and executed by a corresponding `...AdServerVideo`
* module that designed to support adpod video type ads. This import process allows this module to effectively act as a sub-module.
*/
import * as utils from '../src/utils';
import { addBidToAuction, doCallbacksIfTimedout, AUCTION_IN_PROGRESS, callPrebidCache } from '../src/auction';
import { checkAdUnitSetup } from '../src/prebid';
import { checkVideoBidSetup } from '../src/video';
import { setupBeforeHookFnOnce } from '../src/hook';
import { store } from '../src/videoCache';
import { config } from '../src/config';
import { ADPOD } from '../src/mediaTypes';
import Set from 'core-js/library/fn/set';
import find from 'core-js/library/fn/array/find';
const from = require('core-js/library/fn/array/from');
export const TARGETING_KEY_PB_CAT_DUR = 'hb_pb_cat_dur';
export const TARGETING_KEY_CACHE_ID = 'hb_cache_id'
let queueTimeDelay = 50;
let queueSizeLimit = 5;
let bidCacheRegistry = createBidCacheRegistry();
/**
* Create a registry object that stores/manages bids while be held in queue for Prebid Cache.
* @returns registry object with defined accessor functions
*/
function createBidCacheRegistry() {
let registry = {};
function setupRegistrySlot(auctionId) {
registry[auctionId] = {};
registry[auctionId].bidStorage = new Set();
registry[auctionId].queueDispatcher = createDispatcher(queueTimeDelay);
registry[auctionId].initialCacheKey = utils.generateUUID();
}
return {
addBid: function (bid) {
// create parent level object based on auction ID (in case there are concurrent auctions running) to store objects for that auction
if (!registry[bid.auctionId]) {
setupRegistrySlot(bid.auctionId);
}
registry[bid.auctionId].bidStorage.add(bid);
},
removeBid: function (bid) {
registry[bid.auctionId].bidStorage.delete(bid);
},
getBids: function (bid) {
return registry[bid.auctionId] && registry[bid.auctionId].bidStorage.values();
},
getQueueDispatcher: function(bid) {
return registry[bid.auctionId] && registry[bid.auctionId].queueDispatcher;
},
setupInitialCacheKey: function(bid) {
if (!registry[bid.auctionId]) {
registry[bid.auctionId] = {};
registry[bid.auctionId].initialCacheKey = utils.generateUUID();
}
},
getInitialCacheKey: function(bid) {
return registry[bid.auctionId] && registry[bid.auctionId].initialCacheKey;
}
}
}
/**
* Creates a function that when called updates the bid queue and extends the running timer (when called subsequently).
* Once the time threshold for the queue (defined by queueSizeLimit) is reached, the queue will be flushed by calling the `firePrebidCacheCall` function.
* If there is a long enough time between calls (based on timeoutDration), the queue will automatically flush itself.
* @param {Number} timeoutDuration number of milliseconds to pass before timer expires and current bid queue is flushed
* @returns {Function}
*/
function createDispatcher(timeoutDuration) {
let timeout;
let counter = 1;
return function(auctionInstance, bidListArr, afterBidAdded, killQueue) {
const context = this;
var callbackFn = function() {
firePrebidCacheCall.call(context, auctionInstance, bidListArr, afterBidAdded);
};
clearTimeout(timeout);
if (!killQueue) {
// want to fire off the queue if either: size limit is reached or time has passed since last call to dispatcher
if (counter === queueSizeLimit) {
counter = 1;
callbackFn();
} else {
counter++;
timeout = setTimeout(callbackFn, timeoutDuration);
}
} else {
counter = 1;
}
};
}
/**
* This function reads certain fields from the bid to generate a specific key used for caching the bid in Prebid Cache
* @param {Object} bid bid object to update
* @param {Boolean} brandCategoryExclusion value read from setConfig; influences whether category is required or not
*/
function attachPriceIndustryDurationKeyToBid(bid, brandCategoryExclusion) {
let initialCacheKey = bidCacheRegistry.getInitialCacheKey(bid);
let duration = utils.deepAccess(bid, 'video.durationBucket');
let cpmFixed = bid.cpm.toFixed(2);
let pcd;
if (brandCategoryExclusion) {
let category = utils.deepAccess(bid, 'meta.adServerCatId');
pcd = `${cpmFixed}_${category}_${duration}s`;
} else {
pcd = `${cpmFixed}_${duration}s`;
}
if (!bid.adserverTargeting) {
bid.adserverTargeting = {};
}
bid.adserverTargeting[TARGETING_KEY_PB_CAT_DUR] = pcd;
bid.adserverTargeting[TARGETING_KEY_CACHE_ID] = initialCacheKey;
bid.videoCacheKey = initialCacheKey;
bid.customCacheKey = `${pcd}_${initialCacheKey}`;
}
/**
* Updates the running queue for the associated auction.
* Does a check to ensure the auction is still running; if it's not - the previously running queue is killed.
* @param {*} auctionInstance running context of the auction
* @param {Object} bidResponse bid object being added to queue
* @param {Function} afterBidAdded callback function used when Prebid Cache responds
*/
function updateBidQueue(auctionInstance, bidResponse, afterBidAdded) {
let bidListIter = bidCacheRegistry.getBids(bidResponse);
if (bidListIter) {
let bidListArr = from(bidListIter);
let callDispatcher = bidCacheRegistry.getQueueDispatcher(bidResponse);
let killQueue = !!(auctionInstance.getAuctionStatus() !== AUCTION_IN_PROGRESS);
callDispatcher(auctionInstance, bidListArr, afterBidAdded, killQueue);
} else {
utils.logWarn('Attempted to cache a bid from an unknown auction. Bid:', bidResponse);
}
}
/**
* Small helper function to remove bids from internal storage; normally b/c they're about to sent to Prebid Cache for processing.
* @param {Array[Object]} bidResponses list of bids to remove
*/
function removeBidsFromStorage(bidResponses) {
for (let i = 0; i < bidResponses.length; i++) {
bidCacheRegistry.removeBid(bidResponses[i]);
}
}
/**
* This function will send a list of bids to Prebid Cache. It also removes the same bids from the internal bidCacheRegistry
* to maintain which bids are in queue.
* If the bids are successfully cached, they will be added to the respective auction.
* @param {*} auctionInstance running context of the auction
* @param {Array[Object]} bidList list of bid objects that need to be sent to Prebid Cache
* @param {Function} afterBidAdded callback function used when Prebid Cache responds
*/
function firePrebidCacheCall(auctionInstance, bidList, afterBidAdded) {
// remove entries now so other incoming bids won't accidentally have a stale version of the list while PBC is processing the current submitted list
removeBidsFromStorage(bidList);
store(bidList, function (error, cacheIds) {
if (error) {
utils.logWarn(`Failed to save to the video cache: ${error}. Video bid(s) must be discarded.`);
for (let i = 0; i < bidList.length; i++) {
doCallbacksIfTimedout(auctionInstance, bidList[i]);
}
} else {
for (let i = 0; i < cacheIds.length; i++) {
// when uuid in response is empty string then the key already existed, so this bid wasn't cached
if (cacheIds[i].uuid !== '') {
addBidToAuction(auctionInstance, bidList[i]);
} else {
utils.logInfo(`Detected a bid was not cached because the custom key was already registered. Attempted to use key: ${bidList[i].customCacheKey}. Bid was: `, bidList[i]);
}
afterBidAdded();
}
}
});
}
/**
* This is the main hook function to handle adpod bids; maintains the logic to temporarily hold bids in a queue in order to send bulk requests to Prebid Cache.
* @param {Function} fn reference to original function (used by hook logic)
* @param {*} auctionInstance running context of the auction
* @param {Object} bidResponse incoming bid; if adpod, will be processed through hook function. If not adpod, returns to original function.
* @param {Function} afterBidAdded callback function used when Prebid Cache responds
* @param {Object} bidderRequest copy of bid's associated bidderRequest object
*/
export function callPrebidCacheHook(fn, auctionInstance, bidResponse, afterBidAdded, bidderRequest) {
let videoConfig = utils.deepAccess(bidderRequest, 'mediaTypes.video');
if (videoConfig && videoConfig.context === ADPOD) {
let brandCategoryExclusion = config.getConfig('adpod.brandCategoryExclusion');
let adServerCatId = utils.deepAccess(bidResponse, 'meta.adServerCatId');
if (!adServerCatId && brandCategoryExclusion) {
utils.logWarn('Detected a bid without meta.adServerCatId while setConfig({adpod.brandCategoryExclusion}) was enabled. This bid has been rejected:', bidResponse)
afterBidAdded();
}
if (config.getConfig('adpod.deferCaching') === false) {
bidCacheRegistry.addBid(bidResponse);
attachPriceIndustryDurationKeyToBid(bidResponse, brandCategoryExclusion);
updateBidQueue(auctionInstance, bidResponse, afterBidAdded);
} else {
// generate targeting keys for bid
bidCacheRegistry.setupInitialCacheKey(bidResponse);
attachPriceIndustryDurationKeyToBid(bidResponse, brandCategoryExclusion);
// add bid to auction
addBidToAuction(auctionInstance, bidResponse);
afterBidAdded();
}
} else {
fn.call(this, auctionInstance, bidResponse, afterBidAdded, bidderRequest);
}
}
/**
* This hook function will review the adUnit setup and verify certain required values are present in any adpod adUnits.
* If the fields are missing or incorrectly setup, the adUnit is removed from the list.
* @param {Function} fn reference to original function (used by hook logic)
* @param {Array[Object]} adUnits list of adUnits to be evaluated
* @returns {Array[Object]} list of adUnits that passed the check
*/
export function checkAdUnitSetupHook(fn, adUnits) {
let goodAdUnits = adUnits.filter(adUnit => {
let mediaTypes = utils.deepAccess(adUnit, 'mediaTypes');
let videoConfig = utils.deepAccess(mediaTypes, 'video');
if (videoConfig && videoConfig.context === ADPOD) {
// run check to see if other mediaTypes are defined (ie multi-format); reject adUnit if so
if (Object.keys(mediaTypes).length > 1) {
utils.logWarn(`Detected more than one mediaType in adUnitCode: ${adUnit.code} while attempting to define an 'adpod' video adUnit. 'adpod' adUnits cannot be mixed with other mediaTypes. This adUnit will be removed from the auction.`);
return false;
}
let errMsg = `Detected missing or incorrectly setup fields for an adpod adUnit. Please review the following fields of adUnitCode: ${adUnit.code}. This adUnit will be removed from the auction.`;
let playerSize = !!(videoConfig.playerSize && utils.isArrayOfNums(videoConfig.playerSize));
let adPodDurationSec = !!(videoConfig.adPodDurationSec && utils.isNumber(videoConfig.adPodDurationSec) && videoConfig.adPodDurationSec > 0);
let durationRangeSec = !!(videoConfig.durationRangeSec && utils.isArrayOfNums(videoConfig.durationRangeSec) && videoConfig.durationRangeSec.every(range => range > 0));
if (!playerSize || !adPodDurationSec || !durationRangeSec) {
errMsg += (!playerSize) ? '\nmediaTypes.video.playerSize' : '';
errMsg += (!adPodDurationSec) ? '\nmediaTypes.video.adPodDurationSec' : '';
errMsg += (!durationRangeSec) ? '\nmediaTypes.video.durationRangeSec' : '';
utils.logWarn(errMsg);
return false;
}
}
return true;
});
adUnits = goodAdUnits;
fn.call(this, adUnits);
}
/**
* This check evaluates the incoming bid's `video.durationSeconds` field and tests it against specific logic depending on adUnit config. Summary of logic below:
* when adUnit.mediaTypes.video.requireExactDuration is true
* - only bids that exactly match those listed values are accepted (don't round at all).
* - populate the `bid.video.durationBucket` field with the matching duration value
* when adUnit.mediaTypes.video.requireExactDuration is false
* - round the duration to the next highest specified duration value based on adunit. If the duration is above a range within a set buffer, that bid falls down into that bucket.
* (eg if range was [5, 15, 30] -> 2s is rounded to 5s; 17s is rounded back to 15s; 18s is rounded up to 30s)
* - if the bid is above the range of the listed durations (and outside the buffer), reject the bid
* - set the rounded duration value in the `bid.video.durationBucket` field for accepted bids
* @param {Object} bidderRequest copy of the bidderRequest object associated to bidResponse
* @param {Object} bidResponse incoming bidResponse being evaluated by bidderFactory
* @returns {boolean} return false if bid duration is deemed invalid as per adUnit configuration; return true if fine
*/
function checkBidDuration(bidderRequest, bidResponse) {
const buffer = 2;
let bidDuration = utils.deepAccess(bidResponse, 'video.durationSeconds');
let videoConfig = utils.deepAccess(bidderRequest, 'mediaTypes.video');
let adUnitRanges = videoConfig.durationRangeSec;
adUnitRanges.sort((a, b) => a - b); // ensure the ranges are sorted in numeric order
if (!videoConfig.requireExactDuration) {
let max = Math.max(...adUnitRanges);
if (bidDuration <= (max + buffer)) {
let nextHighestRange = find(adUnitRanges, range => (range + buffer) >= bidDuration);
bidResponse.video.durationBucket = nextHighestRange;
} else {
utils.logWarn(`Detected a bid with a duration value outside the accepted ranges specified in adUnit.mediaTypes.video.durationRangeSec. Rejecting bid: `, bidResponse);
return false;
}
} else {
if (find(adUnitRanges, range => range === bidDuration)) {
bidResponse.video.durationBucket = bidDuration;
} else {
utils.logWarn(`Detected a bid with a duration value not part of the list of accepted ranges specified in adUnit.mediaTypes.video.durationRangeSec. Exact match durations must be used for this adUnit. Rejecting bid: `, bidResponse);
return false;
}
}
return true;
}
/**
* This hooked function evaluates an adpod bid and determines if the required fields are present.
* If it's found to not be an adpod bid, it will return to original function via hook logic
* @param {Function} fn reference to original function (used by hook logic)
* @param {Object} bid incoming bid object
* @param {Object} bidRequest bidRequest object of associated bid
* @param {Object} videoMediaType copy of the `bidRequest.mediaTypes.video` object; used in original function
* @param {String} context value of the `bidRequest.mediaTypes.video.context` field; used in original function
* @returns {boolean} this return is only used for adpod bids
*/
export function checkVideoBidSetupHook(fn, bid, bidRequest, videoMediaType, context) {
if (context === ADPOD) {
let result = true;
let brandCategoryExclusion = config.getConfig('adpod.brandCategoryExclusion');
if (brandCategoryExclusion && !utils.deepAccess(bid, 'meta.iabSubCatId')) {
result = false;
}
if (utils.deepAccess(bid, 'video')) {
if (!utils.deepAccess(bid, 'video.context') || bid.video.context !== ADPOD) {
result = false;
}
if (!utils.deepAccess(bid, 'video.durationSeconds') || bid.video.durationSeconds <= 0) {
result = false;
} else {
let isBidGood = checkBidDuration(bidRequest, bid);
if (!isBidGood) result = false;
}
}
if (!config.getConfig('cache.url') && bid.vastXml && !bid.vastUrl) {
utils.logError(`
This bid contains only vastXml and will not work when a prebid cache url is not specified.
Try enabling prebid cache with pbjs.setConfig({ cache: {url: "..."} });
`);
result = false;
};
fn.bail(result);
} else {
fn.call(this, bid, bidRequest, videoMediaType, context);
}
}
/**
* This function reads the (optional) settings for the adpod as set from the setConfig()
* @param {Object} config contains the config settings for adpod module
*/
export function adpodSetConfig(config) {
if (config.bidQueueTimeDelay !== undefined) {
if (typeof config.bidQueueTimeDelay === 'number' && config.bidQueueTimeDelay > 0) {
queueTimeDelay = config.bidQueueTimeDelay;
} else {
utils.logWarn(`Detected invalid value for adpod.bidQueueTimeDelay in setConfig; must be a positive number. Using default: ${queueTimeDelay}`)
}
}
if (config.bidQueueSizeLimit !== undefined) {
if (typeof config.bidQueueSizeLimit === 'number' && config.bidQueueSizeLimit > 0) {
queueSizeLimit = config.bidQueueSizeLimit;
} else {
utils.logWarn(`Detected invalid value for adpod.bidQueueSizeLimit in setConfig; must be a positive number. Using default: ${queueSizeLimit}`)
}
}
}
config.getConfig('adpod', config => adpodSetConfig(config.adpod));
/**
* This function initializes the adpod module's hooks. This is called by the corresponding adserver video module.
*/
export function initAdpodHooks() {
setupBeforeHookFnOnce(callPrebidCache, callPrebidCacheHook);
setupBeforeHookFnOnce(checkAdUnitSetup, checkAdUnitSetupHook);
setupBeforeHookFnOnce(checkVideoBidSetup, checkVideoBidSetupHook);
}
/**
*
* @param {Array[Object]} bids list of 'winning' bids that need to be cached
* @param {Function} callback send the cached bids (or error) back to adserverVideoModule for further processing
}}
*/
export function callPrebidCacheAfterAuction(bids, callback) {
// will call PBC here and execute cb param to initialize player code
store(bids, function(error, cacheIds) {
if (error) {
callback(error, null);
} else {
let successfulCachedBids = [];
for (let i = 0; i < cacheIds.length; i++) {
if (cacheIds[i] !== '') {
successfulCachedBids.push(bids[i]);
}
}
callback(null, successfulCachedBids);
}
})
}
/**
* Compare function to be used in sorting long-form bids. This will compare bids on price per second.
* @param {Object} bid
* @param {Object} bid
*/
export function sortByPricePerSecond(a, b) {
if (a.cpm / a.video.durationBucket < b.cpm / b.video.durationBucket) {
return 1;
}
if (a.cpm / a.video.durationBucket > b.cpm / b.video.durationBucket) {
return -1;
}
return 0;
}