Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fledgeForGpt: provide bidfloor in auction signals #10393

Merged
merged 5 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -502,7 +502,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';

const pbjsInstance = getGlobal();
const { triggerUserSyncs } = userSync;
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';

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