-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Multibid Module: add new module to handle multiple bids from single b…
…idder & update rubicon adapter (#6404) * Multibid module - create new module - Expands the number of key value pairs going to the ad server in the normal Prebid way by establishing the concept of a "dynamic alias" First commit * Continued updates from 1st commit * Adding logWarn for filtered bids * Update to include passing multibid configuration to PBS requests * Update to rubicon bid adapter to pass query param rp_maxbids value taken from bidderRequest.bidLimit * Update to config to look for camelcase property names according to spec. These convert to all lowercase when passed to PBS endpoint * Adjust RP adapter to always include maxbids value - default is 1 * Added support for bidders array in multibid config * Fixed floor comparison to be <= bid cpm as oppossed to just < bid cpm. Updated md file to fix camelCase tpyo * Update to include originalBidderRequest in video call to prebid cache * Update to ignore adpod bids from multibid and allow them to return as normal bids
- Loading branch information
1 parent
270d369
commit 781238b
Showing
11 changed files
with
1,420 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
/** | ||
* This module adds Multibid support to prebid.js | ||
* @module modules/multibid | ||
*/ | ||
|
||
import {config} from '../../src/config.js'; | ||
import {setupBeforeHookFnOnce, getHook} from '../../src/hook.js'; | ||
import * as utils from '../../src/utils.js'; | ||
import events from '../../src/events.js'; | ||
import CONSTANTS from '../../src/constants.json'; | ||
import {addBidderRequests} from '../../src/auction.js'; | ||
import {getHighestCpmBidsFromBidPool, sortByDealAndPriceBucketOrCpm} from '../../src/targeting.js'; | ||
|
||
const MODULE_NAME = 'multibid'; | ||
let hasMultibid = false; | ||
let multiConfig = {}; | ||
let multibidUnits = {}; | ||
|
||
// Storing this globally on init for easy reference to configuration | ||
config.getConfig(MODULE_NAME, conf => { | ||
if (!Array.isArray(conf.multibid) || !conf.multibid.length || !validateMultibid(conf.multibid)) return; | ||
|
||
resetMultiConfig(); | ||
hasMultibid = true; | ||
|
||
conf.multibid.forEach(entry => { | ||
if (entry.bidder) { | ||
multiConfig[entry.bidder] = { | ||
maxbids: entry.maxBids, | ||
prefix: entry.targetBiddercodePrefix | ||
} | ||
} else { | ||
entry.bidders.forEach(key => { | ||
multiConfig[key] = { | ||
maxbids: entry.maxBids, | ||
prefix: entry.targetBiddercodePrefix | ||
} | ||
}); | ||
} | ||
}); | ||
}); | ||
|
||
/** | ||
* @summary validates multibid configuration entries | ||
* @param {Object[]} multibid - example [{bidder: 'bidderA', maxbids: 2, prefix: 'bidA'}, {bidder: 'bidderB', maxbids: 2}] | ||
* @return {Boolean} | ||
*/ | ||
export function validateMultibid(conf) { | ||
let check = true; | ||
let duplicate = conf.filter(entry => { | ||
// Check if entry.bidder is not defined or typeof string, filter entry and reset configuration | ||
if ((!entry.bidder || typeof entry.bidder !== 'string') && (!entry.bidders || !Array.isArray(entry.bidders))) { | ||
utils.logWarn('Filtering multibid entry. Missing required bidder or bidders property.'); | ||
check = false; | ||
return false; | ||
} | ||
|
||
return true; | ||
}).map(entry => { | ||
// Check if entry.maxbids is not defined, not typeof number, or less than 1, set maxbids to 1 and reset configuration | ||
// Check if entry.maxbids is greater than 9, set maxbids to 9 and reset configuration | ||
if (typeof entry.maxBids !== 'number' || entry.maxBids < 1 || entry.maxBids > 9) { | ||
entry.maxBids = (typeof entry.maxBids !== 'number' || entry.maxBids < 1) ? 1 : 9; | ||
check = false; | ||
} | ||
|
||
return entry; | ||
}); | ||
|
||
if (!check) config.setConfig({multibid: duplicate}); | ||
|
||
return check; | ||
} | ||
|
||
/** | ||
* @summary addBidderRequests before hook | ||
* @param {Function} fn reference to original function (used by hook logic) | ||
* @param {Object[]} array containing copy of each bidderRequest object | ||
*/ | ||
export function adjustBidderRequestsHook(fn, bidderRequests) { | ||
bidderRequests.map(bidRequest => { | ||
// Loop through bidderRequests and check if bidderCode exists in multiconfig | ||
// If true, add bidderRequest.bidLimit to bidder request | ||
if (multiConfig[bidRequest.bidderCode]) { | ||
bidRequest.bidLimit = multiConfig[bidRequest.bidderCode].maxbids | ||
} | ||
return bidRequest; | ||
}) | ||
|
||
fn.call(this, bidderRequests); | ||
} | ||
|
||
/** | ||
* @summary addBidResponse before hook | ||
* @param {Function} fn reference to original function (used by hook logic) | ||
* @param {String} ad unit code for bid | ||
* @param {Object} bid object | ||
*/ | ||
export function addBidResponseHook(fn, adUnitCode, bid) { | ||
let floor = utils.deepAccess(bid, 'floorData.floorValue'); | ||
|
||
if (!config.getConfig('multibid')) resetMultiConfig(); | ||
// Checks if multiconfig exists and bid bidderCode exists within config and is an adpod bid | ||
// Else checks if multiconfig exists and bid bidderCode exists within config | ||
// Else continue with no modifications | ||
if (hasMultibid && multiConfig[bid.bidderCode] && utils.deepAccess(bid, 'video.context') === 'adpod') { | ||
fn.call(this, adUnitCode, bid); | ||
} else if (hasMultibid && multiConfig[bid.bidderCode]) { | ||
// Set property multibidPrefix on bid | ||
if (multiConfig[bid.bidderCode].prefix) bid.multibidPrefix = multiConfig[bid.bidderCode].prefix; | ||
bid.originalBidder = bid.bidderCode; | ||
// Check if stored bids for auction include adUnitCode.bidder and max limit not reach for ad unit | ||
if (utils.deepAccess(multibidUnits, `${adUnitCode}.${bid.bidderCode}`)) { | ||
// Store request id under new property originalRequestId, create new unique bidId, | ||
// and push bid into multibid stored bids for auction if max not reached and bid cpm above floor | ||
if (!multibidUnits[adUnitCode][bid.bidderCode].maxReached && (!floor || floor <= bid.cpm)) { | ||
bid.originalRequestId = bid.requestId; | ||
|
||
bid.requestId = utils.getUniqueIdentifierStr(); | ||
multibidUnits[adUnitCode][bid.bidderCode].ads.push(bid); | ||
|
||
let length = multibidUnits[adUnitCode][bid.bidderCode].ads.length; | ||
|
||
if (multiConfig[bid.bidderCode].prefix) bid.targetingBidder = multiConfig[bid.bidderCode].prefix + length; | ||
if (length === multiConfig[bid.bidderCode].maxbids) multibidUnits[adUnitCode][bid.bidderCode].maxReached = true; | ||
|
||
fn.call(this, adUnitCode, bid); | ||
} else { | ||
utils.logWarn(`Filtering multibid received from bidder ${bid.bidderCode}: ` + ((multibidUnits[adUnitCode][bid.bidderCode].maxReached) ? `Maximum bid limit reached for ad unit code ${adUnitCode}` : 'Bid cpm under floors value.')); | ||
} | ||
} else { | ||
if (utils.deepAccess(bid, 'floorData.floorValue')) utils.deepSetValue(multibidUnits, `${adUnitCode}.${bid.bidderCode}`, {floor: utils.deepAccess(bid, 'floorData.floorValue')}); | ||
|
||
utils.deepSetValue(multibidUnits, `${adUnitCode}.${bid.bidderCode}`, {ads: [bid]}); | ||
if (multibidUnits[adUnitCode][bid.bidderCode].ads.length === multiConfig[bid.bidderCode].maxbids) multibidUnits[adUnitCode][bid.bidderCode].maxReached = true; | ||
|
||
fn.call(this, adUnitCode, bid); | ||
} | ||
} else { | ||
fn.call(this, adUnitCode, bid); | ||
} | ||
} | ||
|
||
/** | ||
* A descending sort function that will sort the list of objects based on the following: | ||
* - bids without dynamic aliases are sorted before bids with dynamic aliases | ||
*/ | ||
export function sortByMultibid(a, b) { | ||
if (a.bidder !== a.bidderCode && b.bidder === b.bidderCode) { | ||
return 1; | ||
} | ||
|
||
if (a.bidder === a.bidderCode && b.bidder !== b.bidderCode) { | ||
return -1; | ||
} | ||
|
||
return 0; | ||
} | ||
|
||
/** | ||
* @summary getHighestCpmBidsFromBidPool before hook | ||
* @param {Function} fn reference to original function (used by hook logic) | ||
* @param {Object[]} array of objects containing all bids from bid pool | ||
* @param {Function} function to reduce to only highest cpm value for each bidderCode | ||
* @param {Number} adUnit bidder targeting limit, default set to 0 | ||
* @param {Boolean} default set to false, this hook modifies targeting and sets to true | ||
*/ | ||
export function targetBidPoolHook(fn, bidsReceived, highestCpmCallback, adUnitBidLimit = 0, hasModified = false) { | ||
if (!config.getConfig('multibid')) resetMultiConfig(); | ||
if (hasMultibid) { | ||
const dealPrioritization = config.getConfig('sendBidsControl.dealPrioritization'); | ||
let modifiedBids = []; | ||
let buckets = utils.groupBy(bidsReceived, 'adUnitCode'); | ||
let bids = [].concat.apply([], Object.keys(buckets).reduce((result, slotId) => { | ||
let bucketBids = []; | ||
// Get bids and group by property originalBidder | ||
let bidsByBidderName = utils.groupBy(buckets[slotId], 'originalBidder'); | ||
let adjustedBids = [].concat.apply([], Object.keys(bidsByBidderName).map(key => { | ||
// Reset all bidderCodes to original bidder values and sort by CPM | ||
return bidsByBidderName[key].sort((bidA, bidB) => { | ||
if (bidA.originalBidder && bidA.originalBidder !== bidA.bidderCode) bidA.bidderCode = bidA.originalBidder; | ||
if (bidA.originalBidder && bidB.originalBidder !== bidB.bidderCode) bidB.bidderCode = bidB.originalBidder; | ||
return bidA.cpm > bidB.cpm ? -1 : (bidA.cpm < bidB.cpm ? 1 : 0); | ||
}).map((bid, index) => { | ||
// For each bid (post CPM sort), set dynamic bidderCode using prefix and index if less than maxbid amount | ||
if (utils.deepAccess(multiConfig, `${bid.bidderCode}.prefix`) && index !== 0 && index < multiConfig[bid.bidderCode].maxbids) { | ||
bid.bidderCode = multiConfig[bid.bidderCode].prefix + (index + 1); | ||
} | ||
|
||
return bid | ||
}) | ||
})); | ||
// Get adjustedBids by bidderCode and reduce using highestCpmCallback | ||
let bidsByBidderCode = utils.groupBy(adjustedBids, 'bidderCode'); | ||
Object.keys(bidsByBidderCode).forEach(key => bucketBids.push(bidsByBidderCode[key].reduce(highestCpmCallback))); | ||
// if adUnitBidLimit is set, pass top N number bids | ||
if (adUnitBidLimit > 0) { | ||
bucketBids = dealPrioritization ? bucketBids.sort(sortByDealAndPriceBucketOrCpm(true)) : bucketBids.sort((a, b) => b.cpm - a.cpm); | ||
bucketBids.sort(sortByMultibid); | ||
modifiedBids.push(...bucketBids.slice(0, adUnitBidLimit)); | ||
} else { | ||
modifiedBids.push(...bucketBids); | ||
} | ||
|
||
return [].concat.apply([], modifiedBids); | ||
}, [])); | ||
|
||
fn.call(this, bids, highestCpmCallback, adUnitBidLimit, true); | ||
} else { | ||
fn.call(this, bidsReceived, highestCpmCallback, adUnitBidLimit); | ||
} | ||
} | ||
|
||
/** | ||
* Resets globally stored multibid configuration | ||
*/ | ||
export const resetMultiConfig = () => { hasMultibid = false; multiConfig = {}; }; | ||
|
||
/** | ||
* Resets globally stored multibid ad unit bids | ||
*/ | ||
export const resetMultibidUnits = () => multibidUnits = {}; | ||
|
||
/** | ||
* Set up hooks on init | ||
*/ | ||
function init() { | ||
events.on(CONSTANTS.EVENTS.AUCTION_INIT, resetMultibidUnits); | ||
setupBeforeHookFnOnce(addBidderRequests, adjustBidderRequestsHook); | ||
getHook('addBidResponse').before(addBidResponseHook, 3); | ||
setupBeforeHookFnOnce(getHighestCpmBidsFromBidPool, targetBidPoolHook); | ||
} | ||
|
||
init(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# Overview | ||
|
||
Module Name: multibid | ||
|
||
Purpose: To expand the number of key value pairs going to the ad server in the normal Prebid way by establishing the concept of a "dynamic alias" -- a bidder code that exists only on the response, not in the adunit. | ||
|
||
|
||
# Description | ||
Allowing a single bidder to multi-bid into an auction has several use cases: | ||
|
||
1. allows a bidder to provide both outstream and banner | ||
2. supports the video VAST fallback scenario | ||
3. allows one bid to be blocked in the ad server and the second one still considered | ||
4. add extra high-value bids to the cache for future refreshes | ||
|
||
|
||
# Example of using config | ||
``` | ||
pbjs.setConfig({ | ||
multibid: [{ | ||
bidder: "bidderA", | ||
maxBids: 3, | ||
targetBiddercodePrefix: "bidA" | ||
},{ | ||
bidder: "bidderB", | ||
maxBids: 3, | ||
targetBiddercodePrefix: "bidB" | ||
},{ | ||
bidder: "bidderC", | ||
maxBids: 3 | ||
},{ | ||
bidders: ["bidderD", "bidderE"], | ||
maxBids: 2 | ||
}] | ||
}); | ||
``` | ||
|
||
# Please Note: | ||
- | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.