From 5f37109745d96a9221921796f9cca31440b3dd1d Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Thu, 4 Mar 2021 12:17:17 -0500 Subject: [PATCH 01/11] 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 --- modules/multibid/index.js | 142 ++++++++++++++++++++++++++++++++++++++ modules/multibid/index.md | 32 +++++++++ src/targeting.js | 46 ++++++------ 3 files changed, 200 insertions(+), 20 deletions(-) create mode 100644 modules/multibid/index.js create mode 100644 modules/multibid/index.md diff --git a/modules/multibid/index.js b/modules/multibid/index.js new file mode 100644 index 00000000000..a3e736d4bde --- /dev/null +++ b/modules/multibid/index.js @@ -0,0 +1,142 @@ + + +import {config} from '../../src/config.js'; +import {setupBeforeHookFnOnce} 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, addBidResponse} from '../../src/auction.js'; +import {getHighestCpmBidsFromBidPool, sortByDealAndPriceBucketOrCpm, targeting} from '../../src/targeting.js'; + +/** @type {string} */ +const MODULE_NAME = 'multibid'; +let multiConfig = {}; +let multibidUnits = {}; +let multibidCache = {}; + +config.getConfig(MODULE_NAME, conf => { + if (!Array.isArray(conf.multibid) || !validateMultibid(conf.multibid)) return; + + resetMultiConfig(); + + conf.multibid.forEach(entry => { + multiConfig[entry.bidder] = { + maxbids: entry.maxbids, + prefix: entry.targetbiddercodeprefix + } + }); +}); + +events.on(CONSTANTS.EVENTS.BIDDER_DONE, removeBidderBidData); + +function validateMultibid(conf) { + let check = true; + + let duplicate = conf.filter(entry => { + if (!entry.bidder || typeof entry.bidder !== 'string') { + utils.logWarn('Filtering multibid entry due to missing required bidder property.'); + check = false; + return false; + } + }).map(entry => { + 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; +} + +function addResponses(fn, adUnitCode, bid) { + if (multiConfig && multiConfig[bid.bidderCode]) { + if (multiConfig[bid.bidderCode].prefix) bid.multibidPrefix = multiConfig[bid.bidderCode].prefix; + bid.originalBidder = bid.bidderCode; + if (utils.deepAccess(multibidUnits, `${adUnitCode}.${bid.bidderCode}`)) { + if (!multibidUnits[adUnitCode][bid.bidderCode].maxReached) { + multibidUnits[adUnitCode][bid.bidderCode].ads.push(bid); + + if (multibidUnits[adUnitCode][bid.bidderCode].ads.length === multiConfig[bid.bidderCode].maxbids) multibidUnits[adUnitCode][bid.bidderCode].maxReached = true; + + fn.call(this, adUnitCode, bid); + } + } else { + utils.deepSetValue(multibidUnits, `${adUnitCode}.${bid.bidderCode}`, {ads: [bid]}); + fn.call(this, adUnitCode, bid); + } + } else { + fn.call(this, adUnitCode, bid); + } +} + +function removeBidderBidData(bidRequest) { + bidRequest.bids.forEach(bid => { + if(utils.deepAccess(multibidUnits, `${bid.adUnitCode}.${bid.bidder}`)) delete multibidUnits[bid.adUnitCode][bid.bidder]; + }); +} + +/** +* 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 +**/ +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; +} + +function targetBidPoolHook(fn, bidsReceived, highestCpmCallback, adUnitBidLimit = 0, hasModified = false) { + 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 = []; + let bidsByBidderName = utils.groupBy(buckets[slotId], 'originalBidder'); + let adjustedBids = [].concat.apply([], Object.keys(bidsByBidderName).map(key => { + 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) => { + if (utils.deepAccess(multiConfig, `${bid.bidderCode}.prefix`) && index !== 0 && index < multiConfig[bid.bidderCode].maxbids) { + bid.bidderCode = multiConfig[bid.bidderCode].prefix + (index + 1); + } + + return bid + }) + })); + 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); +} + +const resetMultiConfig = () => multiConfig = {}; + +export function init() { + setupBeforeHookFnOnce(addBidResponse, addResponses); + setupBeforeHookFnOnce(getHighestCpmBidsFromBidPool, targetBidPoolHook); +} + +init(); diff --git a/modules/multibid/index.md b/modules/multibid/index.md new file mode 100644 index 00000000000..84c6d1f5551 --- /dev/null +++ b/modules/multibid/index.md @@ -0,0 +1,32 @@ +# 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 + + + +# Example of using config +``` + pbjs.setConfig({ + multibid: [{ + bidder: "bidderA", + maxbids: 3, + targetbiddercodeprefix: "bidA" + },{ + bidder: "bidderB", + maxbids: 3, + targetbiddercodeprefix: "bidB" + },{ + bidder: "bidderC", + maxbids: 3 + }] + }); +``` + +# Please Note: +- + diff --git a/src/targeting.js b/src/targeting.js index cb53ea0fed3..acc005a6ffa 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -4,6 +4,7 @@ import { NATIVE_TARGETING_KEYS } from './native.js'; import { auctionManager } from './auctionManager.js'; import { sizeSupported } from './sizeMapping.js'; import { ADPOD } from './mediaTypes.js'; +import { hook } from './hook.js'; import includes from 'core-js-pure/features/array/includes.js'; import find from 'core-js-pure/features/array/find.js'; @@ -33,26 +34,31 @@ export let filters = { // If two bids are found for same adUnitCode, we will use the highest one to take part in auction // This can happen in case of concurrent auctions // If adUnitBidLimit is set above 0 return top N number of bids -export function getHighestCpmBidsFromBidPool(bidsReceived, highestCpmCallback, adUnitBidLimit = 0) { - const bids = []; - const dealPrioritization = config.getConfig('sendBidsControl.dealPrioritization'); - // bucket by adUnitcode - let buckets = groupBy(bidsReceived, 'adUnitCode'); - // filter top bid for each bucket by bidder - Object.keys(buckets).forEach(bucketKey => { - let bucketBids = []; - let bidsByBidder = groupBy(buckets[bucketKey], 'bidderCode'); - Object.keys(bidsByBidder).forEach(key => bucketBids.push(bidsByBidder[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); - bids.push(...bucketBids.slice(0, adUnitBidLimit)); - } else { - bids.push(...bucketBids); - } - }); - return bids; -} +export const getHighestCpmBidsFromBidPool = hook('sync', function(bidsReceived, highestCpmCallback, adUnitBidLimit = 0, hasModified = false) { + if (!hasModified) { + const bids = []; + const dealPrioritization = config.getConfig('sendBidsControl.dealPrioritization'); + // bucket by adUnitcode + let buckets = groupBy(bidsReceived, 'adUnitCode'); + // filter top bid for each bucket by bidder + Object.keys(buckets).forEach(bucketKey => { + let bucketBids = []; + let bidsByBidder = groupBy(buckets[bucketKey], 'bidderCode'); + Object.keys(bidsByBidder).forEach(key => bucketBids.push(bidsByBidder[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); + bids.push(...bucketBids.slice(0, adUnitBidLimit)); + } else { + bids.push(...bucketBids); + } + }); + + return bids; + } + + return bidsReceived; +}) /** * A descending sort function that will sort the list of objects based on the following two dimensions: From dbbb0ac42985884a6c21dcc9a3c99fb1f5960392 Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Wed, 10 Mar 2021 19:51:57 -0500 Subject: [PATCH 02/11] Continued updates from 1st commit --- modules/multibid/index.js | 247 +++-- modules/multibid/index.md | 5 + modules/rubiconAnalyticsAdapter.js | 8 + modules/rubiconBidAdapter.js | 6 +- src/auction.js | 2 +- src/targeting.js | 2 +- test/spec/modules/multibid_spec.js | 860 ++++++++++++++++++ .../modules/rubiconAnalyticsAdapter_spec.js | 26 + test/spec/modules/rubiconBidAdapter_spec.js | 140 +++ 9 files changed, 1207 insertions(+), 89 deletions(-) create mode 100644 test/spec/modules/multibid_spec.js diff --git a/modules/multibid/index.js b/modules/multibid/index.js index a3e736d4bde..e760216812f 100644 --- a/modules/multibid/index.js +++ b/modules/multibid/index.js @@ -1,49 +1,61 @@ - +/** + * This module adds Multibid support to prebid.js + * @module modules/multibid + */ import {config} from '../../src/config.js'; -import {setupBeforeHookFnOnce} from '../../src/hook.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, addBidResponse} from '../../src/auction.js'; -import {getHighestCpmBidsFromBidPool, sortByDealAndPriceBucketOrCpm, targeting} from '../../src/targeting.js'; +import {addBidderRequests} from '../../src/auction.js'; +import {getHighestCpmBidsFromBidPool, sortByDealAndPriceBucketOrCpm} from '../../src/targeting.js'; -/** @type {string} */ const MODULE_NAME = 'multibid'; +let hasMultibid = false; let multiConfig = {}; let multibidUnits = {}; -let multibidCache = {}; -config.getConfig(MODULE_NAME, conf => { - if (!Array.isArray(conf.multibid) || !validateMultibid(conf.multibid)) return; +// 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(); + resetMultiConfig(); + hasMultibid = true; - conf.multibid.forEach(entry => { - multiConfig[entry.bidder] = { - maxbids: entry.maxbids, - prefix: entry.targetbiddercodeprefix - } - }); + conf.multibid.forEach(entry => { + multiConfig[entry.bidder] = { + maxbids: entry.maxbids, + prefix: entry.targetbiddercodeprefix + } + }); }); -events.on(CONSTANTS.EVENTS.BIDDER_DONE, removeBidderBidData); - -function validateMultibid(conf) { +/** + * @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') { utils.logWarn('Filtering multibid entry due to missing required bidder 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; - } + } + + return entry; }); if (!check) config.setConfig({multibid: duplicate}); @@ -51,91 +63,154 @@ function validateMultibid(conf) { return check; } -function addResponses(fn, adUnitCode, bid) { - if (multiConfig && multiConfig[bid.bidderCode]) { - if (multiConfig[bid.bidderCode].prefix) bid.multibidPrefix = multiConfig[bid.bidderCode].prefix; - bid.originalBidder = bid.bidderCode; - if (utils.deepAccess(multibidUnits, `${adUnitCode}.${bid.bidderCode}`)) { - if (!multibidUnits[adUnitCode][bid.bidderCode].maxReached) { - multibidUnits[adUnitCode][bid.bidderCode].ads.push(bid); +/** + * @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; + }) - if (multibidUnits[adUnitCode][bid.bidderCode].ads.length === multiConfig[bid.bidderCode].maxbids) multibidUnits[adUnitCode][bid.bidderCode].maxReached = true; + 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 + 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.deepSetValue(multibidUnits, `${adUnitCode}.${bid.bidderCode}`, {ads: [bid]}); fn.call(this, adUnitCode, bid); } } 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]}); fn.call(this, adUnitCode, bid); } -} - -function removeBidderBidData(bidRequest) { - bidRequest.bids.forEach(bid => { - if(utils.deepAccess(multibidUnits, `${bid.adUnitCode}.${bid.bidder}`)) delete multibidUnits[bid.adUnitCode][bid.bidder]; - }); + } 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 -**/ -function sortByMultibid(a, b) { - if (a.bidder !== a.bidderCode && b.bidder === b.bidderCode) { - return 1; - } +*/ +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; - } + if (a.bidder === a.bidderCode && b.bidder !== b.bidderCode) { + return -1; + } - return 0; + return 0; } -function targetBidPoolHook(fn, bidsReceived, highestCpmCallback, adUnitBidLimit = 0, hasModified = false) { - 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 = []; - let bidsByBidderName = utils.groupBy(buckets[slotId], 'originalBidder'); - let adjustedBids = [].concat.apply([], Object.keys(bidsByBidderName).map(key => { - 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) => { - if (utils.deepAccess(multiConfig, `${bid.bidderCode}.prefix`) && index !== 0 && index < multiConfig[bid.bidderCode].maxbids) { - bid.bidderCode = multiConfig[bid.bidderCode].prefix + (index + 1); - } - - return bid - }) - })); - 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); - } +/** + * @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); - }, [])); + return [].concat.apply([], modifiedBids); + }, [])); - fn.call(this, bids, highestCpmCallback, adUnitBidLimit, true); + fn.call(this, bids, highestCpmCallback, adUnitBidLimit, true); + } else { + fn.call(this, bidsReceived, highestCpmCallback, adUnitBidLimit); + } } -const resetMultiConfig = () => multiConfig = {}; +/** +* Resets globally stored multibid configuration +*/ +export const resetMultiConfig = () => { hasMultibid = false; multiConfig = {}; }; + +/** +* Resets globally stored multibid ad unit bids +*/ +export const resetMultibidUnits = () => multibidUnits = {}; -export function init() { - setupBeforeHookFnOnce(addBidResponse, addResponses); +/** +* 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); } diff --git a/modules/multibid/index.md b/modules/multibid/index.md index 84c6d1f5551..93fa2f5e144 100644 --- a/modules/multibid/index.md +++ b/modules/multibid/index.md @@ -6,7 +6,12 @@ Purpose: To expand the number of key value pairs going to the ad server in the n # 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 diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index f216cbd6235..6d721cb6fc6 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -118,6 +118,7 @@ function sendMessage(auctionId, bidWonId) { function formatBid(bid) { return utils.pick(bid, [ 'bidder', + 'bidderDetail', 'bidId', bidId => utils.deepAccess(bid, 'bidResponse.pbsBidId') || utils.deepAccess(bid, 'bidResponse.seatBidId') || bidId, 'status', 'error', @@ -662,6 +663,13 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { break; case BID_RESPONSE: let auctionEntry = cache.auctions[args.auctionId]; + + if (!auctionEntry.bids[args.requestId] && args.originalRequestId) { + auctionEntry.bids[args.requestId] = {...auctionEntry.bids[args.originalRequestId]}; + auctionEntry.bids[args.requestId].bidId = args.requestId; + auctionEntry.bids[args.requestId].bidderDetail = args.targetingBidder; + } + let bid = auctionEntry.bids[args.requestId]; // If floor resolved gptSlot but we have not yet, then update the adUnit to have the adSlot name if (!utils.deepAccess(bid, 'adUnit.gam.adSlot') && utils.deepAccess(args, 'floorData.matchedFields.gptSlot')) { diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 7297c82440f..4ace826cf0d 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -640,6 +640,8 @@ export const spec = { } let ads = responseObj.ads; + let lastImpId; + let multibid = 0; // video ads array is wrapped in an object if (typeof bidRequest === 'object' && !Array.isArray(bidRequest) && bidType(bidRequest) === 'video' && typeof ads === 'object') { @@ -652,12 +654,14 @@ export const spec = { } return ads.reduce((bids, ad, i) => { + (ad.impression_id && lastImpId === ad.impression_id) ? multibid++ : lastImpId = ad.impression_id; + if (ad.status !== 'ok') { return bids; } // associate bidRequests; assuming ads matches bidRequest - const associatedBidRequest = Array.isArray(bidRequest) ? bidRequest[i] : bidRequest; + const associatedBidRequest = Array.isArray(bidRequest) ? bidRequest[i - multibid] : bidRequest; if (associatedBidRequest && typeof associatedBidRequest === 'object') { let bid = { diff --git a/src/auction.js b/src/auction.js index ec9ff7d8209..217a50be3d6 100644 --- a/src/auction.js +++ b/src/auction.js @@ -519,7 +519,7 @@ function getPreparedBidForAuction({adUnitCode, bid, bidderRequest, auctionId}) { events.emit(CONSTANTS.EVENTS.BID_ADJUSTMENT, bidObject); // a publisher-defined renderer can be used to render bids - const bidReq = bidderRequest.bids && find(bidderRequest.bids, bid => bid.adUnitCode == adUnitCode); + const bidReq = bidderRequest.bids && find(bidderRequest.bids, bid => bid.adUnitCode == adUnitCode && bid.bidId == bidObject.requestId); const adUnitRenderer = bidReq && bidReq.renderer; // a publisher can also define a renderer for a mediaType diff --git a/src/targeting.js b/src/targeting.js index acc005a6ffa..365453e1e8f 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -56,7 +56,7 @@ export const getHighestCpmBidsFromBidPool = hook('sync', function(bidsReceived, return bids; } - + return bidsReceived; }) diff --git a/test/spec/modules/multibid_spec.js b/test/spec/modules/multibid_spec.js new file mode 100644 index 00000000000..1ee13cead6d --- /dev/null +++ b/test/spec/modules/multibid_spec.js @@ -0,0 +1,860 @@ +import {expect} from 'chai'; +import { + validateMultibid, + adjustBidderRequestsHook, + addBidResponseHook, + resetMultibidUnits, + sortByMultibid, + targetBidPoolHook, + resetMultiConfig +} from 'modules/multibid/index.js'; +import {parse as parseQuery} from 'querystring'; +import {config} from 'src/config.js'; +import * as utils from 'src/utils.js'; +import find from 'core-js-pure/features/array/find.js'; + +describe('multibid adapter', function () { + let sandbox, + logErrorSpy; + let logErrorStub; + let logWarnStub; + let logInfoStub; + let bidArray = [{ + 'bidderCode': 'bidderA', + 'requestId': '1c5f0a05d3629a', + 'cpm': 75, + 'originalCpm': 75, + 'bidder': 'bidderA', + }, { + 'bidderCode': 'bidderA', + 'cpm': 52, + 'requestId': '2e6j8s05r4363h', + 'originalCpm': 52, + 'bidder': 'bidderA', + }]; + let bidCacheArray = [{ + 'bidderCode': 'bidderA', + 'requestId': '1c5f0a05d3629a', + 'cpm': 66, + 'originalCpm': 66, + 'bidder': 'bidderA', + 'originalBidder': 'bidderA', + 'multibidPrefix': 'bidA' + }, { + 'bidderCode': 'bidderA', + 'cpm': 38, + 'requestId': '2e6j8s05r4363h', + 'originalCpm': 38, + 'bidder': 'bidderA', + 'originalBidder': 'bidderA', + 'multibidPrefix': 'bidA' + }]; + let bidArrayAlt = [{ + 'bidderCode': 'bidderA', + 'requestId': '1c5f0a05d3629a', + 'cpm': 29, + 'originalCpm': 29, + 'bidder': 'bidderA' + }, { + 'bidderCode': 'bidderA', + 'cpm': 52, + 'requestId': '2e6j8s05r4363h', + 'originalCpm': 52, + 'bidder': 'bidderA' + }, { + 'bidderCode': 'bidderB', + 'cpm': 3, + 'requestId': '7g8h5j45l7654i', + 'originalCpm': 3, + 'bidder': 'bidderB' + }, { + 'bidderCode': 'bidderC', + 'cpm': 12, + 'requestId': '9d7f4h56t6483u', + 'originalCpm': 12, + 'bidder': 'bidderC' + }]; + let bidderRequests = [{ + 'bidderCode': 'bidderA', + 'auctionId': 'e6bd4400-28fc-459b-9905-ad64d044daaa', + 'bidderRequestId': '10e78266423c0e', + 'bids': [{ + 'bidder': 'bidderA', + 'params': {'placementId': 1234567}, + 'crumbs': {'pubcid': 'fb4cfc66-ff3d-4fda-bef8-3f2cb6fe9412'}, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'adUnitCode': 'test-div', + 'transactionId': 'c153f3da-84f0-4be8-95cb-0647c458bc60', + 'sizes': [[300, 250]], + 'bidId': '2408ef83b84c9d', + 'bidderRequestId': '10e78266423c0e', + 'auctionId': 'e6bd4400-28fc-459b-9905-ad64d044daaa', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }] + }, { + 'bidderCode': 'bidderB', + 'auctionId': 'e6bd4400-28fc-459b-9905-ad64d044daaa', + 'bidderRequestId': '10e78266423c0e', + 'bids': [{ + 'bidder': 'bidderB', + 'params': {'placementId': 1234567}, + 'crumbs': {'pubcid': 'fb4cfc66-ff3d-4fda-bef8-3f2cb6fe9412'}, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'adUnitCode': 'test-div', + 'transactionId': 'c153f3da-84f0-4be8-95cb-0647c458bc60', + 'sizes': [[300, 250]], + 'bidId': '2408ef83b84c9d', + 'bidderRequestId': '10e78266423c0e', + 'auctionId': 'e6bd4400-28fc-459b-9905-ad64d044daaa', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }] + }]; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + logErrorSpy = sinon.spy(utils, 'logError'); + }); + + afterEach(function () { + sandbox.restore(); + utils.logError.restore(); + config.resetConfig(); + resetMultiConfig(); + resetMultibidUnits(); + }); + + describe('adjustBidderRequestsHook', function () { + let result; + let callbackFn = function (bidderRequests) { + result = bidderRequests; + }; + + beforeEach(function() { + result = null; + logWarnStub = sinon.stub(utils, 'logWarn'); + }); + + afterEach(function() { + logWarnStub.restore(); + }); + + it('does not modify bidderRequest when no multibid config exists', function () { + let bidRequests = [{...bidderRequests[0]}]; + + adjustBidderRequestsHook(callbackFn, bidRequests); + + expect(result).to.not.equal(null); + expect(result).to.deep.equal(bidRequests); + }); + + it('does modify bidderRequest when multibid config exists', function () { + let bidRequests = [{...bidderRequests[0]}]; + + config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2}]}); + + adjustBidderRequestsHook(callbackFn, [{...bidderRequests[0]}]); + + expect(result).to.not.equal(null); + expect(result).to.not.deep.equal(bidRequests); + expect(result[0].bidLimit).to.equal(2); + }); + + it('does only modifies bidderRequest when multibid config exists for bidder', function () { + let bidRequests = [{...bidderRequests[0]}, {...bidderRequests[1]}]; + + config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2}]}); + + adjustBidderRequestsHook(callbackFn, [{...bidderRequests[0]}, {...bidderRequests[1]}]); + + expect(result).to.not.equal(null); + expect(result[0]).to.not.deep.equal(bidRequests[0]); + expect(result[0].bidLimit).to.equal(2); + expect(result[1]).to.deep.equal(bidRequests[1]); + expect(result[1].bidLimit).to.equal(undefined); + }); + }); + + describe('addBidResponseHook', function () { + let result; + let callbackFn = function (adUnitCode, bid) { + result = { + 'adUnitCode': adUnitCode, + 'bid': bid + }; + }; + + beforeEach(function() { + result = null; + logWarnStub = sinon.stub(utils, 'logWarn'); + }); + + afterEach(function() { + logWarnStub.restore(); + }); + + it('adds original bids and does not modify', function () { + let adUnitCode = 'test-div'; + let bids = [{...bidArray[0]}, {...bidArray[1]}]; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[0]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[1]}); + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[1]); + }); + + it('modifies and adds both bids based on multibid configuration', function () { + let adUnitCode = 'test-div'; + let bids = [{...bidArray[0]}, {...bidArray[1]}]; + + config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}]}); + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + bids[0].multibidPrefix = 'bidA'; + bids[0].originalBidder = 'bidderA'; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[0]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[1]}); + + bids[1].multibidPrefix = 'bidA'; + bids[1].originalBidder = 'bidderA'; + bids[1].targetingBidder = 'bidA2'; + bids[1].originalRequestId = '2e6j8s05r4363h'; + + delete bids[1].requestId; + delete result.bid.requestId; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[1]); + }); + + it('only modifies bids defined in the multibid configuration', function () { + let adUnitCode = 'test-div'; + let bids = [{...bidArray[0]}, {...bidArray[1]}]; + + bids.push({ + 'bidderCode': 'bidderB', + 'cpm': 33, + 'requestId': '1j8s5f89y2345l', + 'originalCpm': 33, + 'bidder': 'bidderB', + }); + + config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}]}); + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + bids[0].multibidPrefix = 'bidA'; + bids[0].originalBidder = 'bidderA'; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[0]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[1]}); + + bids[1].multibidPrefix = 'bidA'; + bids[1].originalBidder = 'bidderA'; + bids[1].targetingBidder = 'bidA2'; + bids[1].originalRequestId = '2e6j8s05r4363h'; + bids[1].requestId = result.bid.requestId; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[1]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[2]}); + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[2]); + }); + + it('only modifies and returns bids under limit for a specifc bidder in the multibid configuration', function () { + let adUnitCode = 'test-div'; + let bids = [{...bidArray[0]}, {...bidArray[1]}]; + + bids.push({ + 'bidderCode': 'bidderA', + 'cpm': 33, + 'requestId': '1j8s5f89y2345l', + 'originalCpm': 33, + 'bidder': 'bidderA', + }); + + config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}]}); + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + bids[0].multibidPrefix = 'bidA'; + bids[0].originalBidder = 'bidderA'; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[0]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[1]}); + + bids[1].multibidPrefix = 'bidA'; + bids[1].originalBidder = 'bidderA'; + bids[1].targetingBidder = 'bidA2'; + bids[1].originalRequestId = '2e6j8s05r4363h'; + bids[1].requestId = result.bid.requestId; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[1]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[2]}); + + expect(result).to.equal(null); + }); + + it('if no prefix in multibid configuration, modifies and returns bids under limit without preifx property', function () { + let adUnitCode = 'test-div'; + let bids = [{...bidArray[0]}, {...bidArray[1]}]; + + bids.push({ + 'bidderCode': 'bidderA', + 'cpm': 33, + 'requestId': '1j8s5f89y2345l', + 'originalCpm': 33, + 'bidder': 'bidderA', + }); + + config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2}]}); + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + bids[0].originalBidder = 'bidderA'; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[0]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[1]}); + + bids[1].originalBidder = 'bidderA'; + bids[1].originalRequestId = '2e6j8s05r4363h'; + bids[1].requestId = result.bid.requestId; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[1]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[2]}); + + expect(result).to.equal(null); + }); + + it('does not include extra bids if cpm is less than floor value', function () { + let adUnitCode = 'test-div'; + let bids = [{...bidArrayAlt[1]}, {...bidArrayAlt[0]}, {...bidArrayAlt[2]}, {...bidArrayAlt[3]}]; + + bids.map(bid => { + bid.floorData = { + cpmAfterAdjustments: bid.cpm, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + floorCurrency: 'USD', + floorRule: '*|banner', + floorRuleValue: 65, + floorValue: 65, + matchedFields: { + gptSlot: 'test-div', + mediaType: 'banner' + } + } + + return bid; + }); + + config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}]}); + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + bids[0].multibidPrefix = 'bidA'; + bids[0].originalBidder = 'bidderA'; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid.bidder).to.equal('bidderA'); + expect(result.bid.targetingBidder).to.equal(undefined); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[1]}); + + expect(result).to.equal(null); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[2]}); + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid.bidder).to.equal('bidderB'); + expect(result.bid.targetingBidder).to.equal(undefined); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[3]}); + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid.bidder).to.equal('bidderC'); + expect(result.bid.targetingBidder).to.equal(undefined); + }); + + it('does include extra bids if cpm is not less than floor value', function () { + let adUnitCode = 'test-div'; + let bids = [{...bidArrayAlt[1]}, {...bidArrayAlt[0]}]; + + bids.map(bid => { + bid.floorData = { + cpmAfterAdjustments: bid.cpm, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + floorCurrency: 'USD', + floorRule: '*|banner', + floorRuleValue: 25, + floorValue: 25, + matchedFields: { + gptSlot: 'test-div', + mediaType: 'banner' + } + } + + return bid; + }); + + config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}]}); + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + bids[0].multibidPrefix = 'bidA'; + bids[0].originalBidder = 'bidderA'; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid.bidder).to.equal('bidderA'); + expect(result.bid.targetingBidder).to.equal(undefined); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + bids[0].multibidPrefix = 'bidA'; + bids[0].originalBidder = 'bidderA'; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid.bidder).to.equal('bidderA'); + expect(result.bid.targetingBidder).to.equal('bidA2'); + }); + }); + + describe('targetBidPoolHook', function () { + let result; + let bidResult; + let callbackFn = function (bidsReceived, highestCpmCallback, adUnitBidLimit = 0, hasModified = false) { + result = { + 'bidsReceived': bidsReceived, + 'adUnitBidLimit': adUnitBidLimit, + 'hasModified': hasModified + }; + }; + let bidResponseCallback = function (adUnitCode, bid) { + bidResult = bid; + }; + + beforeEach(function() { + result = null; + bidResult = null; + logWarnStub = sinon.stub(utils, 'logWarn'); + }); + + afterEach(function() { + logWarnStub.restore(); + }); + + it('it does not run filter on bidsReceived if no multibid configuration found', function () { + let bids = [{...bidArray[0]}, {...bidArray[1]}]; + targetBidPoolHook(callbackFn, bids, utils.getHighestCpm); + + expect(result).to.not.equal(null); + expect(result.bidsReceived).to.not.equal(null); + expect(result.bidsReceived.length).to.equal(2); + expect(result.bidsReceived).to.deep.equal(bids); + expect(result.adUnitBidLimit).to.not.equal(null); + expect(result.adUnitBidLimit).to.equal(0); + expect(result.hasModified).to.not.equal(null); + expect(result.hasModified).to.equal(false); + }); + + it('it does filter on bidsReceived if multibid configuration found with no prefix', function () { + let bids = [{...bidArray[0]}, {...bidArray[1]}]; + + config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2}]}); + + targetBidPoolHook(callbackFn, bids, utils.getHighestCpm); + bids.pop(); + + expect(result).to.not.equal(null); + expect(result.bidsReceived).to.not.equal(null); + expect(result.bidsReceived.length).to.equal(1); + expect(result.bidsReceived).to.deep.equal(bids); + expect(result.adUnitBidLimit).to.not.equal(null); + expect(result.adUnitBidLimit).to.equal(0); + expect(result.hasModified).to.not.equal(null); + expect(result.hasModified).to.equal(true); + }); + + it('it sorts and creates dynamic alias on bidsReceived if multibid configuration found with prefix', function () { + let modifiedBids = [{...bidArray[1]}, {...bidArray[0]}].map(bid => { + addBidResponseHook(bidResponseCallback, 'test-div', {...bid}); + + return bidResult; + }); + + config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}]}); + + targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm); + + expect(result).to.not.equal(null); + expect(result.bidsReceived).to.not.equal(null); + expect(result.bidsReceived.length).to.equal(2); + expect(result.bidsReceived).to.deep.equal([modifiedBids[1], modifiedBids[0]]); + expect(result.bidsReceived[0].bidderCode).to.equal('bidderA'); + expect(result.bidsReceived[0].bidder).to.equal('bidderA'); + expect(result.bidsReceived[1].bidderCode).to.equal('bidA2'); + expect(result.bidsReceived[1].bidder).to.equal('bidderA'); + expect(result.adUnitBidLimit).to.not.equal(null); + expect(result.adUnitBidLimit).to.equal(0); + expect(result.hasModified).to.not.equal(null); + expect(result.hasModified).to.equal(true); + }); + + it('it sorts by cpm treating dynamic alias as unique bid when no bid limit defined', function () { + let modifiedBids = [{...bidArrayAlt[0]}, {...bidArrayAlt[2]}, {...bidArrayAlt[3]}, {...bidArrayAlt[1]}].map(bid => { + addBidResponseHook(bidResponseCallback, 'test-div', {...bid}); + + return bidResult; + }); + + config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}]}); + + targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm); + + expect(result).to.not.equal(null); + expect(result.bidsReceived).to.not.equal(null); + expect(result.bidsReceived.length).to.equal(4); + expect(result.bidsReceived).to.deep.equal([modifiedBids[3], modifiedBids[0], modifiedBids[2], modifiedBids[1]]); + expect(result.bidsReceived[0].bidderCode).to.equal('bidderA'); + expect(result.bidsReceived[0].bidder).to.equal('bidderA'); + expect(result.bidsReceived[0].cpm).to.equal(52); + expect(result.bidsReceived[1].bidderCode).to.equal('bidA2'); + expect(result.bidsReceived[1].bidder).to.equal('bidderA'); + expect(result.bidsReceived[1].cpm).to.equal(29); + expect(result.bidsReceived[2].bidderCode).to.equal('bidderC'); + expect(result.bidsReceived[2].bidder).to.equal('bidderC'); + expect(result.bidsReceived[2].cpm).to.equal(12); + expect(result.bidsReceived[3].bidderCode).to.equal('bidderB'); + expect(result.bidsReceived[3].bidder).to.equal('bidderB'); + expect(result.bidsReceived[3].cpm).to.equal(3); + expect(result.adUnitBidLimit).to.not.equal(null); + expect(result.adUnitBidLimit).to.equal(0); + expect(result.hasModified).to.not.equal(null); + expect(result.hasModified).to.equal(true); + }); + + it('it should filter out dynamic bid when bid limit is less than unique bid pool', function () { + let modifiedBids = [{...bidArrayAlt[0]}, {...bidArrayAlt[2]}, {...bidArrayAlt[3]}, {...bidArrayAlt[1]}].map(bid => { + addBidResponseHook(bidResponseCallback, 'test-div', {...bid}); + + return bidResult; + }); + + config.setConfig({ multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}] }); + + targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm, 3); + + expect(result).to.not.equal(null); + expect(result.bidsReceived).to.not.equal(null); + expect(result.bidsReceived.length).to.equal(3); + expect(result.bidsReceived).to.deep.equal([modifiedBids[3], modifiedBids[2], modifiedBids[1]]); + expect(result.bidsReceived[0].bidderCode).to.equal('bidderA'); + expect(result.bidsReceived[1].bidderCode).to.equal('bidderC'); + expect(result.bidsReceived[2].bidderCode).to.equal('bidderB'); + expect(result.adUnitBidLimit).to.not.equal(null); + expect(result.adUnitBidLimit).to.equal(3); + expect(result.hasModified).to.not.equal(null); + expect(result.hasModified).to.equal(true); + }); + + it('it should collect all bids from auction and bid cache then sort and filter', function () { + config.setConfig({ multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}] }); + + let modifiedBids = [{...bidArrayAlt[0]}, {...bidArrayAlt[2]}, {...bidArrayAlt[3]}, {...bidArrayAlt[1]}].map(bid => { + addBidResponseHook(bidResponseCallback, 'test-div', {...bid}); + + return bidResult; + }); + + let bidPool = [].concat.apply(modifiedBids, [{...bidCacheArray[0]}, {...bidCacheArray[1]}]); + + expect(bidPool.length).to.equal(6); + + targetBidPoolHook(callbackFn, bidPool, utils.getHighestCpm); + + expect(result).to.not.equal(null); + expect(result.bidsReceived).to.not.equal(null); + expect(result.bidsReceived.length).to.equal(4); + expect(result.bidsReceived).to.deep.equal([bidPool[4], bidPool[3], bidPool[2], bidPool[1]]); + expect(result.bidsReceived[0].bidderCode).to.equal('bidderA'); + expect(result.bidsReceived[1].bidderCode).to.equal('bidA2'); + expect(result.bidsReceived[2].bidderCode).to.equal('bidderC'); + expect(result.bidsReceived[3].bidderCode).to.equal('bidderB'); + expect(result.adUnitBidLimit).to.not.equal(null); + expect(result.adUnitBidLimit).to.equal(0); + expect(result.hasModified).to.not.equal(null); + expect(result.hasModified).to.equal(true); + }); + }); + + describe('validate multibid', function () { + it('should fail validation for missing bidder name in entry', function () { + let conf = [{maxbids: 1}]; + let result = validateMultibid(conf); + + expect(result).to.equal(false); + }); + + it('should pass validation on all multibid entries', function () { + let conf = [{bidder: 'bidderA', maxbids: 1}, {bidder: 'bidderB', maxbids: 2}]; + let result = validateMultibid(conf); + + expect(result).to.equal(true); + }); + + it('should fail validation for maxbids less than 1 in entry', function () { + let conf = [{bidder: 'bidderA', maxbids: 0}, {bidder: 'bidderB', maxbids: 2}]; + let result = validateMultibid(conf); + + expect(result).to.equal(false); + }); + + it('should fail validation for maxbids greater than 9 in entry', function () { + let conf = [{bidder: 'bidderA', maxbids: 10}, {bidder: 'bidderB', maxbids: 2}]; + let result = validateMultibid(conf); + + expect(result).to.equal(false); + }); + + it('should add multbid entries to global config', function () { + config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 1}]}); + let conf = config.getConfig('multibid'); + + expect(conf).to.deep.equal([{bidder: 'bidderA', maxbids: 1}]); + }); + + it('should modify multbid entries and add to global config', function () { + config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 0}, {bidder: 'bidderB', maxbids: 15}]}); + let conf = config.getConfig('multibid'); + + expect(conf).to.deep.equal([{bidder: 'bidderA', maxbids: 1}, {bidder: 'bidderB', maxbids: 9}]); + }); + + it('should filter multbid entry and add modified to global config', function () { + config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 0}, {maxbids: 15}]}); + let conf = config.getConfig('multibid'); + + expect(conf.length).to.equal(1); + expect(conf).to.deep.equal([{bidder: 'bidderA', maxbids: 1}]); + }); + }); + + describe('sort multibid', function () { + it('should not alter order', function () { + let bids = [{ + 'bidderCode': 'bidderA', + 'cpm': 75, + 'originalCpm': 75, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }, { + 'bidderCode': 'bidA2', + 'cpm': 75, + 'originalCpm': 75, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }]; + + let expected = [{ + 'bidderCode': 'bidderA', + 'cpm': 75, + 'originalCpm': 75, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }, { + 'bidderCode': 'bidA2', + 'cpm': 75, + 'originalCpm': 75, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }]; + let result = bids.sort(sortByMultibid); + + expect(result).to.deep.equal(expected); + }); + + it('should sort dynamic alias bidders to end', function () { + let bids = [{ + 'bidderCode': 'bidA2', + 'cpm': 75, + 'originalCpm': 75, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }, { + 'bidderCode': 'bidderA', + 'cpm': 22, + 'originalCpm': 22, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }, { + 'bidderCode': 'bidderB', + 'cpm': 4, + 'originalCpm': 4, + 'multibidPrefix': 'bidB', + 'originalBidder': 'bidderB', + 'bidder': 'bidderB', + }, { + 'bidderCode': 'bidB', + 'cpm': 2, + 'originalCpm': 2, + 'multibidPrefix': 'bidB', + 'originalBidder': 'bidderB', + 'bidder': 'bidderB', + }]; + let expected = [{ + 'bidderCode': 'bidderA', + 'cpm': 22, + 'originalCpm': 22, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }, { + 'bidderCode': 'bidderB', + 'cpm': 4, + 'originalCpm': 4, + 'multibidPrefix': 'bidB', + 'originalBidder': 'bidderB', + 'bidder': 'bidderB', + }, { + 'bidderCode': 'bidA2', + 'cpm': 75, + 'originalCpm': 75, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }, { + 'bidderCode': 'bidB', + 'cpm': 2, + 'originalCpm': 2, + 'multibidPrefix': 'bidB', + 'originalBidder': 'bidderB', + 'bidder': 'bidderB', + }]; + let result = bids.sort(sortByMultibid); + + expect(result).to.deep.equal(expected); + }); + }); +}); diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 71e5446ed06..c15cf2a6b26 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -1698,6 +1698,32 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].pattern).to.equal('1234/mycoolsite/*&gpt_skyscraper&deviceType=mobile'); }); + it('should pass bidderDetail for multibid auctions', function () { + let bidResponse = utils.deepClone(MOCK.BID_RESPONSE[1]); + bidResponse.targetingBidder = 'rubi2'; + bidResponse.originalRequestId = bidResponse.requestId; + bidResponse.requestId = '1a2b3c4d5e6f7g8h9'; + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, bidResponse); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + + clock.tick(SEND_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + + let message = JSON.parse(server.requests[0].requestBody); + validate(message); + + expect(message.auctions[0].adUnits[1].bids[1].bidder).to.equal('rubicon'); + expect(message.auctions[0].adUnits[1].bids[1].bidderDetail).to.equal('rubi2'); + }); + it('should successfully convert bid price to USD in parseBidResponse', function () { // Set the rates setConfig({ diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 8c25d97dada..c3db6c1f085 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -2518,6 +2518,146 @@ describe('the rubicon adapter', function () { expect(bids[0].cpm).to.be.equal(0); }); + it('should create bids with matching requestIds if imp id matches', function () { + let bidRequests = [{ + 'bidder': 'rubicon', + 'params': { + 'accountId': 1001, + 'siteId': 12345, + 'zoneId': 67890, + 'floor': null + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '404a7b28-f276-41cc-a5cf-c1d3dc5671f9', + 'sizes': [[300, 250]], + 'bidId': '557ba307cef098', + 'bidderRequestId': '46a00704ffeb7', + 'auctionId': '3fdc6494-da94-44a0-a292-b55a90b08b2c', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'startTime': 1615412098213 + }, { + 'bidder': 'rubicon', + 'params': { + 'accountId': 1001, + 'siteId': 12345, + 'zoneId': 67890, + 'floor': null + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-1', + 'transactionId': '404a7b28-f276-41cc-a5cf-c1d3dc5671f9', + 'sizes': [[300, 250]], + 'bidId': '456gt123jkl098', + 'bidderRequestId': '46a00704ffeb7', + 'auctionId': '3fdc6494-da94-44a0-a292-b55a90b08b2c', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'startTime': 1615412098213 + }]; + + let response = { + 'status': 'ok', + 'account_id': 14062, + 'site_id': 70608, + 'zone_id': 530022, + 'size_id': 15, + 'alt_size_ids': [ + 43 + ], + 'tracking': '', + 'inventory': {}, + 'ads': [ + { + 'status': 'ok', + 'impression_id': '153dc240-8229-4604-b8f5-256933b9374c', + 'size_id': '15', + 'ad_id': '6', + 'advertiser': 7, + 'network': 8, + 'creative_id': 'crid-9', + 'type': 'script', + 'script': 'alert(\'foo\')', + 'campaign_id': 10, + 'cpm': 0.811, + 'targeting': [ + { + 'key': 'rpfl_14062', + 'values': [ + '15_tier_all_test' + ] + } + ] + }, + { + 'status': 'ok', + 'impression_id': '153dc240-8229-4604-b8f5-256933b9374c', + 'size_id': '15', + 'ad_id': '7', + 'advertiser': 7, + 'network': 8, + 'creative_id': 'crid-9', + 'type': 'script', + 'script': 'alert(\'foo\')', + 'campaign_id': 10, + 'cpm': 0.911, + 'targeting': [ + { + 'key': 'rpfl_14062', + 'values': [ + '43_tier_all_test' + ] + } + ] + }, + { + 'status': 'ok', + 'impression_id': '153dc240-8229-4604-b8f5-256933b9374d', + 'size_id': '43', + 'ad_id': '7', + 'advertiser': 7, + 'network': 8, + 'creative_id': 'crid-9', + 'type': 'script', + 'script': 'alert(\'foo\')', + 'campaign_id': 10, + 'cpm': 1.911, + 'targeting': [ + { + 'key': 'rpfl_14062', + 'values': [ + '43_tier_all_test' + ] + } + ] + } + ] + }; + + config.setConfig({ multibid: [{bidder: 'rubicon', maxbids: 2, targetbiddercodeprefix: 'rubi'}] }); + + let bids = spec.interpretResponse({body: response}, { + bidRequest: bidRequests + }); + + expect(bids).to.be.lengthOf(3); + expect(bids[0].requestId).to.not.equal(bids[1].requestId); + expect(bids[1].requestId).to.equal(bids[2].requestId); + }); + it('should handle an error with no ads returned', function () { let response = { 'status': 'ok', From 919faef89eb845c7960eddf9a4cf69ec1ad44bc8 Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Wed, 10 Mar 2021 20:17:03 -0500 Subject: [PATCH 03/11] Adding logWarn for filtered bids --- modules/multibid/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/multibid/index.js b/modules/multibid/index.js index e760216812f..be347232706 100644 --- a/modules/multibid/index.js +++ b/modules/multibid/index.js @@ -112,11 +112,15 @@ export function addBidResponseHook(fn, adUnitCode, bid) { 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 { From c100592fa07281f8e39323d8f340d5ed0da93e24 Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Thu, 11 Mar 2021 07:46:43 -0500 Subject: [PATCH 04/11] Update to include passing multibid configuration to PBS requests --- modules/prebidServerBidAdapter/index.js | 5 +++++ modules/rubiconBidAdapter.js | 5 +++++ .../modules/prebidServerBidAdapter_spec.js | 17 ++++++++++++++++ test/spec/modules/rubiconBidAdapter_spec.js | 20 +++++++++++++++++++ 4 files changed, 47 insertions(+) diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 088b5430f46..691bdf36457 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -743,6 +743,11 @@ const OPEN_RTB_PROTOCOL = { utils.deepSetValue(request, 'ext.prebid.data.eidpermissions', eidPermissions); } + const multibid = config.getConfig('multibid'); + if (multibid) { + utils.deepSetValue(request, 'ext.prebid.multibid', multibid); + } + if (bidRequests) { if (firstBidRequest.gdprConsent) { // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 602542b0041..59f649cbb9b 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -256,6 +256,11 @@ export const spec = { utils.deepSetValue(data, 'source.ext.schain', bidRequest.schain); } + const multibid = config.getConfig('multibid'); + if (multibid) { + utils.deepSetValue(data, 'ext.prebid.multibid', multibid); + } + applyFPD(bidRequest, VIDEO, data); // if storedAuctionResponse has been set, pass SRID diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index babee7e10d7..006b32a4994 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -1575,6 +1575,23 @@ describe('S2S Adapter', function () { expect(parsedRequestBody.source.ext.schain).to.deep.equal(schainObject); }); + it('passes multibid array in request', function () { + const bidRequests = utils.deepClone(BID_REQUESTS); + const multibid = [{ + bidder: 'bidderA', + maxbids: 2 + }, { + bidder: 'bidderB', + maxbids: 2 + }]; + + config.setConfig({multibid: multibid}); + + adapter.callBids(REQUEST, bidRequests, addBidResponse, done, ajax); + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + expect(parsedRequestBody.ext.prebid.multibid).to.deep.equal(multibid); + }); + it('passes first party data in request', () => { const s2sBidRequest = utils.deepClone(REQUEST); const bidRequests = utils.deepClone(BID_REQUESTS); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 9100de1f4ea..d065a71b747 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -1626,6 +1626,26 @@ describe('the rubicon adapter', function () { expect(request.data.imp[0].ext).to.not.haveOwnProperty('rubicon'); }); + it('should add multibid configuration to PBS Request', function () { + createVideoBidderRequest(); + + const multibid = [{ + bidder: 'bidderA', + maxbids: 2 + }, { + bidder: 'bidderB', + maxbids: 2 + }]; + + config.setConfig({multibid: multibid}); + + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + + // should have the aliases object sent to PBS + expect(request.data.ext.prebid).to.haveOwnProperty('multibid'); + expect(request.data.ext.prebid.multibid).to.deep.equal(multibid); + }); + it('should send video exp param correctly when set', function () { createVideoBidderRequest(); config.setConfig({s2sConfig: {defaultTtl: 600}}); From 0d0ff5a07cb230dba614fd0999336e48f5be886e Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Wed, 17 Mar 2021 12:18:31 -0400 Subject: [PATCH 05/11] Update to rubicon bid adapter to pass query param rp_maxbids value taken from bidderRequest.bidLimit --- modules/rubiconBidAdapter.js | 4 ++++ test/spec/modules/rubiconBidAdapter_spec.js | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 59f649cbb9b..46e34e41c5d 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -515,6 +515,10 @@ export const spec = { data['us_privacy'] = encodeURIComponent(bidderRequest.uspConsent); } + if (bidderRequest.bidLimit) { + data['rp_maxbids'] = bidderRequest.bidLimit; + } + applyFPD(bidRequest, BANNER, data); if (config.getConfig('coppa') === true) { diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index d065a71b747..0fb6c3cb23b 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -488,6 +488,17 @@ describe('the rubicon adapter', function () { data = parseQuery(request.data); expect(data.rp_hard_floor).to.equal('1.23'); }); + + it('should send rp_maxbids to AE if rubicon multibid config exists', function () { + var multibidRequest = utils.deepClone(bidderRequest); + multibidRequest.bidLimit = 5; + + let [request] = spec.buildRequests(multibidRequest.bids, multibidRequest); + let data = parseQuery(request.data); + + expect(data['rp_maxbids']).to.equal('5'); + }); + it('should not send p_pos to AE if not params.position specified', function () { var noposRequest = utils.deepClone(bidderRequest); delete noposRequest.bids[0].params.position; From 4427244e27d0b8c59ad69017868b4953ed2afcee Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Mon, 22 Mar 2021 12:19:22 -0400 Subject: [PATCH 06/11] Update to config to look for camelcase property names according to spec. These convert to all lowercase when passed to PBS endpoint --- modules/multibid/index.js | 8 ++-- modules/prebidServerBidAdapter/index.js | 12 ++++- modules/rubiconBidAdapter.js | 12 ++++- test/spec/modules/multibid_spec.js | 46 +++++++++---------- .../modules/prebidServerBidAdapter_spec.js | 9 +++- test/spec/modules/rubiconBidAdapter_spec.js | 9 +++- 6 files changed, 65 insertions(+), 31 deletions(-) diff --git a/modules/multibid/index.js b/modules/multibid/index.js index be347232706..5feb773e849 100644 --- a/modules/multibid/index.js +++ b/modules/multibid/index.js @@ -25,8 +25,8 @@ config.getConfig(MODULE_NAME, conf => { conf.multibid.forEach(entry => { multiConfig[entry.bidder] = { - maxbids: entry.maxbids, - prefix: entry.targetbiddercodeprefix + maxbids: entry.maxBids, + prefix: entry.targetBiddercodePrefix } }); }); @@ -50,8 +50,8 @@ export function validateMultibid(conf) { }).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; + if (typeof entry.maxBids !== 'number' || entry.maxBids < 1 || entry.maxBids > 9) { + entry.maxBids = (typeof entry.maxBids !== 'number' || entry.maxBids < 1) ? 1 : 9; check = false; } diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 691bdf36457..bd0d21a04b9 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -745,7 +745,17 @@ const OPEN_RTB_PROTOCOL = { const multibid = config.getConfig('multibid'); if (multibid) { - utils.deepSetValue(request, 'ext.prebid.multibid', multibid); + utils.deepSetValue(request, 'ext.prebid.multibid', multibid.reduce((result, i) => { + let obj = {}; + + Object.keys(i).forEach(key => { + obj[key.toLowerCase()] = i[key]; + }); + + result.push(obj); + + return result; + }, [])); } if (bidRequests) { diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 46e34e41c5d..f4308bbe30d 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -258,7 +258,17 @@ export const spec = { const multibid = config.getConfig('multibid'); if (multibid) { - utils.deepSetValue(data, 'ext.prebid.multibid', multibid); + utils.deepSetValue(data, 'ext.prebid.multibid', multibid.reduce((result, i) => { + let obj = {}; + + Object.keys(i).forEach(key => { + obj[key.toLowerCase()] = i[key]; + }); + + result.push(obj); + + return result; + }, [])); } applyFPD(bidRequest, VIDEO, data); diff --git a/test/spec/modules/multibid_spec.js b/test/spec/modules/multibid_spec.js index 1ee13cead6d..7babb32d549 100644 --- a/test/spec/modules/multibid_spec.js +++ b/test/spec/modules/multibid_spec.js @@ -164,7 +164,7 @@ describe('multibid adapter', function () { it('does modify bidderRequest when multibid config exists', function () { let bidRequests = [{...bidderRequests[0]}]; - config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2}]}); + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2}]}); adjustBidderRequestsHook(callbackFn, [{...bidderRequests[0]}]); @@ -176,7 +176,7 @@ describe('multibid adapter', function () { it('does only modifies bidderRequest when multibid config exists for bidder', function () { let bidRequests = [{...bidderRequests[0]}, {...bidderRequests[1]}]; - config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2}]}); + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2}]}); adjustBidderRequestsHook(callbackFn, [{...bidderRequests[0]}, {...bidderRequests[1]}]); @@ -233,7 +233,7 @@ describe('multibid adapter', function () { let adUnitCode = 'test-div'; let bids = [{...bidArray[0]}, {...bidArray[1]}]; - config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}]}); + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); @@ -277,7 +277,7 @@ describe('multibid adapter', function () { 'bidder': 'bidderB', }); - config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}]}); + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); @@ -329,7 +329,7 @@ describe('multibid adapter', function () { 'bidder': 'bidderA', }); - config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}]}); + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); @@ -377,7 +377,7 @@ describe('multibid adapter', function () { 'bidder': 'bidderA', }); - config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2}]}); + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2}]}); addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); @@ -436,7 +436,7 @@ describe('multibid adapter', function () { return bid; }); - config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}]}); + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); @@ -505,7 +505,7 @@ describe('multibid adapter', function () { return bid; }); - config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}]}); + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); @@ -576,7 +576,7 @@ describe('multibid adapter', function () { it('it does filter on bidsReceived if multibid configuration found with no prefix', function () { let bids = [{...bidArray[0]}, {...bidArray[1]}]; - config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2}]}); + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2}]}); targetBidPoolHook(callbackFn, bids, utils.getHighestCpm); bids.pop(); @@ -598,7 +598,7 @@ describe('multibid adapter', function () { return bidResult; }); - config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}]}); + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm); @@ -623,7 +623,7 @@ describe('multibid adapter', function () { return bidResult; }); - config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}]}); + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm); @@ -656,7 +656,7 @@ describe('multibid adapter', function () { return bidResult; }); - config.setConfig({ multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}] }); + config.setConfig({ multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}] }); targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm, 3); @@ -674,7 +674,7 @@ describe('multibid adapter', function () { }); it('it should collect all bids from auction and bid cache then sort and filter', function () { - config.setConfig({ multibid: [{bidder: 'bidderA', maxbids: 2, targetbiddercodeprefix: 'bidA'}] }); + config.setConfig({ multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}] }); let modifiedBids = [{...bidArrayAlt[0]}, {...bidArrayAlt[2]}, {...bidArrayAlt[3]}, {...bidArrayAlt[1]}].map(bid => { addBidResponseHook(bidResponseCallback, 'test-div', {...bid}); @@ -705,53 +705,53 @@ describe('multibid adapter', function () { describe('validate multibid', function () { it('should fail validation for missing bidder name in entry', function () { - let conf = [{maxbids: 1}]; + let conf = [{maxBids: 1}]; let result = validateMultibid(conf); expect(result).to.equal(false); }); it('should pass validation on all multibid entries', function () { - let conf = [{bidder: 'bidderA', maxbids: 1}, {bidder: 'bidderB', maxbids: 2}]; + let conf = [{bidder: 'bidderA', maxBids: 1}, {bidder: 'bidderB', maxBids: 2}]; let result = validateMultibid(conf); expect(result).to.equal(true); }); it('should fail validation for maxbids less than 1 in entry', function () { - let conf = [{bidder: 'bidderA', maxbids: 0}, {bidder: 'bidderB', maxbids: 2}]; + let conf = [{bidder: 'bidderA', maxBids: 0}, {bidder: 'bidderB', maxBids: 2}]; let result = validateMultibid(conf); expect(result).to.equal(false); }); it('should fail validation for maxbids greater than 9 in entry', function () { - let conf = [{bidder: 'bidderA', maxbids: 10}, {bidder: 'bidderB', maxbids: 2}]; + let conf = [{bidder: 'bidderA', maxBids: 10}, {bidder: 'bidderB', maxBids: 2}]; let result = validateMultibid(conf); expect(result).to.equal(false); }); it('should add multbid entries to global config', function () { - config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 1}]}); + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 1}]}); let conf = config.getConfig('multibid'); - expect(conf).to.deep.equal([{bidder: 'bidderA', maxbids: 1}]); + expect(conf).to.deep.equal([{bidder: 'bidderA', maxBids: 1}]); }); it('should modify multbid entries and add to global config', function () { - config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 0}, {bidder: 'bidderB', maxbids: 15}]}); + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 0}, {bidder: 'bidderB', maxBids: 15}]}); let conf = config.getConfig('multibid'); - expect(conf).to.deep.equal([{bidder: 'bidderA', maxbids: 1}, {bidder: 'bidderB', maxbids: 9}]); + expect(conf).to.deep.equal([{bidder: 'bidderA', maxBids: 1}, {bidder: 'bidderB', maxBids: 9}]); }); it('should filter multbid entry and add modified to global config', function () { - config.setConfig({multibid: [{bidder: 'bidderA', maxbids: 0}, {maxbids: 15}]}); + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 0}, {maxBids: 15}]}); let conf = config.getConfig('multibid'); expect(conf.length).to.equal(1); - expect(conf).to.deep.equal([{bidder: 'bidderA', maxbids: 1}]); + expect(conf).to.deep.equal([{bidder: 'bidderA', maxBids: 1}]); }); }); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 006b32a4994..fb7c2670bd6 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -1578,6 +1578,13 @@ describe('S2S Adapter', function () { it('passes multibid array in request', function () { const bidRequests = utils.deepClone(BID_REQUESTS); const multibid = [{ + bidder: 'bidderA', + maxBids: 2 + }, { + bidder: 'bidderB', + maxBids: 2 + }]; + const expected = [{ bidder: 'bidderA', maxbids: 2 }, { @@ -1589,7 +1596,7 @@ describe('S2S Adapter', function () { adapter.callBids(REQUEST, bidRequests, addBidResponse, done, ajax); const parsedRequestBody = JSON.parse(server.requests[0].requestBody); - expect(parsedRequestBody.ext.prebid.multibid).to.deep.equal(multibid); + expect(parsedRequestBody.ext.prebid.multibid).to.deep.equal(expected); }); it('passes first party data in request', () => { diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 0fb6c3cb23b..f95c341a97d 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -1641,6 +1641,13 @@ describe('the rubicon adapter', function () { createVideoBidderRequest(); const multibid = [{ + bidder: 'bidderA', + maxBids: 2 + }, { + bidder: 'bidderB', + maxBids: 2 + }]; + const expected = [{ bidder: 'bidderA', maxbids: 2 }, { @@ -1654,7 +1661,7 @@ describe('the rubicon adapter', function () { // should have the aliases object sent to PBS expect(request.data.ext.prebid).to.haveOwnProperty('multibid'); - expect(request.data.ext.prebid.multibid).to.deep.equal(multibid); + expect(request.data.ext.prebid.multibid).to.deep.equal(expected); }); it('should send video exp param correctly when set', function () { From 673a211fcf943abbcb01a1923efd7727f84a5f74 Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Fri, 26 Mar 2021 11:12:45 -0400 Subject: [PATCH 07/11] Adjust RP adapter to always include maxbids value - default is 1 --- modules/rubiconBidAdapter.js | 4 +--- test/spec/modules/rubiconBidAdapter_spec.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index f4308bbe30d..7d1659fb0f7 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -525,9 +525,7 @@ export const spec = { data['us_privacy'] = encodeURIComponent(bidderRequest.uspConsent); } - if (bidderRequest.bidLimit) { - data['rp_maxbids'] = bidderRequest.bidLimit; - } + data['rp_maxbids'] = bidderRequest.bidLimit || 1; applyFPD(bidRequest, BANNER, data); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index f95c341a97d..889d3fdd161 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -565,7 +565,7 @@ describe('the rubicon adapter', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const referenceOrdering = ['account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw', 'tg_v.ucat', 'tg_v.lastsearch', 'tg_v.likes', 'tg_i.rating', 'tg_i.prodtype', 'tk_flint', 'x_source.tid', 'x_source.pchain', 'p_screen_res', 'rp_secure', 'tk_user_key', 'tg_fl.eid', 'slots', 'rand']; + const referenceOrdering = ['account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw', 'tg_v.ucat', 'tg_v.lastsearch', 'tg_v.likes', 'tg_i.rating', 'tg_i.prodtype', 'tk_flint', 'x_source.tid', 'x_source.pchain', 'p_screen_res', 'rp_secure', 'tk_user_key', 'tg_fl.eid', 'rp_maxbids', 'slots', 'rand']; request.data.split('&').forEach((item, i) => { expect(item.split('=')[0]).to.equal(referenceOrdering[i]); From eef4aa8316feeaa392a6058ae5cd2057906c69da Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Mon, 29 Mar 2021 11:34:57 -0400 Subject: [PATCH 08/11] Added support for bidders array in multibid config --- modules/multibid/index.js | 19 +++++++++++---- test/spec/modules/multibid_spec.js | 39 +++++++++--------------------- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/modules/multibid/index.js b/modules/multibid/index.js index 5feb773e849..0a85f4f55d0 100644 --- a/modules/multibid/index.js +++ b/modules/multibid/index.js @@ -24,9 +24,18 @@ config.getConfig(MODULE_NAME, conf => { hasMultibid = true; conf.multibid.forEach(entry => { - multiConfig[entry.bidder] = { - maxbids: entry.maxBids, - prefix: entry.targetBiddercodePrefix + 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 + } + }); } }); }); @@ -40,8 +49,8 @@ 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') { - utils.logWarn('Filtering multibid entry due to missing required bidder property.'); + 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; } diff --git a/test/spec/modules/multibid_spec.js b/test/spec/modules/multibid_spec.js index 7babb32d549..e849392ee4b 100644 --- a/test/spec/modules/multibid_spec.js +++ b/test/spec/modules/multibid_spec.js @@ -14,11 +14,6 @@ import * as utils from 'src/utils.js'; import find from 'core-js-pure/features/array/find.js'; describe('multibid adapter', function () { - let sandbox, - logErrorSpy; - let logErrorStub; - let logWarnStub; - let logInfoStub; let bidArray = [{ 'bidderCode': 'bidderA', 'requestId': '1c5f0a05d3629a', @@ -124,14 +119,7 @@ describe('multibid adapter', function () { }] }]; - beforeEach(function () { - sandbox = sinon.sandbox.create(); - logErrorSpy = sinon.spy(utils, 'logError'); - }); - afterEach(function () { - sandbox.restore(); - utils.logError.restore(); config.resetConfig(); resetMultiConfig(); resetMultibidUnits(); @@ -145,11 +133,6 @@ describe('multibid adapter', function () { beforeEach(function() { result = null; - logWarnStub = sinon.stub(utils, 'logWarn'); - }); - - afterEach(function() { - logWarnStub.restore(); }); it('does not modify bidderRequest when no multibid config exists', function () { @@ -173,6 +156,18 @@ describe('multibid adapter', function () { expect(result[0].bidLimit).to.equal(2); }); + it('does modify bidderRequest when multibid config exists using bidders array', function () { + let bidRequests = [{...bidderRequests[0]}]; + + config.setConfig({multibid: [{bidders: ['bidderA'], maxBids: 2}]}); + + adjustBidderRequestsHook(callbackFn, [{...bidderRequests[0]}]); + + expect(result).to.not.equal(null); + expect(result).to.not.deep.equal(bidRequests); + expect(result[0].bidLimit).to.equal(2); + }); + it('does only modifies bidderRequest when multibid config exists for bidder', function () { let bidRequests = [{...bidderRequests[0]}, {...bidderRequests[1]}]; @@ -199,11 +194,6 @@ describe('multibid adapter', function () { beforeEach(function() { result = null; - logWarnStub = sinon.stub(utils, 'logWarn'); - }); - - afterEach(function() { - logWarnStub.restore(); }); it('adds original bids and does not modify', function () { @@ -552,11 +542,6 @@ describe('multibid adapter', function () { beforeEach(function() { result = null; bidResult = null; - logWarnStub = sinon.stub(utils, 'logWarn'); - }); - - afterEach(function() { - logWarnStub.restore(); }); it('it does not run filter on bidsReceived if no multibid configuration found', function () { From 2141b638d0cc11b554221cb5eee18fa4faeba190 Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Mon, 29 Mar 2021 16:37:46 -0400 Subject: [PATCH 09/11] Fixed floor comparison to be <= bid cpm as oppossed to just < bid cpm. Updated md file to fix camelCase tpyo --- modules/multibid/index.js | 2 +- modules/multibid/index.md | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/modules/multibid/index.js b/modules/multibid/index.js index 0a85f4f55d0..b7d9b665d1e 100644 --- a/modules/multibid/index.js +++ b/modules/multibid/index.js @@ -109,7 +109,7 @@ export function addBidResponseHook(fn, adUnitCode, bid) { 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)) { + if (!multibidUnits[adUnitCode][bid.bidderCode].maxReached && (!floor || floor <= bid.cpm)) { bid.originalRequestId = bid.requestId; bid.requestId = utils.getUniqueIdentifierStr(); diff --git a/modules/multibid/index.md b/modules/multibid/index.md index 93fa2f5e144..a431d9b7960 100644 --- a/modules/multibid/index.md +++ b/modules/multibid/index.md @@ -19,15 +19,18 @@ Allowing a single bidder to multi-bid into an auction has several use cases: pbjs.setConfig({ multibid: [{ bidder: "bidderA", - maxbids: 3, - targetbiddercodeprefix: "bidA" + maxBids: 3, + targetBiddercodePrefix: "bidA" },{ bidder: "bidderB", - maxbids: 3, - targetbiddercodeprefix: "bidB" + maxBids: 3, + targetBiddercodePrefix: "bidB" },{ bidder: "bidderC", - maxbids: 3 + maxBids: 3 + },{ + bidders: ["bidderD", "bidderE"], + maxBids: 2 }] }); ``` From 7b84ec6131aab5257ce2e6a01a47abbf4486e242 Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Wed, 31 Mar 2021 14:16:35 -0400 Subject: [PATCH 10/11] Update to include originalBidderRequest in video call to prebid cache --- src/auction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auction.js b/src/auction.js index 217a50be3d6..2f0c81bbf98 100644 --- a/src/auction.js +++ b/src/auction.js @@ -452,7 +452,7 @@ export function addBidToAuction(auctionInstance, bidResponse) { function tryAddVideoBid(auctionInstance, bidResponse, bidRequests, afterBidAdded) { let addBid = true; - const bidderRequest = getBidRequest(bidResponse.requestId, [bidRequests]); + const bidderRequest = getBidRequest(bidResponse.originalRequestId || bidResponse.requestId, [bidRequests]); const videoMediaType = bidderRequest && deepAccess(bidderRequest, 'mediaTypes.video'); const context = videoMediaType && deepAccess(videoMediaType, 'context'); From dbac409a379a9d772797da767e2a7be2b611ab46 Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Wed, 31 Mar 2021 14:58:54 -0400 Subject: [PATCH 11/11] Update to ignore adpod bids from multibid and allow them to return as normal bids --- modules/multibid/index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/multibid/index.js b/modules/multibid/index.js index b7d9b665d1e..dd4999b2dca 100644 --- a/modules/multibid/index.js +++ b/modules/multibid/index.js @@ -100,8 +100,12 @@ 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 - if (hasMultibid && multiConfig[bid.bidderCode]) { + // 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;