Skip to content

Commit

Permalink
fledgeForGpt: provide bidfloor in auction signals (#10393)
Browse files Browse the repository at this point in the history
* fledgeForGpt: delay slot config to end of auction

* currency utils

* reducers

* Set `prebid.bidfloor` and `prebid.bidfloorcur` in fledge auction signals

---------

Co-authored-by: Patrick McCann <patmmccann@gmail.com>
  • Loading branch information
dgirardi and patmmccann authored Sep 14, 2023
1 parent ca555d3 commit 7764e53
Show file tree
Hide file tree
Showing 20 changed files with 2,000 additions and 1,589 deletions.
31 changes: 31 additions & 0 deletions libraries/currencyUtils/currency.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {getGlobal} from '../../src/prebidGlobal.js';
import {keyCompare} from '../../src/utils/reducers.js';

/**
* Attempt to convert `amount` from the currency `fromCur` to the currency `toCur`.
*
* By default, when the conversion is not possible (currency module not present or
* throwing errors), the amount is returned unchanged. This behavior can be
* toggled off with bestEffort = false.
*/
export function convertCurrency(amount, fromCur, toCur, bestEffort = true) {
if (fromCur === toCur) return amount;
let result = amount;
try {
result = getGlobal().convertCurrency(amount, fromCur, toCur);
} catch (e) {
if (!bestEffort) throw e;
}
return result;
}

export function currencyNormalizer(toCurrency = null, bestEffort = true, convert = convertCurrency) {
return function (amount, currency) {
if (toCurrency == null) toCurrency = currency;
return convert(amount, currency, toCurrency, bestEffort);
}
}

export function currencyCompare(get = (obj) => [obj.cpm, obj.currency], normalize = currencyNormalizer()) {
return keyCompare(obj => normalize.apply(null, get(obj)))
}
73 changes: 63 additions & 10 deletions modules/fledgeForGpt.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
*/
import { config } from '../src/config.js';
import { getHook } from '../src/hook.js';
import { getGptSlotForAdUnitCode, logInfo, logWarn } from '../src/utils.js';
import {deepSetValue, getGptSlotForAdUnitCode, logInfo, logWarn, mergeDeep} from '../src/utils.js';
import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js';
import * as events from '../src/events.js'
import CONSTANTS from '../src/constants.json';
import {currencyCompare} from '../libraries/currencyUtils/currency.js';
import {maximum, minimum} from '../src/utils/reducers.js';

const MODULE = 'fledgeForGpt'
const PENDING = {};

export let isEnabled = false;

Expand All @@ -21,35 +26,83 @@ export function init(cfg) {
if (!isEnabled) {
getHook('addComponentAuction').before(addComponentAuctionHook);
getHook('makeBidRequests').after(markForFledge);
events.on(CONSTANTS.EVENTS.AUCTION_INIT, onAuctionInit);
events.on(CONSTANTS.EVENTS.AUCTION_END, onAuctionEnd);
isEnabled = true;
}
logInfo(`${MODULE} enabled (browser ${isFledgeSupported() ? 'supports' : 'does NOT support'} fledge)`, cfg);
} else {
if (isEnabled) {
getHook('addComponentAuction').getHooks({hook: addComponentAuctionHook}).remove();
getHook('makeBidRequests').getHooks({hook: markForFledge}).remove()
events.off(CONSTANTS.EVENTS.AUCTION_INIT, onAuctionInit);
events.off(CONSTANTS.EVENTS.AUCTION_END, onAuctionEnd);
isEnabled = false;
}
logInfo(`${MODULE} disabled`, cfg);
}
}

export function addComponentAuctionHook(next, adUnitCode, componentAuctionConfig) {
const seller = componentAuctionConfig.seller;
function setComponentAuction(adUnitCode, auctionConfigs) {
const gptSlot = getGptSlotForAdUnitCode(adUnitCode);
if (gptSlot && gptSlot.setConfig) {
gptSlot.setConfig({
componentAuction: [{
configKey: seller,
auctionConfig: componentAuctionConfig
}]
componentAuction: auctionConfigs.map(cfg => ({
configKey: cfg.seller,
auctionConfig: cfg
}))
});
logInfo(MODULE, `register component auction config for: ${adUnitCode} x ${seller}: ${gptSlot.getAdUnitPath()}`, componentAuctionConfig);
logInfo(MODULE, `register component auction configs for: ${adUnitCode}: ${gptSlot.getAdUnitPath()}`, auctionConfigs);
} else {
logWarn(MODULE, `unable to register component auction config for: ${adUnitCode} x ${seller}.`);
logWarn(MODULE, `unable to register component auction config for ${adUnitCode}`, auctionConfigs);
}
}

function onAuctionInit({auctionId}) {
PENDING[auctionId] = {};
}

function getSlotSignals(bidsReceived = [], bidRequests = []) {
let bidfloor, bidfloorcur;
if (bidsReceived.length > 0) {
const bestBid = bidsReceived.reduce(maximum(currencyCompare(bid => [bid.cpm, bid.currency])));
bidfloor = bestBid.cpm;
bidfloorcur = bestBid.currency;
} else {
const floors = bidRequests.map(bid => typeof bid.getFloor === 'function' && bid.getFloor()).filter(f => f);
const minFloor = floors.length && floors.reduce(minimum(currencyCompare(floor => [floor.floor, floor.currency])))
bidfloor = minFloor?.floor;
bidfloorcur = minFloor?.currency;
}
const cfg = {};
if (bidfloor) {
deepSetValue(cfg, 'auctionSignals.prebid.bidfloor', bidfloor);
bidfloorcur && deepSetValue(cfg, 'auctionSignals.prebid.bidfloorcur', bidfloorcur);
}
return cfg;
}

next(adUnitCode, componentAuctionConfig);
function onAuctionEnd({auctionId, bidsReceived, bidderRequests}) {
try {
const allReqs = bidderRequests?.flatMap(br => br.bids);
Object.entries(PENDING[auctionId]).forEach(([adUnitCode, auctionConfigs]) => {
const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode;
const slotSignals = getSlotSignals(bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit));
setComponentAuction(adUnitCode, auctionConfigs.map(cfg => mergeDeep({}, slotSignals, cfg)))
})
} finally {
delete PENDING[auctionId];
}
}

export function addComponentAuctionHook(next, auctionId, adUnitCode, componentAuctionConfig) {
if (PENDING.hasOwnProperty(auctionId)) {
!PENDING[auctionId].hasOwnProperty(adUnitCode) && (PENDING[auctionId][adUnitCode] = []);
PENDING[auctionId][adUnitCode].push(componentAuctionConfig);
} else {
logWarn(MODULE, `Received component auction config for auction that has closed (auction '${auctionId}', adUnit '${adUnitCode}')`, componentAuctionConfig)
}
next(auctionId, adUnitCode, componentAuctionConfig);
}

function isFledgeSupported() {
Expand Down
2 changes: 1 addition & 1 deletion modules/prebidServerBidAdapter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ export function PrebidServer() {
}
},
onFledge: ({adUnitCode, config}) => {
addComponentAuction(adUnitCode, config);
addComponentAuction(bidRequests[0].auctionId, adUnitCode, config);
}
})
}
Expand Down
12 changes: 4 additions & 8 deletions modules/prebidServerBidAdapter/ortbConverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ import {pbsExtensions} from '../../libraries/pbsExtensions/pbsExtensions.js';
import {setImpBidParams} from '../../libraries/pbsExtensions/processors/params.js';
import {SUPPORTED_MEDIA_TYPES} from '../../libraries/pbsExtensions/processors/mediaType.js';
import {IMP, REQUEST, RESPONSE} from '../../src/pbjsORTB.js';
import {beConvertCurrency} from '../../src/utils/currency.js';
import {redactor} from '../../src/activities/redactor.js';
import {s2sActivityParams} from '../../src/adapterManager.js';
import {activityParams} from '../../src/activities/activityParams.js';
import {MODULE_TYPE_BIDDER} from '../../src/activities/modules.js';
import {isActivityAllowed} from '../../src/activities/rules.js';
import {ACTIVITY_TRANSMIT_TID} from '../../src/activities/activities.js';
import {currencyCompare} from '../../libraries/currencyUtils/currency.js';
import {minimum} from '../../src/utils/reducers.js';

const DEFAULT_S2S_TTL = 60;
const DEFAULT_S2S_CURRENCY = 'USD';
Expand Down Expand Up @@ -141,6 +142,7 @@ const PBS_CONVERTER = ortbConverter({
bidfloor(orig, imp, proxyBidRequest, context) {
// for bid floors, we pass each bidRequest associated with this imp through normal bidfloor processing,
// and aggregate all of them into a single, minimum floor to put in the request
const getMin = minimum(currencyCompare(floor => [floor.bidfloor, floor.bidfloorcur]));
let min;
for (const req of context.actualBidRequests.values()) {
const floor = {};
Expand All @@ -149,14 +151,8 @@ const PBS_CONVERTER = ortbConverter({
if (floor.bidfloorcur == null || floor.bidfloor == null) {
min = null;
break;
} else if (min == null) {
min = floor;
} else {
const value = beConvertCurrency(floor.bidfloor, floor.bidfloorcur, min.bidfloorcur);
if (value != null && value < min.bidfloor) {
min = floor;
}
}
min = min == null ? floor : getMin(min, floor);
}
if (min != null) {
Object.assign(imp, min);
Expand Down
6 changes: 3 additions & 3 deletions modules/priceFloors.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import {bidderSettings} from '../src/bidderSettings.js';
import {auctionManager} from '../src/auctionManager.js';
import {IMP, PBS, registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js';
import {timedAuctionHook, timedBidResponseHook} from '../src/utils/perfMetrics.js';
import {beConvertCurrency} from '../src/utils/currency.js';
import {adjustCpm} from '../src/utils/cpm.js';
import {convertCurrency} from '../libraries/currencyUtils/currency.js';

/**
* @summary This Module is intended to provide users with the ability to dynamically set and enforce price floors on a per auction basis.
Expand Down Expand Up @@ -794,8 +794,8 @@ export function setImpExtPrebidFloors(imp, bidRequest, context) {
if (floorMinCur == null) { floorMinCur = imp.bidfloorcur }
const ortb2ImpFloorCur = imp.ext?.prebid?.floors?.floorMinCur || imp.ext?.prebid?.floorMinCur || floorMinCur;
const ortb2ImpFloorMin = imp.ext?.prebid?.floors?.floorMin || imp.ext?.prebid?.floorMin;
const convertedFloorMinValue = beConvertCurrency(imp.bidfloor, imp.bidfloorcur, floorMinCur);
const convertedOrtb2ImpFloorMinValue = ortb2ImpFloorMin && ortb2ImpFloorCur ? beConvertCurrency(ortb2ImpFloorMin, ortb2ImpFloorCur, floorMinCur) : false;
const convertedFloorMinValue = convertCurrency(imp.bidfloor, imp.bidfloorcur, floorMinCur);
const convertedOrtb2ImpFloorMinValue = ortb2ImpFloorMin && ortb2ImpFloorCur ? convertCurrency(ortb2ImpFloorMin, ortb2ImpFloorCur, floorMinCur) : false;

const lowestImpFloorMin = convertedOrtb2ImpFloorMinValue && convertedOrtb2ImpFloorMinValue < convertedFloorMinValue
? convertedOrtb2ImpFloorMinValue
Expand Down
5 changes: 1 addition & 4 deletions src/adapters/bidderFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,14 +288,11 @@ export function newBidder(spec) {
onTimelyResponse(spec.code);
responses.push(resp)
},
/** Process eventual BidderAuctionResponse.fledgeAuctionConfig field in response.
* @param {Array<FledgeAuctionConfig>} fledgeAuctionConfigs
*/
onFledgeAuctionConfigs: (fledgeAuctionConfigs) => {
fledgeAuctionConfigs.forEach((fledgeAuctionConfig) => {
const bidRequest = bidRequestMap[fledgeAuctionConfig.bidId];
if (bidRequest) {
addComponentAuction(bidRequest.adUnitCode, fledgeAuctionConfig.config);
addComponentAuction(bidRequest.auctionId, bidRequest.adUnitCode, fledgeAuctionConfig.config);
} else {
logWarn('Received fledge auction configuration for an unknown bidId', fledgeAuctionConfig);
}
Expand Down
2 changes: 1 addition & 1 deletion src/prebid.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
deepSetValue,
flatten,
generateUUID,
getHighestCpm,
inIframe,
insertElement,
isArray,
Expand Down Expand Up @@ -52,6 +51,7 @@ import {newMetrics, useMetrics} from './utils/perfMetrics.js';
import {defer, GreedyPromise} from './utils/promise.js';
import {enrichFPD} from './fpd/enrichment.js';
import {allConsent} from './consentHandler.js';
import {getHighestCpm} from './utils/reducers.js';
import {fillVideoDefaults} from './video.js';

const pbjsInstance = getGlobal();
Expand Down
3 changes: 1 addition & 2 deletions src/targeting.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {
deepAccess,
deepClone,
getHighestCpm,
getOldestHighestCpmBid,
groupBy,
isAdUnitCodeMatchingSlot,
isArray,
Expand All @@ -24,6 +22,7 @@ import {hook} from './hook.js';
import {bidderSettings} from './bidderSettings.js';
import {find, includes} from './polyfill.js';
import CONSTANTS from './constants.json';
import {getHighestCpm, getOldestHighestCpmBid} from './utils/reducers.js';
import {getTTL} from './bidTTL.js';

var pbTargetingKeys = [];
Expand Down
20 changes: 0 additions & 20 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -729,26 +729,6 @@ export function isApnGetTagDefined() {
}
}

// This function will get highest cpm value bid, in case of tie it will return the bid with lowest timeToRespond
export const getHighestCpm = getHighestCpmCallback('timeToRespond', (previous, current) => previous > current);

// This function will get the oldest hightest cpm value bid, in case of tie it will return the bid which came in first
// Use case for tie: https://github.com/prebid/Prebid.js/issues/2448
export const getOldestHighestCpmBid = getHighestCpmCallback('responseTimestamp', (previous, current) => previous > current);

// This function will get the latest hightest cpm value bid, in case of tie it will return the bid which came in last
// Use case for tie: https://github.com/prebid/Prebid.js/issues/2539
export const getLatestHighestCpmBid = getHighestCpmCallback('responseTimestamp', (previous, current) => previous < current);

function getHighestCpmCallback(useTieBreakerProperty, tieBreakerCallback) {
return (previous, current) => {
if (previous.cpm === current.cpm) {
return tieBreakerCallback(previous[useTieBreakerProperty], current[useTieBreakerProperty]) ? current : previous;
}
return previous.cpm < current.cpm ? current : previous;
}
}

/**
* Fisher–Yates shuffle
* http://stackoverflow.com/a/6274398
Expand Down
16 changes: 0 additions & 16 deletions src/utils/currency.js

This file was deleted.

44 changes: 44 additions & 0 deletions src/utils/reducers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export function simpleCompare(a, b) {
if (a === b) return 0;
return a < b ? -1 : 1;
}

export function keyCompare(key = (item) => item) {
return (a, b) => simpleCompare(key(a), key(b))
}

export function reverseCompare(compare = simpleCompare) {
return (a, b) => -compare(a, b) || 0;
}

export function tiebreakCompare(...compares) {
return function (a, b) {
for (const cmp of compares) {
const val = cmp(a, b);
if (val !== 0) return val;
}
return 0;
}
}

export function minimum(compare = simpleCompare) {
return (min, item) => compare(item, min) < 0 ? item : min;
}

export function maximum(compare = simpleCompare) {
return minimum(reverseCompare(compare));
}

const cpmCompare = keyCompare((bid) => bid.cpm);
const timestampCompare = keyCompare((bid) => bid.responseTimestamp);

// This function will get highest cpm value bid, in case of tie it will return the bid with lowest timeToRespond
export const getHighestCpm = maximum(tiebreakCompare(cpmCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond))))

// This function will get the oldest hightest cpm value bid, in case of tie it will return the bid which came in first
// Use case for tie: https://github.com/prebid/Prebid.js/issues/2448
export const getOldestHighestCpmBid = maximum(tiebreakCompare(cpmCompare, reverseCompare(timestampCompare)))

// This function will get the latest hightest cpm value bid, in case of tie it will return the bid which came in last
// Use case for tie: https://github.com/prebid/Prebid.js/issues/2539
export const getLatestHighestCpmBid = maximum(tiebreakCompare(cpmCompare, timestampCompare))
Loading

0 comments on commit 7764e53

Please sign in to comment.