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

PAAPI/fledgeForGpt: make auction configs available independently from GPT #10930

Merged
merged 13 commits into from
Feb 14, 2024
15 changes: 6 additions & 9 deletions integrationExamples/gpt/fledge_example.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,11 @@

pbjs.que.push(function() {
pbjs.setConfig({
fledgeForGpt: {
enabled: true
}
});

pbjs.setBidderConfig({
bidders: ['openx'],
config: {
fledgeEnabled: true
paapi: {
enabled: true,
gpt: {
autoconfig: false
}
}
});

Expand All @@ -69,6 +65,7 @@
googletag.cmd.push(function() {
pbjs.que.push(function() {
pbjs.setTargetingForGPTAsync();
pbjs.setPAAPIConfigForGPT();
googletag.pubads().refresh();
});
});
Expand Down
3 changes: 3 additions & 0 deletions modules/.submodules.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@
"videoModule": [
"jwplayerVideoProvider",
"videojsVideoProvider"
],
"paapi": [
"fledgeForGpt"
]
}
}
243 changes: 89 additions & 154 deletions modules/fledgeForGpt.js
Original file line number Diff line number Diff line change
@@ -1,169 +1,104 @@
/**
* Fledge modules is responsible for registering fledged auction configs into the GPT slot;
* GPT is resposible to run the fledge auction.
* GPT-specific slot configuration logic for PAAPI.
*/
import { config } from '../src/config.js';
import { getHook } from '../src/hook.js';
import {deepSetValue, 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';
import {submodule} from '../src/hook.js';
import {deepAccess, logInfo, logWarn} from '../src/utils.js';
import {getGptSlotForAdUnitCode} from '../libraries/gptUtils/gptUtils.js';

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

export let isEnabled = false;

config.getConfig('fledgeForGpt', config => init(config.fledgeForGpt));

/**
* Module init.
*/
export function init(cfg) {
if (cfg && cfg.enabled === true) {
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;
import {config} from '../src/config.js';
import {getGlobal} from '../src/prebidGlobal.js';

const MODULE = 'fledgeForGpt';

let getPAAPIConfig;

// for backwards compat, we attempt to automatically set GPT configuration as soon as we
// have the auction configs available. Disabling this allows one to call pbjs.setPAAPIConfigForGPT at their
// own pace.
let autoconfig = true;

Object.entries({
[MODULE]: MODULE,
'paapi': 'paapi.gpt'
}).forEach(([topic, ns]) => {
const configKey = `${ns}.autoconfig`;
config.getConfig(topic, (cfg) => {
autoconfig = deepAccess(cfg, configKey, true);
});
});

export function slotConfigurator() {
const PREVIOUSLY_SET = {};
return function setComponentAuction(adUnitCode, auctionConfigs, reset = true) {
const gptSlot = getGptSlotForAdUnitCode(adUnitCode);
if (gptSlot && gptSlot.setConfig) {
let previous = PREVIOUSLY_SET[adUnitCode] ?? {};
let configsBySeller = Object.fromEntries(auctionConfigs.map(cfg => [cfg.seller, cfg]));
const sellers = Object.keys(configsBySeller);
if (reset) {
configsBySeller = Object.assign(previous, configsBySeller);
previous = Object.fromEntries(sellers.map(seller => [seller, null]));
} else {
sellers.forEach(seller => {
previous[seller] = null;
});
}
Object.keys(previous).length ? PREVIOUSLY_SET[adUnitCode] = previous : delete PREVIOUSLY_SET[adUnitCode];
const componentAuction = Object.entries(configsBySeller)
.map(([configKey, auctionConfig]) => ({configKey, auctionConfig}));
if (componentAuction.length > 0) {
gptSlot.setConfig({componentAuction});
logInfo(MODULE, `register component auction configs for: ${adUnitCode}: ${gptSlot.getAdUnitPath()}`, auctionConfigs);
}
} else if (auctionConfigs.length > 0) {
logWarn(MODULE, `unable to register component auction config for ${adUnitCode}`, auctionConfigs);
}
logInfo(`${MODULE} disabled`, cfg);
}
}

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

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

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);
export function onAuctionConfigFactory(setGptConfig = setComponentAuction) {
return function onAuctionConfig(auctionId, configsByAdUnit, markAsUsed) {
if (autoconfig) {
Object.entries(configsByAdUnit).forEach(([adUnitCode, cfg]) => {
setGptConfig(adUnitCode, cfg?.componentAuctions ?? []);
markAsUsed(adUnitCode);
});
}
}
return cfg;
}

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)))
export function setPAAPIConfigFactory(
getConfig = (filters) => getPAAPIConfig(filters, true),
setGptConfig = setComponentAuction) {
/**
* Configure GPT slots with PAAPI auction configs.
* `filters` are the same filters accepted by `pbjs.getPAAPIConfig`;
*/
return function(filters = {}) {
let some = false;
Object.entries(
getConfig(filters) || {}
).forEach(([au, config]) => {
if (config != null) {
some = true;
}
setGptConfig(au, config?.componentAuctions || [], true);
})
} finally {
delete PENDING[auctionId];
}
}

function setFPDSignals(auctionConfig, fpd) {
auctionConfig.auctionSignals = mergeDeep({}, {prebid: fpd}, auctionConfig.auctionSignals);
}

export function addComponentAuctionHook(next, request, componentAuctionConfig) {
const {adUnitCode, auctionId, ortb2, ortb2Imp} = request;
if (PENDING.hasOwnProperty(auctionId)) {
setFPDSignals(componentAuctionConfig, {ortb2, ortb2Imp});
!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(request, componentAuctionConfig);
}

function isFledgeSupported() {
return 'runAdAuction' in navigator && 'joinAdInterestGroup' in navigator
}

export function markForFledge(next, bidderRequests) {
if (isFledgeSupported()) {
const globalFledgeConfig = config.getConfig('fledgeForGpt');
const bidders = globalFledgeConfig?.bidders ?? [];
bidderRequests.forEach((bidderReq) => {
const useGlobalConfig = globalFledgeConfig?.enabled && (bidders.length === 0 || bidders.includes(bidderReq.bidderCode));
config.runWithBidder(bidderReq.bidderCode, () => {
const fledgeEnabled = config.getConfig('fledgeEnabled') ?? (useGlobalConfig ? globalFledgeConfig.enabled : undefined);
const defaultForSlots = config.getConfig('defaultForSlots') ?? (useGlobalConfig ? globalFledgeConfig?.defaultForSlots : undefined);
Object.assign(bidderReq, {fledgeEnabled});
bidderReq.bids.forEach(bidReq => { deepSetValue(bidReq, 'ortb2Imp.ext.ae', bidReq.ortb2Imp?.ext?.ae ?? defaultForSlots) })
})
});
}
next(bidderRequests);
}

export function setImpExtAe(imp, bidRequest, context) {
if (imp.ext?.ae && !context.bidderRequest.fledgeEnabled) {
delete imp.ext?.ae;
}
}
registerOrtbProcessor({type: IMP, name: 'impExtAe', fn: setImpExtAe});

// to make it easier to share code between the PBS adapter and adapters whose backend is PBS, break up
// fledge response processing in two steps: first aggregate all the auction configs by their imp...

export function parseExtPrebidFledge(response, ortbResponse, context) {
(ortbResponse.ext?.prebid?.fledge?.auctionconfigs || []).forEach((cfg) => {
const impCtx = context.impContext[cfg.impid];
if (!impCtx?.imp?.ext?.ae) {
logWarn('Received fledge auction configuration for an impression that was not in the request or did not ask for it', cfg, impCtx?.imp);
} else {
impCtx.fledgeConfigs = impCtx.fledgeConfigs || [];
impCtx.fledgeConfigs.push(cfg);
if (!some) {
logInfo(`${MODULE}: No component auctions available to set`);
}
})
}
}
registerOrtbProcessor({type: RESPONSE, name: 'extPrebidFledge', fn: parseExtPrebidFledge, dialects: [PBS]});

// ...then, make them available in the adapter's response. This is the client side version, for which the
// interpretResponse api is {fledgeAuctionConfigs: [{bidId, config}]}
/**
* Configure GPT slots with PAAPI component auctions. Accepts the same filter arguments as `pbjs.getPAAPIConfig`.
*/
getGlobal().setPAAPIConfigForGPT = setPAAPIConfigFactory();

export function setResponseFledgeConfigs(response, ortbResponse, context) {
const configs = Object.values(context.impContext)
.flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => ({bidId: impCtx.bidRequest.bidId, config: cfg.config})));
if (configs.length > 0) {
response.fledgeAuctionConfigs = configs;
submodule('paapi', {
name: 'gpt',
onAuctionConfig: onAuctionConfigFactory(),
init(params) {
getPAAPIConfig = params.getPAAPIConfig;
}
}
registerOrtbProcessor({type: RESPONSE, name: 'fledgeAuctionConfigs', priority: -1, fn: setResponseFledgeConfigs, dialects: [PBS]})
});
Loading