Skip to content

Commit

Permalink
PAAPI: add support for protected audience extensions and "direct" buy…
Browse files Browse the repository at this point in the history
…ers (`igb`) (#11277)

* set ortb2Imp.ext.igs

* PAAPI: add ortbConverter support for request ext.igi and response ext.igi.igs

* Update paapi hook to accept auction config as one field

* mergeBuyers

* partitionBuyers

* add fpd to perBuyerSignals

* complete igb treatment

* improve names

* parse ext.igi.igb

* naming improvement

* update debugging mod to work with igb

* fix mergeBuyers to work with actual URL origins

* rename componentBuyers to componentSeller

* signal componentSeller config to bid adapters
  • Loading branch information
dgirardi committed Jun 3, 2024
1 parent 2bacdeb commit 0ee4efd
Show file tree
Hide file tree
Showing 11 changed files with 887 additions and 191 deletions.
11 changes: 9 additions & 2 deletions modules/debugging/bidInterceptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,17 @@ Object.assign(BidInterceptor.prototype, {
},

paapiReplacer(paapiDef, ruleNo) {
function wrap(configs = []) {
return configs.map(config => {
return Object.keys(config).some(k => !['config', 'igb'].includes(k))
? {config}
: config
});
}
if (Array.isArray(paapiDef)) {
return () => paapiDef;
return () => wrap(paapiDef);
} else if (typeof paapiDef === 'function') {
return paapiDef
return (...args) => wrap(paapiDef(...args))
} else {
this.logger.logError(`Invalid 'paapi' definition for debug bid interceptor (in rule #${ruleNo})`);
}
Expand Down
2 changes: 1 addition & 1 deletion modules/debugging/debugging.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export function bidderBidInterceptor(next, interceptBids, spec, bids, bidRequest
bids,
bidRequest,
addBid: cbs.onBid,
addPaapiConfig: (config, bidRequest) => cbs.onPaapi({bidId: bidRequest.bidId, config}),
addPaapiConfig: (config, bidRequest) => cbs.onPaapi({bidId: bidRequest.bidId, ...config}),
done
}));
if (bids.length === 0) {
Expand Down
2 changes: 1 addition & 1 deletion modules/debugging/pbsInterceptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function makePbsInterceptor({createBid}) {
adUnitCode: bidRequest.adUnitCode,
ortb2: bidderRequest.ortb2,
ortb2Imp: bidRequest.ortb2Imp,
config
...config
})
},
done
Expand Down
241 changes: 205 additions & 36 deletions modules/paapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import {config} from '../src/config.js';
import {getHook, module} from '../src/hook.js';
import {deepSetValue, logInfo, logWarn, mergeDeep, parseSizesInput} from '../src/utils.js';
import {deepSetValue, logInfo, logWarn, mergeDeep, deepEqual, parseSizesInput, deepAccess} from '../src/utils.js';
import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js';
import * as events from '../src/events.js';
import {EVENTS} from '../src/constants.js';
Expand All @@ -24,7 +24,7 @@ export function registerSubmodule(submod) {

module('paapi', registerSubmodule);

function auctionConfigs() {
function auctionStore() {
const store = new WeakMap();
return function (auctionId, init = {}) {
const auction = auctionManager.index.getAuction({auctionId});
Expand All @@ -36,8 +36,10 @@ function auctionConfigs() {
};
}

const pendingForAuction = auctionConfigs();
const configsForAuction = auctionConfigs();
const pendingConfigsForAuction = auctionStore();
const configsForAuction = auctionStore();
const pendingBuyersForAuction = auctionStore();

let latestAuctionForAdUnit = {};
let moduleConfig = {};

Expand Down Expand Up @@ -65,7 +67,7 @@ export function init(cfg, configNamespace) {
}
}

getHook('addComponentAuction').before(addComponentAuctionHook);
getHook('addPaapiConfig').before(addPaapiConfigHook);
getHook('makeBidRequests').after(markForFledge);
events.on(EVENTS.AUCTION_END, onAuctionEnd);

Expand All @@ -89,6 +91,23 @@ function getSlotSignals(bidsReceived = [], bidRequests = []) {
return cfg;
}

export function buyersToAuctionConfigs(igbRequests, merge = mergeBuyers, config = moduleConfig?.componentSeller ?? {}, partitioners = {
compact: (igbRequests) => partitionBuyers(igbRequests.map(req => req[1])).map(part => [{}, part]),
expand: partitionBuyersByBidder
}) {
if (!config.auctionConfig) {
logWarn(MODULE, 'Cannot use IG buyers: paapi.componentSeller.auctionConfig not set', igbRequests.map(req => req[1]));
return [];
}
const partition = partitioners[config.separateAuctions ? 'expand' : 'compact'];
return partition(igbRequests)
.map(([request, igbs]) => {
const auctionConfig = mergeDeep(merge(igbs), config.auctionConfig);
auctionConfig.auctionSignals = setFPD(auctionConfig.auctionSignals || {}, request);
return auctionConfig;
});
}

function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adUnits}) {
const adUnitsByCode = Object.fromEntries(adUnits?.map(au => [au.code, au]) || [])
const allReqs = bidderRequests?.flatMap(br => br.bids);
Expand All @@ -97,7 +116,14 @@ function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adU
paapiConfigs[au] = null;
!latestAuctionForAdUnit.hasOwnProperty(au) && (latestAuctionForAdUnit[au] = null);
});
Object.entries(pendingForAuction(auctionId) || {}).forEach(([adUnitCode, auctionConfigs]) => {
const pendingConfigs = pendingConfigsForAuction(auctionId);
const pendingBuyers = pendingBuyersForAuction(auctionId);
if (pendingConfigs && pendingBuyers) {
Object.entries(pendingBuyers).forEach(([adUnitCode, igbRequests]) => {
buyersToAuctionConfigs(igbRequests).forEach(auctionConfig => append(pendingConfigs, adUnitCode, auctionConfig))
})
}
Object.entries(pendingConfigs || {}).forEach(([adUnitCode, auctionConfigs]) => {
const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode;
const slotSignals = getSlotSignals(bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit));
paapiConfigs[adUnitCode] = {
Expand Down Expand Up @@ -126,25 +152,117 @@ function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adU
);
}

function setFPDSignals(auctionConfig, fpd) {
auctionConfig.auctionSignals = mergeDeep({}, {prebid: fpd}, auctionConfig.auctionSignals);
function append(target, key, value) {
!target.hasOwnProperty(key) && (target[key] = []);
target[key].push(value);
}

export function addComponentAuctionHook(next, request, componentAuctionConfig) {
function setFPD(target, {ortb2, ortb2Imp}) {
ortb2 != null && deepSetValue(target, 'prebid.ortb2', mergeDeep({}, ortb2, target.prebid?.ortb2));
ortb2Imp != null && deepSetValue(target, 'prebid.ortb2Imp', mergeDeep({}, ortb2Imp, target.prebid?.ortb2Imp));
return target;
}

export function addPaapiConfigHook(next, request, paapiConfig) {
if (getFledgeConfig().enabled) {
const {adUnitCode, auctionId, ortb2, ortb2Imp} = request;
const configs = pendingForAuction(auctionId);
if (configs != null) {
setFPDSignals(componentAuctionConfig, {ortb2, ortb2Imp});
!configs.hasOwnProperty(adUnitCode) && (configs[adUnitCode] = []);
configs[adUnitCode].push(componentAuctionConfig);
} else {
logWarn(MODULE, `Received component auction config for auction that has closed (auction '${auctionId}', adUnit '${adUnitCode}')`, componentAuctionConfig);
const {adUnitCode, auctionId} = request;

// eslint-disable-next-line no-inner-declarations
function storePendingData(store, data) {
const target = store(auctionId);
if (target != null) {
append(target, adUnitCode, data)
} else {
logWarn(MODULE, `Received PAAPI config for auction that has closed (auction '${auctionId}', adUnit '${adUnitCode}')`, data);
}
}

const {config, igb} = paapiConfig;
if (config) {
config.auctionSignals = setFPD(config.auctionSignals || {}, request);
(config.interestGroupBuyers || []).forEach(buyer => {
deepSetValue(config, `perBuyerSignals.${buyer}`, setFPD(config.perBuyerSignals?.[buyer] || {}, request));
})
storePendingData(pendingConfigsForAuction, config);
}
if (igb && checkOrigin(igb)) {
igb.pbs = setFPD(igb.pbs || {}, request);
storePendingData(pendingBuyersForAuction, [request, igb])
}
}
next(request, componentAuctionConfig);
next(request, paapiConfig);
}

export const IGB_TO_CONFIG = {
cur: 'perBuyerCurrencies',
pbs: 'perBuyerSignals',
ps: 'perBuyerPrioritySignals',
maxbid: 'auctionSignals.prebid.perBuyerMaxbid',
}

function checkOrigin(igb) {
if (igb.origin) return true;
logWarn('PAAPI buyer does not specify origin and will be ignored', igb);
}

/**
* Convert a list of InterestGroupBuyer (igb) objects into a partial auction config.
* https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/Protected%20Audience%20Support.md
*/
export function mergeBuyers(igbs) {
const buyers = new Set();
return Object.assign(
igbs.reduce((config, igb) => {
if (checkOrigin(igb)) {
if (!buyers.has(igb.origin)) {
buyers.add(igb.origin);
Object.entries(IGB_TO_CONFIG).forEach(([igbField, configField]) => {
if (igb[igbField] != null) {
const entry = deepAccess(config, configField) || {}
entry[igb.origin] = igb[igbField];
deepSetValue(config, configField, entry);
}
});
} else {
logWarn(MODULE, `Duplicate buyer: ${igb.origin}. All but the first will be ignored`, igbs);
}
}
return config;
}, {}),
{
interestGroupBuyers: Array.from(buyers.keys())
}
);
}

/**
* Partition a list of InterestGroupBuyer (igb) object into sets that can each be merged into a single auction.
* If the same buyer (origin) appears more than once, it will be split across different partition unless the igb objects
* are identical.
*/
export function partitionBuyers(igbs) {
return igbs.reduce((partitions, igb) => {
if (checkOrigin(igb)) {
let partition = partitions.find(part => !part.hasOwnProperty(igb.origin) || deepEqual(part[igb.origin], igb));
if (!partition) {
partition = {};
partitions.push(partition);
}
partition[igb.origin] = igb;
}
return partitions;
}, []).map(part => Object.values(part));
}

export function partitionBuyersByBidder(igbRequests) {
const requests = {};
const igbs = {};
igbRequests.forEach(([request, igb]) => {
!requests.hasOwnProperty(request.bidder) && (requests[request.bidder] = request);
append(igbs, request.bidder, igb);
})
return Object.entries(igbs).map(([bidder, igbs]) => [requests[bidder], igbs])
}
/**
* Get PAAPI auction configuration.
*
Expand Down Expand Up @@ -197,9 +315,30 @@ export function markForFledge(next, bidderRequests) {
bidderRequests.forEach((bidderReq) => {
config.runWithBidder(bidderReq.bidderCode, () => {
const {enabled, ae} = getFledgeConfig();
Object.assign(bidderReq, {fledgeEnabled: enabled});
Object.assign(bidderReq, {
fledgeEnabled: enabled,
paapi: {
enabled,
componentSeller: !!moduleConfig.componentSeller?.auctionConfig
}
});
bidderReq.bids.forEach(bidReq => {
deepSetValue(bidReq, 'ortb2Imp.ext.ae', bidReq.ortb2Imp?.ext?.ae ?? ae);
// https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/Protected%20Audience%20Support.md
const igsAe = bidReq.ortb2Imp?.ext?.igs != null
? bidReq.ortb2Imp.ext.igs.ae || 1
: null
const extAe = bidReq.ortb2Imp?.ext?.ae;
if (igsAe !== extAe && igsAe != null && extAe != null) {
logWarn(MODULE, `Bid request defines conflicting ortb2Imp.ext.ae and ortb2Imp.ext.igs, using the latter`, bidReq);
}
const bidAe = igsAe ?? extAe ?? ae;
if (bidAe) {
deepSetValue(bidReq, 'ortb2Imp.ext.ae', bidAe);
bidReq.ortb2Imp.ext.igs = Object.assign({
ae: bidAe,
biddable: 1
}, bidReq.ortb2Imp.ext.igs)
}
});
});
});
Expand All @@ -208,48 +347,78 @@ export function markForFledge(next, bidderRequests) {
}

export function setImpExtAe(imp, bidRequest, context) {
if (imp.ext?.ae && !context.bidderRequest.fledgeEnabled) {
if (!context.bidderRequest.fledgeEnabled) {
delete imp.ext?.ae;
delete imp.ext?.igs;
}
}

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];
function paapiResponseParser(configs, response, context) {
configs.forEach((config) => {
const impCtx = context.impContext[config.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);
logWarn(MODULE, 'Received auction configuration for an impression that was not in the request or did not ask for it', config, impCtx?.imp);
} else {
impCtx.fledgeConfigs = impCtx.fledgeConfigs || [];
impCtx.fledgeConfigs.push(cfg);
impCtx.paapiConfigs = impCtx.paapiConfigs || [];
impCtx.paapiConfigs.push(config);
}
});
}

export function parseExtIgi(response, ortbResponse, context) {
paapiResponseParser(
(ortbResponse.ext?.igi || []).flatMap(igi => {
return (igi?.igs || []).map(igs => {
if (igs.impid !== igi.impid && igs.impid != null && igi.impid != null) {
logWarn(MODULE, 'ORTB response ext.igi.igs.impid conflicts with parent\'s impid', igi);
}
return {
config: igs.config,
impid: igs.impid ?? igi.impid
}
}).concat((igi?.igb || []).map(igb => ({
igb,
impid: igi.impid
})))
}),
response,
context
)
}

// 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) {
paapiResponseParser(
(ortbResponse.ext?.prebid?.fledge?.auctionconfigs || []),
response,
context
)
}

registerOrtbProcessor({type: RESPONSE, name: 'extPrebidFledge', fn: parseExtPrebidFledge, dialects: [PBS]});
registerOrtbProcessor({type: RESPONSE, name: 'extIgiIgs', fn: parseExtIgi});

// ...then, make them available in the adapter's response. This is the client side version, for which the
// interpretResponse api is {fledgeAuctionConfigs: [{bidId, config}]}

export function setResponseFledgeConfigs(response, ortbResponse, context) {
export function setResponsePaapiConfigs(response, ortbResponse, context) {
const configs = Object.values(context.impContext)
.flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => ({
.flatMap((impCtx) => (impCtx.paapiConfigs || []).map(cfg => ({
bidId: impCtx.bidRequest.bidId,
config: cfg.config
...cfg
})));
if (configs.length > 0) {
response.fledgeAuctionConfigs = configs;
response.paapi = configs;
}
}

registerOrtbProcessor({
type: RESPONSE,
name: 'fledgeAuctionConfigs',
priority: -1,
fn: setResponseFledgeConfigs,
dialects: [PBS]
fn: setResponsePaapiConfigs,
});
4 changes: 2 additions & 2 deletions modules/prebidServerBidAdapter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import { EVENTS, REJECTION_REASON, S2S } from '../../src/constants.js';
import adapterManager, {s2sActivityParams} from '../../src/adapterManager.js';
import {config} from '../../src/config.js';
import {addComponentAuction, isValid} from '../../src/adapters/bidderFactory.js';
import {addPaapiConfig, isValid} from '../../src/adapters/bidderFactory.js';
import * as events from '../../src/events.js';
import {includes} from '../../src/polyfill.js';
import {S2S_VENDORS} from './config.js';
Expand Down Expand Up @@ -509,7 +509,7 @@ export function PrebidServer() {
}
},
onFledge: (params) => {
addComponentAuction({auctionId: bidRequests[0].auctionId, ...params}, params.config);
addPaapiConfig({auctionId: bidRequests[0].auctionId, ...params}, {config: params.config});
}
})
}
Expand Down
Loading

0 comments on commit 0ee4efd

Please sign in to comment.