modules/prebidServerBidAdapter/ortbConverter.js
import {ortbConverter} from '../../libraries/ortbConverter/converter.js';
import {deepAccess, deepSetValue, getBidRequest, logError, logWarn, mergeDeep, timestamp} from '../../src/utils.js';
import {config} from '../../src/config.js';
import {S2S, STATUS} from '../../src/constants.js';
import {createBid} from '../../src/bidfactory.js';
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 {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';
import {s2sDefaultConfig} from './index.js';
const DEFAULT_S2S_TTL = 60;
const DEFAULT_S2S_CURRENCY = 'USD';
const DEFAULT_S2S_NETREVENUE = true;
const BIDDER_SPECIFIC_REQUEST_PROPS = new Set(['bidderCode', 'bidderRequestId', 'uniquePbsTid', 'bids', 'timeout']);
const PBS_CONVERTER = ortbConverter({
processors: pbsExtensions,
context: {
netRevenue: DEFAULT_S2S_NETREVENUE,
},
imp(buildImp, proxyBidRequest, context) {
Object.assign(context, proxyBidRequest.pbsData);
const imp = buildImp(proxyBidRequest, context);
(proxyBidRequest.bids || []).forEach(bid => {
if (bid.ortb2Imp && Object.keys(bid.ortb2Imp).length > 0) {
// set bidder-level imp attributes; see https://github.com/prebid/prebid-server/issues/2335
deepSetValue(imp, `ext.prebid.imp.${bid.bidder}`, bid.ortb2Imp);
}
});
if (Object.values(SUPPORTED_MEDIA_TYPES).some(mtype => imp[mtype])) {
imp.secure = context.s2sBidRequest.s2sConfig.secure;
return imp;
}
},
request(buildRequest, imps, proxyBidderRequest, context) {
if (!imps.length) {
logError('Request to Prebid Server rejected due to invalid media type(s) in adUnit.');
} else {
let {s2sBidRequest} = context;
const request = buildRequest(imps, proxyBidderRequest, context);
request.tmax = s2sBidRequest.s2sConfig.timeout ?? Math.min(s2sBidRequest.requestBidsTimeout * 0.75, s2sBidRequest.s2sConfig.maxTimeout ?? s2sDefaultConfig.maxTimeout);
request.ext.tmaxmax = request.ext.tmaxmax || s2sBidRequest.requestBidsTimeout;
[request.app, request.dooh, request.site].forEach(section => {
if (section && !section.publisher?.id) {
deepSetValue(section, 'publisher.id', s2sBidRequest.s2sConfig.accountId);
}
})
if (!context.transmitTids) {
deepSetValue(request, 'ext.prebid.createtids', false);
}
return request;
}
},
bidResponse(buildBidResponse, bid, context) {
// before sending the response throgh "stock" ortb conversion, here we need to:
// - filter out ones that come from an "unknown" bidder (if allowUnknownBidderCode is not set)
// - overwrite context.bidRequest with the actual bid request for this seat / imp combination
let bidRequest = context.actualBidRequests.get(context.seatbid.seat);
if (bidRequest == null) {
// for stored impressions, a request was made with bidder code `null`. Pick it up here so that NO_BID, BID_WON, etc events
// can work as expected (otherwise, the original request will always result in NO_BID).
bidRequest = context.actualBidRequests.get(null);
}
if (bidRequest) {
Object.assign(context, {
bidRequest,
bidderRequest: context.actualBidderRequests.find(req => req.bidderCode === bidRequest.bidder)
})
}
const bidResponse = buildBidResponse(bid, context);
bidResponse.requestBidder = bidRequest?.bidder;
if (bidResponse.native?.ortb) {
// TODO: do we need to set bidResponse.adm here?
// Any consumers can now get the same object from bidResponse.native.ortb;
// I could not find any, which raises the question - who is looking for this?
bidResponse.adm = bidResponse.native.ortb;
}
// because core has special treatment for PBS adapter responses, we need some additional processing
bidResponse.requestTimestamp = context.requestTimestamp;
return {
bid: Object.assign(createBid(STATUS.GOOD, {
src: S2S.SRC,
bidId: bidRequest ? (bidRequest.bidId || bidRequest.bid_Id) : null,
transactionId: context.adUnit.transactionId,
adUnitId: context.adUnit.adUnitId,
auctionId: context.bidderRequest.auctionId,
}), bidResponse),
adUnit: context.adUnit.code
};
},
overrides: {
[IMP]: {
id(orig, imp, proxyBidRequest, context) {
imp.id = context.impId;
},
params(orig, imp, proxyBidRequest, context) {
// override params processing to do it for each bidRequest in this imp;
// also, take overrides from s2sConfig.adapterOptions
const adapterOptions = context.s2sBidRequest.s2sConfig.adapterOptions;
for (const req of context.actualBidRequests.values()) {
setImpBidParams(imp, req, context, context);
if (adapterOptions && adapterOptions[req.bidder]) {
Object.assign(imp.ext.prebid.bidder[req.bidder], adapterOptions[req.bidder]);
}
}
},
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 = {};
orig(floor, req, context);
// if any bid does not have a valid floor, do not attempt to send any to PBS
if (floor.bidfloorcur == null || floor.bidfloor == null) {
min = null;
break;
}
min = min == null ? floor : getMin(min, floor);
}
if (min != null) {
Object.assign(imp, min);
}
}
},
[REQUEST]: {
fpd(orig, ortbRequest, proxyBidderRequest, context) {
// FPD is handled different for PBS - the base request will only contain global FPD;
// bidder-specific values are set in ext.prebid.bidderconfig
if (context.transmitTids) {
deepSetValue(ortbRequest, 'source.tid', proxyBidderRequest.auctionId);
}
mergeDeep(ortbRequest, context.s2sBidRequest.ortb2Fragments?.global);
// also merge in s2sConfig.extPrebid
if (context.s2sBidRequest.s2sConfig.extPrebid && typeof context.s2sBidRequest.s2sConfig.extPrebid === 'object') {
deepSetValue(ortbRequest, 'ext.prebid', mergeDeep(ortbRequest.ext?.prebid || {}, context.s2sBidRequest.s2sConfig.extPrebid));
}
// for global FPD, check allowed activities against "prebid.pbsBidAdapter"...
context.getRedactor().ortb2(ortbRequest);
const fpdConfigs = Object.entries(context.s2sBidRequest.ortb2Fragments?.bidder || {}).filter(([bidder]) => {
const bidders = context.s2sBidRequest.s2sConfig.bidders;
const allowUnknownBidderCodes = context.s2sBidRequest.s2sConfig.allowUnknownBidderCodes;
return allowUnknownBidderCodes || (bidders && bidders.includes(bidder));
}).map(([bidder, ortb2]) => ({
// ... but for bidder specific FPD we can use the actual bidder
bidders: [bidder],
config: {ortb2: context.getRedactor(bidder).ortb2(ortb2)}
}));
if (fpdConfigs.length) {
deepSetValue(ortbRequest, 'ext.prebid.bidderconfig', fpdConfigs);
}
},
extPrebidAliases(orig, ortbRequest, proxyBidderRequest, context) {
// override alias processing to do it for each bidder in the request
context.actualBidderRequests.forEach(req => orig(ortbRequest, req, context));
},
sourceExtSchain(orig, ortbRequest, proxyBidderRequest, context) {
// pass schains in ext.prebid.schains
let chains = (deepAccess(ortbRequest, 'ext.prebid.schains') || []);
const chainBidders = new Set(chains.flatMap((item) => item.bidders));
chains = Object.values(
chains
.concat(context.actualBidderRequests
.filter((req) => !chainBidders.has(req.bidderCode)) // schain defined in s2sConfig.extPrebid takes precedence
.map((req) => ({
bidders: [req.bidderCode],
schain: deepAccess(req, 'bids.0.schain')
})))
.filter(({bidders, schain}) => bidders?.length > 0 && schain)
.reduce((chains, {bidders, schain}) => {
const key = JSON.stringify(schain);
if (!chains.hasOwnProperty(key)) {
chains[key] = {bidders: new Set(), schain};
}
bidders.forEach((bidder) => chains[key].bidders.add(bidder));
return chains;
}, {})
).map(({bidders, schain}) => ({bidders: Array.from(bidders), schain}));
if (chains.length) {
deepSetValue(ortbRequest, 'ext.prebid.schains', chains);
}
}
},
[RESPONSE]: {
serverSideStats(orig, response, ortbResponse, context) {
// override to process each request
context.actualBidderRequests.forEach(req => orig(response, ortbResponse, {...context, bidderRequest: req, bidRequests: req.bids}));
},
paapiConfigs(orig, response, ortbResponse, context) {
const configs = Object.values(context.impContext)
.flatMap((impCtx) => (impCtx.paapiConfigs || []).map(cfg => {
const bidderReq = impCtx.actualBidderRequests.find(br => br.bidderCode === cfg.bidder);
const bidReq = impCtx.actualBidRequests.get(cfg.bidder);
return {
adUnitCode: impCtx.adUnit.code,
ortb2: bidderReq?.ortb2,
ortb2Imp: bidReq?.ortb2Imp,
bidder: cfg.bidder,
config: cfg.config
};
}));
if (configs.length > 0) {
response.paapi = configs;
}
}
}
},
});
export function buildPBSRequest(s2sBidRequest, bidderRequests, adUnits, requestedBidders) {
const requestTimestamp = timestamp();
const impIds = new Set();
const proxyBidRequests = [];
const s2sParams = s2sActivityParams(s2sBidRequest.s2sConfig);
const getRedactor = (() => {
const global = redactor(s2sParams);
const bidders = {};
return (bidder) => {
if (bidder == null) return global;
if (!bidders.hasOwnProperty(bidder)) {
bidders[bidder] = redactor(activityParams(MODULE_TYPE_BIDDER, bidder));
}
return bidders[bidder]
}
})();
adUnits = adUnits.map((au) => getRedactor().bidRequest(au))
adUnits.forEach(adUnit => {
const actualBidRequests = new Map();
adUnits.bids = adUnit.bids.map(br => getRedactor(br.bidder).bidRequest(br));
adUnit.bids.forEach((bid) => {
if (bid.mediaTypes != null) {
// TODO: support labels / conditional bids
// for now, just warn about them
logWarn(`Prebid Server adapter does not (yet) support bidder-specific mediaTypes for the same adUnit. Size mapping configuration will be ignored for adUnit: ${adUnit.code}, bidder: ${bid.bidder}`);
}
actualBidRequests.set(bid.bidder, getBidRequest(bid.bid_id, bidderRequests));
});
let impId = adUnit.code;
let i = 1;
while (impIds.has(impId)) {
i++;
impId = `${adUnit.code}-${i}`;
}
impIds.add(impId)
proxyBidRequests.push({
...adUnit,
adUnitCode: adUnit.code,
pbsData: {impId, actualBidRequests, adUnit},
});
});
const proxyBidderRequest = {
...Object.fromEntries(Object.entries(bidderRequests[0]).filter(([k]) => !BIDDER_SPECIFIC_REQUEST_PROPS.has(k))),
paapi: {
enabled: bidderRequests.some(br => br.paapi?.enabled)
}
}
return PBS_CONVERTER.toORTB({
bidderRequest: proxyBidderRequest,
bidRequests: proxyBidRequests,
context: {
currency: config.getConfig('currency.adServerCurrency') || DEFAULT_S2S_CURRENCY,
ttl: s2sBidRequest.s2sConfig.defaultTtl || DEFAULT_S2S_TTL,
requestTimestamp,
s2sBidRequest,
requestedBidders,
actualBidderRequests: bidderRequests,
nativeRequest: s2sBidRequest.s2sConfig.ortbNative,
getRedactor,
transmitTids: isActivityAllowed(ACTIVITY_TRANSMIT_TID, s2sParams),
}
});
}
export function interpretPBSResponse(response, request) {
return PBS_CONVERTER.fromORTB({response, request});
}