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

Prebid Server adapter: fledge support #9342

Merged
merged 13 commits into from
Mar 30, 2023
5 changes: 3 additions & 2 deletions libraries/ortbConverter/converter.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,13 @@ export function ortbConverter({
req: Object.assign({bidRequests}, defaultContext, context),
imp: {}
}
ctx.req.impContext = ctx.imp;
const imps = bidRequests.map(bidRequest => {
const impContext = Object.assign({bidderRequest, reqContext: ctx.req}, defaultContext, context);
const result = buildImp(bidRequest, impContext);
if (result != null) {
if (result.hasOwnProperty('id')) {
impContext.bidRequest = bidRequest;
Object.assign(impContext, {bidRequest, imp: result});
ctx.imp[result.id] = impContext;
return result;
}
Expand All @@ -116,7 +117,7 @@ export function ortbConverter({
throw new Error('ortbRequest passed to `fromORTB` must be the same object returned by `toORTB`')
}
function augmentContext(ctx, extraParams = {}) {
return Object.assign({ortbRequest: request}, extraParams, ctx);
return Object.assign(ctx, {ortbRequest: request}, extraParams, ctx);
}
const impsById = Object.fromEntries((request.imp || []).map(imp => [imp.id, imp]));
const bidResponses = (response.seatbid || []).flatMap(seatbid =>
Expand Down
60 changes: 54 additions & 6 deletions modules/fledgeForGpt.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { config } from '../src/config.js';
import { getHook } from '../src/hook.js';
import { getGptSlotForAdUnitCode, logInfo, logWarn } from '../src/utils.js';
import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js';

const MODULE = 'fledgeForGpt'

Expand All @@ -21,22 +22,20 @@ export function init(cfg) {
getHook('addComponentAuction').before(addComponentAuctionHook);
isEnabled = true;
}
logInfo(MODULE, `isEnabled`, cfg);
logInfo(`${MODULE} enabled (browser ${isFledgeSupported() ? 'supports' : 'does NOT support'} fledge)`, cfg);
} else {
if (isEnabled) {
getHook('addComponentAuction').getHooks({hook: addComponentAuctionHook}).remove();
isEnabled = false;
}
logInfo(MODULE, `isDisabled`, cfg);
logInfo(`${MODULE} disabled`, cfg);
}
}

export function addComponentAuctionHook(next, bidRequest, componentAuctionConfig) {
export function addComponentAuctionHook(next, adUnitCode, componentAuctionConfig) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would a hook not benefit more from seeing the original bidRequest ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

possibly, but we don't always have it. In prebid server we may have multiple requests (the fledge config references an imp, but there can be any number of bidders (requests) for that imp). With allowUnknownBidderCodes we may also have zero.

const seller = componentAuctionConfig.seller;
const adUnitCode = bidRequest.adUnitCode;
const gptSlot = getGptSlotForAdUnitCode(adUnitCode);
if (gptSlot && gptSlot.setConfig) {
delete componentAuctionConfig.bidId;
gptSlot.setConfig({
componentAuction: [{
configKey: seller,
Expand All @@ -48,5 +47,54 @@ export function addComponentAuctionHook(next, bidRequest, componentAuctionConfig
logWarn(MODULE, `unable to register component auction config for: ${adUnitCode} x ${seller}.`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should addComponentAuction be skipped altogether if FLEDGE is not supported ?
On such a browser, receiving unsolicited auction configs from an adapter will print this misleading under the circumstances warning.

Copy link
Collaborator Author

@dgirardi dgirardi Mar 14, 2023

Choose a reason for hiding this comment

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

why is the warning misleading? to me it makes perfect sense: if your browser does not support flegde, bidders should not be trying to run fledge auctions and I'd want to be warned if they do.

Copy link
Contributor

Choose a reason for hiding this comment

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

The message "unable to do something" tells me the expectation was that it should have been able to, so something went wrong.
If it said "ignored unsolicited fledge configs", that would be more accurate, but I understand we may not want to have a log message for every specific condition

}

next(bidRequest, componentAuctionConfig);
next(adUnitCode, componentAuctionConfig);
}

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

export function markForFledge(next, bidderRequests) {
if (isFledgeSupported()) {
bidderRequests.forEach((req) => {
req.fledgeEnabled = config.runWithBidder(req.bidderCode, () => config.getConfig('fledgeEnabled'))
})
}
next(bidderRequests);
}
getHook('makeBidRequests').after(markForFledge);

export function setImpExtAe(imp, bidRequest, context) {
if (!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);
Copy link
Contributor

Choose a reason for hiding this comment

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

validation behavior is different between PBS and regular adapter. If adapters adopt this method it may become a non-issue but it may be confusing as is.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I am not sure this validation makes sense outside of here. There are two things we are checking here -

  1. the request should have set imp.ext.ae; does this check make sense for an adapter that (in general) may not be using ORTB at all?
  2. the fledge config should refer to an imp that existed in the request. this is necessary for PBS because the protocol pairs a fledge config with impid. But, for client-side adapters, they must provide a valid bidId instead - the check is here, although there's no warning.

I can add a warning for 2, I'm not sure about 1.

Copy link
Contributor

Choose a reason for hiding this comment

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

Should be fine to leave it as it is now and update in a later iteration if it becomes a problem with usage.

} else {
impCtx.fledgeConfigs = impCtx.fledgeConfigs || [];
impCtx.fledgeConfigs.push(cfg);
}
})
}
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}]}

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;
}
}
registerOrtbProcessor({type: RESPONSE, name: 'fledgeAuctionConfigs', priority: -1, fn: setResponseFledgeConfigs, dialects: [PBS]})
12 changes: 5 additions & 7 deletions modules/openxOrtbBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ const converter = ortbConverter({
if (bidRequest.mediaTypes[VIDEO]?.context === 'outstream') {
imp.video.placement = imp.video.placement || 4;
}
if (imp.ext?.ae && !context.bidderRequest.fledgeEnabled) {
// TODO: we may want to standardize this and move fledge logic to ortbConverter
delete imp.ext.ae;
}
mergeDeep(imp, {
tagid: bidRequest.params.unit,
ext: {
Expand Down Expand Up @@ -106,10 +102,12 @@ const converter = ortbConverter({
let fledgeAuctionConfigs = utils.deepAccess(ortbResponse, 'ext.fledge_auction_configs');
if (fledgeAuctionConfigs) {
fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => {
return Object.assign({
return {
bidId,
auctionSignals: {}
}, cfg);
config: Object.assign({
auctionSignals: {},
}, cfg)
}
});
return {
bids: response.bids,
Expand Down
12 changes: 9 additions & 3 deletions modules/prebidServerBidAdapter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
import CONSTANTS from '../../src/constants.json';
import adapterManager from '../../src/adapterManager.js';
import {config} from '../../src/config.js';
import {isValid} from '../../src/adapters/bidderFactory.js';
import {addComponentAuction, 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 @@ -496,6 +496,9 @@ export function PrebidServer() {
addBidResponse.reject(adUnit, bid, CONSTANTS.REJECTION_REASON.INVALID);
}
}
},
onFledge: ({adUnitCode, config}) => {
addComponentAuction(adUnitCode, config);
}
})
}
Expand All @@ -521,7 +524,7 @@ export function PrebidServer() {
* @param onError {function(String, {})} invoked on HTTP failure - with status message and XHR error
* @param onBid {function({})} invoked once for each bid in the response - with the bid as returned by interpretResponse
*/
export const processPBSRequest = hook('sync', function (s2sBidRequest, bidRequests, ajax, {onResponse, onError, onBid}) {
export const processPBSRequest = hook('sync', function (s2sBidRequest, bidRequests, ajax, {onResponse, onError, onBid, onFledge}) {
let { gdprConsent } = getConsentData(bidRequests);
const adUnits = deepClone(s2sBidRequest.ad_units);

Expand All @@ -545,8 +548,11 @@ export const processPBSRequest = hook('sync', function (s2sBidRequest, bidReques
let result;
try {
result = JSON.parse(response);
const bids = s2sBidRequest.metrics.measureTime('interpretResponse', () => interpretPBSResponse(result, request).bids);
const {bids, fledgeAuctionConfigs} = s2sBidRequest.metrics.measureTime('interpretResponse', () => interpretPBSResponse(result, request));
bids.forEach(onBid);
if (fledgeAuctionConfigs) {
fledgeAuctionConfigs.forEach(onFledge);
}
} catch (error) {
logError(error);
}
Expand Down
14 changes: 12 additions & 2 deletions modules/prebidServerBidAdapter/ortbConverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,13 @@ const PBS_CONVERTER = ortbConverter({
serverSideStats(orig, response, ortbResponse, context) {
// override to process each request
context.actualBidderRequests.forEach(req => orig(response, ortbResponse, {...context, bidderRequest: req, bidRequests: req.bids}));
},
fledgeAuctionConfigs(orig, response, ortbResponse, context) {
const configs = Object.values(context.impContext)
.flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => ({adUnitCode: impCtx.adUnit.code, config: cfg.config})));
if (configs.length > 0) {
response.fledgeAuctionConfigs = configs;
}
}
}
},
Expand Down Expand Up @@ -254,11 +261,14 @@ export function buildPBSRequest(s2sBidRequest, bidderRequests, adUnits, requeste
...adUnit,
adUnitCode: adUnit.code,
...getDefinedParams(actualBidRequests.values().next().value || {}, ['userId', 'userIdAsEids', 'schain']),
pbsData: {impId, actualBidRequests, adUnit}
pbsData: {impId, actualBidRequests, adUnit},
});
});

const proxyBidderRequest = Object.fromEntries(Object.entries(bidderRequests[0]).filter(([k]) => !BIDDER_SPECIFIC_REQUEST_PROPS.has(k)))
const proxyBidderRequest = {
...Object.fromEntries(Object.entries(bidderRequests[0]).filter(([k]) => !BIDDER_SPECIFIC_REQUEST_PROPS.has(k))),
fledgeEnabled: bidderRequests.some(req => req.fledgeEnabled)
}

return PBS_CONVERTER.toORTB({
bidderRequest: proxyBidderRequest,
Expand Down
8 changes: 5 additions & 3 deletions modules/rtbhouseBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,12 @@ export const spec = {

if (fledgeAuctionConfigs) {
fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => {
return Object.assign({
return {
bidId,
auctionSignals: {}
}, cfg);
config: Object.assign({
auctionSignals: {}
}, cfg)
}
});
logInfo('Response with FLEDGE:', { bids, fledgeAuctionConfigs });
return {
Expand Down
7 changes: 0 additions & 7 deletions src/adapterManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,13 +338,6 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a
}
});

bidRequests.forEach(bidRequest => {
config.runWithBidder(bidRequest.bidderCode, () => {
const fledgeEnabledFromConfig = config.getConfig('fledgeEnabled');
bidRequest['fledgeEnabled'] = navigator.runAdAuction && fledgeEnabledFromConfig
});
});

return bidRequests;
}, 'makeBidRequests');

Expand Down
43 changes: 28 additions & 15 deletions src/adapters/bidderFactory.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import Adapter from '../adapter.js';
import adapterManager from '../adapterManager.js';
import { config } from '../config.js';
import { createBid } from '../bidfactory.js';
import { userSync } from '../userSync.js';
import { nativeBidIsValid } from '../native.js';
import { isValidVideoBid } from '../video.js';
import {config} from '../config.js';
import {createBid} from '../bidfactory.js';
import {userSync} from '../userSync.js';
import {nativeBidIsValid} from '../native.js';
import {isValidVideoBid} from '../video.js';
import CONSTANTS from '../constants.json';
import * as events from '../events.js';
import {includes} from '../polyfill.js';
import { ajax } from '../ajax.js';
import { logWarn, logInfo, logError, parseQueryStringParameters, delayExecution, parseSizesInput, flatten, uniques, timestamp, deepAccess, isArray, isPlainObject } from '../utils.js';
import { ADPOD } from '../mediaTypes.js';
import { getHook, hook } from '../hook.js';
import { getCoreStorageManager } from '../storageManager.js';
import {ajax} from '../ajax.js';
import {
deepAccess,
delayExecution,
flatten,
isArray,
isPlainObject,
logError,
logWarn,
parseQueryStringParameters,
parseSizesInput,
timestamp,
uniques
} from '../utils.js';
import {ADPOD} from '../mediaTypes.js';
import {getHook, hook} from '../hook.js';
import {getCoreStorageManager} from '../storageManager.js';
import {auctionManager} from '../auctionManager.js';
import { bidderSettings } from '../bidderSettings.js';
import {bidderSettings} from '../bidderSettings.js';
import {useMetrics} from '../utils/perfMetrics.js';

export const storage = getCoreStorageManager('bidderFactory');
Expand Down Expand Up @@ -247,7 +259,9 @@ export function newBidder(spec) {
fledgeAuctionConfigs.forEach((fledgeAuctionConfig) => {
const bidRequest = bidRequestMap[fledgeAuctionConfig.bidId];
if (bidRequest) {
addComponentAuction(bidRequest, fledgeAuctionConfig);
addComponentAuction(bidRequest.adUnitCode, fledgeAuctionConfig.config);
} else {
logWarn('Received fledge auction configuration for an unknown bidId', fledgeAuctionConfig);
}
});
},
Expand Down Expand Up @@ -467,9 +481,8 @@ export const registerSyncInner = hook('async', function(spec, responses, gdprCon
}
}, 'registerSyncs')

export const addComponentAuction = hook('sync', (_bidRequest, fledgeAuctionConfig) => {
logInfo(`bidderFactory.addComponentAuction`, fledgeAuctionConfig);
}, 'addComponentAuction')
export const addComponentAuction = hook('sync', (adUnitCode, fledgeAuctionConfig) => {
}, 'addComponentAuction');

export function preloadBidderMappingFile(fn, adUnits) {
if (!config.getConfig('adpod.brandCategoryExclusion')) {
Expand Down
Loading