Skip to content

Commit

Permalink
PBS Adapter: fix inconsistency in how bidderconfig is merged, with a …
Browse files Browse the repository at this point in the history
…special case for EIDs (prebid#12571)

* PBS adapter: pre-merge arrays in bidder config

* PBS Adapter: consolidate EIDs using eidpermissions

* fix lint

* optimizations
  • Loading branch information
dgirardi authored Dec 12, 2024
1 parent 91ae090 commit 3782de4
Show file tree
Hide file tree
Showing 3 changed files with 596 additions and 25 deletions.
158 changes: 158 additions & 0 deletions modules/prebidServerBidAdapter/bidderConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import {mergeDeep, deepEqual, deepAccess, deepSetValue, deepClone} from '../../src/utils.js';
import {ORTB_EIDS_PATHS} from '../../src/activities/redactor.js';

/**
* Perform a partial pre-merge of bidder config for PBS.
*
* Prebid.js and Prebid Server use different strategies for merging global and bidder-specific config; JS attemps to
* merge arrays (concatenating them, with some deduping, cfr. mergeDeep), while PBS only merges objects -
* a bidder-specific array will replace a global array.
*
* This returns bidder config (from `bidder`) where arrays are replaced with what you get from merging them with `global`,
* so that the result of merging in PBS is the same as in JS.
*/
export function getPBSBidderConfig({global, bidder}) {
return Object.fromEntries(
Object.entries(bidder).map(([bidderCode, bidderConfig]) => {
return [bidderCode, replaceArrays(bidderConfig, mergeDeep({}, global, bidderConfig))]
})
)
}

function replaceArrays(config, mergedConfig) {
return Object.fromEntries(
Object.entries(config).map(([key, value]) => {
const mergedValue = mergedConfig[key];
if (Array.isArray(value)) {
if (!deepEqual(value, mergedValue) && Array.isArray(mergedValue)) {
value = mergedValue;
}
} else if (value != null && typeof value === 'object') {
value = replaceArrays(value, mergedValue);
}
return [key, value];
})
)
}

/**
* Extract all EIDs from FPD.
*
* Returns {eids, conflicts}, where:
*
* - `eids` contains an object of the form `{eid, bidders}` for each unique EID object found anywhere in FPD;
* `bidders` is a list of all the bidders that refer to that specific EID object, or false if that EID object is defined globally.
* - `conflicts` is a set containing all EID sources that appear in multiple, otherwise different, EID objects.
*/
export function extractEids({global, bidder}) {
const entries = [];
const bySource = {};
const conflicts = new Set()

function getEntry(eid) {
let entry = entries.find((candidate) => deepEqual(candidate.eid, eid));
if (entry == null) {
entry = {eid, bidders: []}
entries.push(entry);
}
if (bySource[eid.source] == null) {
bySource[eid.source] = entry.eid;
} else if (entry.eid === eid) {
// if this is the first time we see this eid, but not the first time we see its source, we have a conflict
conflicts.add(eid.source);
}
return entry;
}

ORTB_EIDS_PATHS.forEach(path => {
(deepAccess(global, path) || []).forEach(eid => {
getEntry(eid).bidders = false;
});
})
Object.entries(bidder).forEach(([bidderCode, bidderConfig]) => {
ORTB_EIDS_PATHS.forEach(path => {
(deepAccess(bidderConfig, path) || []).forEach(eid => {
const entry = getEntry(eid);
if (entry.bidders !== false) {
entry.bidders.push(bidderCode);
}
})
})
})
return {eids: entries, conflicts};
}

/**
* Consolidate extracted EIDs to take advantage of PBS's eidpermissions feature:
* https://docs.prebid.org/prebid-server/endpoints/openrtb2/pbs-endpoint-auction.html#eid-permissions
*
* If different bidders have different EID configurations, in most cases we can avoid repeating it in each bidder's
* specific config. As long as there are no conflicts (different EID objects that refer to the same source constitute a conflict),
* the EID can be set as global, and eidpermissions can restrict its access only to specific bidders.
*
* Returns {global, bidder, permissions}, where:
* - `global` is a list of global EID objects (some of which may be restricted through `permissions`
* - `bidder` is a map from bidder code to EID objects that are specific to that bidder, and cannot be restricted through `permissions`
* - `permissions` is a list of EID permissions as expected by PBS.
*/
export function consolidateEids({eids, conflicts = new Set()}) {
const globalEntries = [];
const bidderEntries = [];
const byBidder = {};
eids.forEach(eid => {
(eid.bidders === false ? globalEntries : bidderEntries).push(eid);
});
bidderEntries.forEach(({eid, bidders}) => {
if (!conflicts.has(eid.source)) {
globalEntries.push({eid, bidders})
} else {
bidders.forEach(bidderCode => {
(byBidder[bidderCode] = byBidder[bidderCode] || []).push(eid)
})
}
});
return {
global: globalEntries.map(({eid}) => eid),
permissions: globalEntries.filter(({bidders}) => bidders !== false).map(({eid, bidders}) => ({
source: eid.source,
bidders
})),
bidder: byBidder
}
}

function replaceEids({global, bidder}) {
const consolidated = consolidateEids(extractEids({global, bidder}));
global = deepClone(global);
bidder = deepClone(bidder);
function removeEids(target) {
delete target?.user?.eids;
delete target?.user?.ext?.eids;
}
removeEids(global);
Object.values(bidder).forEach(removeEids);
if (consolidated.global.length) {
deepSetValue(global, 'user.ext.eids', consolidated.global);
}
if (consolidated.permissions.length) {
deepSetValue(global, 'ext.prebid.data.eidpermissions', consolidated.permissions);
}
Object.entries(consolidated.bidder).forEach(([bidderCode, bidderEids]) => {
if (bidderEids.length) {
deepSetValue(bidder[bidderCode], 'user.ext.eids', bidderEids);
}
})
return {global, bidder}
}

export function premergeFpd(ortb2Fragments) {
if (ortb2Fragments == null || Object.keys(ortb2Fragments.bidder || {}).length === 0) {
return ortb2Fragments;
} else {
ortb2Fragments = replaceEids(ortb2Fragments);
return {
...ortb2Fragments,
bidder: getPBSBidderConfig(ortb2Fragments)
};
}
}
6 changes: 5 additions & 1 deletion modules/prebidServerBidAdapter/ortbConverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {ACTIVITY_TRANSMIT_TID} from '../../src/activities/activities.js';
import {currencyCompare} from '../../libraries/currencyUtils/currency.js';
import {minimum} from '../../src/utils/reducers.js';
import {s2sDefaultConfig} from './index.js';
import {premergeFpd} from './bidderConfig.js';

const DEFAULT_S2S_TTL = 60;
const DEFAULT_S2S_CURRENCY = 'USD';
Expand Down Expand Up @@ -296,7 +297,10 @@ export function buildPBSRequest(s2sBidRequest, bidderRequests, adUnits, requeste
currency: config.getConfig('currency.adServerCurrency') || DEFAULT_S2S_CURRENCY,
ttl: s2sBidRequest.s2sConfig.defaultTtl || DEFAULT_S2S_TTL,
requestTimestamp,
s2sBidRequest,
s2sBidRequest: {
...s2sBidRequest,
ortb2Fragments: premergeFpd(s2sBidRequest.ortb2Fragments)
},
requestedBidders,
actualBidderRequests: bidderRequests,
nativeRequest: s2sBidRequest.s2sConfig.ortbNative,
Expand Down
Loading

0 comments on commit 3782de4

Please sign in to comment.