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: add support for protected audience extensions and "direct" buyers (igb) #11277

Merged
merged 23 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticed on https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/Protected%20Audience%20Support.md, it states that the biddable field is 0 by default (indicating a buyer is not allowed). Also saw in the prebid "fledge" slack channel (about a month back) that the biddable field default may get reversed to 1 instead. Is there an update on the latest around this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK there's no concrete plan for the spec to change.

Copy link
Collaborator

@patmmccann patmmccann May 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this default in the spec a strange choice as well, however, Prebid can default to a different value than a dsp should interpret a result with a missing value. A dsp should consider biddable null as non-biddable, we'll always pass 1, bc we consider these requests worthwhile. What a dsp should consider biddable null to mean doesn't impact what field we should choose to pass.

}, 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 @@ -504,7 +504,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