Skip to content

Commit

Permalink
Core: deferred rendering (#11914)
Browse files Browse the repository at this point in the history
* mark bids with deferRendering / deferBilling

* refactor internals of triggerBilling

* deferred rendering

* trigger deferred render from triggerBilling

* Fix dfp gdpr parameter dupes
  • Loading branch information
dgirardi authored Sep 24, 2024
1 parent 5135da2 commit 4675958
Show file tree
Hide file tree
Showing 23 changed files with 451 additions and 243 deletions.
16 changes: 11 additions & 5 deletions libraries/dfpUtils/dfpUtils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {gdprDataHandler} from '../../src/consentHandler.js';

/** Safe defaults which work on pretty much all video calls. */
export const DEFAULT_DFP_PARAMS = {
env: 'vp',
Expand All @@ -12,9 +14,13 @@ export const DFP_ENDPOINT = {
pathname: '/gampad/ads'
}

export const setGdprConsent = (gdprConsent, queryParams) => {
if (!gdprConsent) { return; }
if (typeof gdprConsent.gdprApplies === 'boolean') { queryParams.gdpr = Number(gdprConsent.gdprApplies); }
if (gdprConsent.consentString) { queryParams.gdpr_consent = gdprConsent.consentString; }
if (gdprConsent.addtlConsent) { queryParams.addtl_consent = gdprConsent.addtlConsent; }
export function gdprParams() {
const gdprConsent = gdprDataHandler.getConsentData();
const params = {};
if (gdprConsent) {
if (typeof gdprConsent.gdprApplies === 'boolean') { params.gdpr = Number(gdprConsent.gdprApplies); }
if (gdprConsent.consentString) { params.gdpr_consent = gdprConsent.consentString; }
if (gdprConsent.addtlConsent) { params.addtl_consent = gdprConsent.addtlConsent; }
}
return params;
}
19 changes: 6 additions & 13 deletions modules/bidViewability.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

import {config} from '../src/config.js';
import * as events from '../src/events.js';
import { EVENTS } from '../src/constants.js';
import {EVENTS} from '../src/constants.js';
import {isFn, logWarn, triggerPixel} from '../src/utils.js';
import {getGlobal} from '../src/prebidGlobal.js';
import adapterManager, {gdprDataHandler, uspDataHandler, gppDataHandler} from '../src/adapterManager.js';
import adapterManager, {gppDataHandler, uspDataHandler} from '../src/adapterManager.js';
import {find} from '../src/polyfill.js';
import {gdprParams} from '../libraries/dfpUtils/dfpUtils.js';

const MODULE_NAME = 'bidViewability';
const CONFIG_ENABLED = 'enabled';
Expand All @@ -32,14 +33,7 @@ export let getMatchingWinningBidForGPTSlot = (globalModuleConfig, slot) => {

export let fireViewabilityPixels = (globalModuleConfig, bid) => {
if (globalModuleConfig[CONFIG_FIRE_PIXELS] === true && bid.hasOwnProperty(BID_VURL_ARRAY)) {
let queryParams = {};

const gdprConsent = gdprDataHandler.getConsentData();
if (gdprConsent) {
if (typeof gdprConsent.gdprApplies === 'boolean') { queryParams.gdpr = Number(gdprConsent.gdprApplies); }
if (gdprConsent.consentString) { queryParams.gdpr_consent = gdprConsent.consentString; }
if (gdprConsent.addtlConsent) { queryParams.addtl_consent = gdprConsent.addtlConsent; }
}
let queryParams = gdprParams();

const uspConsent = uspDataHandler.getConsentData();
if (uspConsent) { queryParams.us_privacy = uspConsent; }
Expand Down Expand Up @@ -67,7 +61,6 @@ export let logWinningBidNotFound = (slot) => {

export let impressionViewableHandler = (globalModuleConfig, slot, event) => {
let respectiveBid = getMatchingWinningBidForGPTSlot(globalModuleConfig, slot);
let respectiveDeferredAdUnit = getGlobal().adUnits.find(adUnit => adUnit.deferBilling && respectiveBid.adUnitCode === adUnit.code);

if (respectiveBid === null) {
logWinningBidNotFound(slot);
Expand All @@ -77,8 +70,8 @@ export let impressionViewableHandler = (globalModuleConfig, slot, event) => {
// trigger respective bidder's onBidViewable handler
adapterManager.callBidViewableBidder(respectiveBid.adapterCode || respectiveBid.bidder, respectiveBid);

if (respectiveDeferredAdUnit) {
adapterManager.callBidBillableBidder(respectiveBid);
if (respectiveBid.deferBilling) {
adapterManager.triggerBilling(respectiveBid);
}

// emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels
Expand Down
8 changes: 3 additions & 5 deletions modules/dfpAdServerVideo.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
* This module adds [DFP support]{@link https://www.doubleclickbygoogle.com/} for Video to Prebid.
*/

import { DEFAULT_DFP_PARAMS, DFP_ENDPOINT, setGdprConsent } from '../libraries/dfpUtils/dfpUtils.js';
import { getSignals } from '../libraries/gptUtils/gptUtils.js';
import { registerVideoSupport } from '../src/adServerManager.js';
import { gdprDataHandler } from '../src/adapterManager.js';
import { getPPID } from '../src/adserver.js';
import { auctionManager } from '../src/auctionManager.js';
import { config } from '../src/config.js';
Expand All @@ -24,6 +22,7 @@ import {
parseSizesInput,
parseUrl
} from '../src/utils.js';
import {DEFAULT_DFP_PARAMS, DFP_ENDPOINT, gdprParams} from '../libraries/dfpUtils/dfpUtils.js';
/**
* @typedef {Object} DfpVideoParams
*
Expand Down Expand Up @@ -108,13 +107,12 @@ export function buildDfpVideoUrl(options) {
urlComponents.search,
derivedParams,
options.params,
{ cust_params: encodedCustomParams }
{ cust_params: encodedCustomParams },
gdprParams()
);

const descriptionUrl = getDescriptionUrl(bid, options, 'params');
if (descriptionUrl) { queryParams.description_url = descriptionUrl; }
const gdprConsent = gdprDataHandler.getConsentData();
setGdprConsent(gdprConsent, queryParams);

if (!queryParams.ppid) {
const ppid = getPPID();
Expand Down
9 changes: 3 additions & 6 deletions modules/dfpAdpod.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {submodule} from '../src/hook.js';
import {buildUrl, deepAccess, formatQS, logError, parseSizesInput} from '../src/utils.js';
import {auctionManager} from '../src/auctionManager.js';
import {DEFAULT_DFP_PARAMS, DFP_ENDPOINT, setGdprConsent} from '../libraries/dfpUtils/dfpUtils.js';
import {gdprDataHandler} from '../src/consentHandler.js';
import {DEFAULT_DFP_PARAMS, DFP_ENDPOINT, gdprParams} from '../libraries/dfpUtils/dfpUtils.js';
import {registerVideoSupport} from '../src/adServerManager.js';

export const adpodUtils = {};
Expand Down Expand Up @@ -75,12 +74,10 @@ export function buildAdpodVideoUrl({code, params, callback} = {}) {
DEFAULT_DFP_PARAMS,
derivedParams,
params,
{ cust_params: encodedCustomParams }
{ cust_params: encodedCustomParams },
gdprParams(),
);

const gdprConsent = gdprDataHandler.getConsentData();
setGdprConsent(gdprConsent, queryParams);

const masterTag = buildUrl({
...DFP_ENDPOINT,
search: queryParams
Expand Down
5 changes: 4 additions & 1 deletion modules/prebidServerBidAdapter/ortbConverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ const PBS_CONVERTER = ortbConverter({
transactionId: context.adUnit.transactionId,
adUnitId: context.adUnit.adUnitId,
auctionId: context.bidderRequest.auctionId,
}), bidResponse),
}), bidResponse, {
deferRendering: !!context.adUnit.deferBilling,
deferBilling: !!context.adUnit.deferBilling
}),
adUnit: context.adUnit.code
};
},
Expand Down
1 change: 0 additions & 1 deletion modules/topLevelPaapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ export function getRenderingDataHook(next, bid, options) {

export function markWinningBidHook(next, bid) {
if (isPaapiBid(bid)) {
bid.status = BID_STATUS.RENDERED;
emit(EVENTS.BID_WON, bid);
next.bail();
} else {
Expand Down
81 changes: 60 additions & 21 deletions src/adRendering.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {hook} from './hook.js';
import {fireNativeTrackers} from './native.js';
import {GreedyPromise} from './utils/promise.js';
import adapterManager from './adapterManager.js';
import {useMetrics} from './utils/perfMetrics.js';

const { AD_RENDER_FAILED, AD_RENDER_SUCCEEDED, STALE_RENDER, BID_WON } = EVENTS;
const { EXCEPTION } = AD_RENDER_FAILED_REASON;
Expand Down Expand Up @@ -167,32 +168,70 @@ doRender.before(function (next, args) {
}, 100)

export function handleRender({renderFn, resizeFn, adId, options, bidResponse, doc}) {
deferRendering(bidResponse, () => {
if (bidResponse == null) {
emitAdRenderFail({
reason: AD_RENDER_FAILED_REASON.CANNOT_FIND_AD,
message: `Cannot find ad '${adId}'`,
id: adId
});
return;
}
if (bidResponse.status === BID_STATUS.RENDERED) {
logWarn(`Ad id ${adId} has been rendered before`);
events.emit(STALE_RENDER, bidResponse);
if (deepAccess(config.getConfig('auctionOptions'), 'suppressStaleRender')) {
return;
}
}
try {
doRender({renderFn, resizeFn, bidResponse, options, doc});
} catch (e) {
emitAdRenderFail({
reason: AD_RENDER_FAILED_REASON.EXCEPTION,
message: e.message,
id: adId,
bid: bidResponse
});
}
})
}

export function markBidAsRendered(bidResponse) {
const metrics = useMetrics(bidResponse.metrics);
metrics.checkpoint('bidRender');
metrics.timeBetween('bidWon', 'bidRender', 'render.deferred');
metrics.timeBetween('auctionEnd', 'bidRender', 'render.pending');
metrics.timeBetween('requestBids', 'bidRender', 'render.e2e');
bidResponse.status = BID_STATUS.RENDERED;
}

const DEFERRED_RENDER = new WeakMap();
const WINNERS = new WeakSet();

export function deferRendering(bidResponse, renderFn) {
if (bidResponse == null) {
emitAdRenderFail({
reason: AD_RENDER_FAILED_REASON.CANNOT_FIND_AD,
message: `Cannot find ad '${adId}'`,
id: adId
});
// if the bid is missing, let renderFn deal with it now
renderFn();
return;
}
if (bidResponse.status === BID_STATUS.RENDERED) {
logWarn(`Ad id ${adId} has been rendered before`);
events.emit(STALE_RENDER, bidResponse);
if (deepAccess(config.getConfig('auctionOptions'), 'suppressStaleRender')) {
return;
}
DEFERRED_RENDER.set(bidResponse, renderFn);
if (!bidResponse.deferRendering) {
renderIfDeferred(bidResponse);
}
try {
doRender({renderFn, resizeFn, bidResponse, options, doc});
} catch (e) {
emitAdRenderFail({
reason: AD_RENDER_FAILED_REASON.EXCEPTION,
message: e.message,
id: adId,
bid: bidResponse
});
if (!WINNERS.has(bidResponse)) {
WINNERS.add(bidResponse);
markWinningBid(bidResponse);
}
}

export function renderIfDeferred(bidResponse) {
const renderFn = DEFERRED_RENDER.get(bidResponse);
if (renderFn) {
renderFn();
markBidAsRendered(bidResponse);
DEFERRED_RENDER.delete(bidResponse);
}
markWinningBid(bidResponse);
}

export function renderAdDirect(doc, adId, options) {
Expand Down
27 changes: 20 additions & 7 deletions src/adapterManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getUniqueIdentifierStr,
getUserConfiguredParams,
groupBy,
internal,
isArray,
isPlainObject,
isValidMediaTypes,
Expand All @@ -30,13 +31,15 @@ import {find, includes} from './polyfill.js';
import {
getBidderRequestsCounter,
getBidderWinsCounter,
getRequestsCounter, incrementBidderRequestsCounter,
incrementBidderWinsCounter, incrementRequestsCounter
getRequestsCounter,
incrementBidderRequestsCounter,
incrementBidderWinsCounter,
incrementRequestsCounter
} from './adUnits.js';
import {getRefererInfo} from './refererDetection.js';
import {GDPR_GVLIDS, gdprDataHandler, gppDataHandler, uspDataHandler, } from './consentHandler.js';
import * as events from './events.js';
import { EVENTS, S2S } from './constants.js';
import {EVENTS, S2S} from './constants.js';
import {useMetrics} from './utils/perfMetrics.js';
import {auctionManager} from './auctionManager.js';
import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, MODULE_TYPE_PREBID} from './activities/modules.js';
Expand Down Expand Up @@ -134,6 +137,7 @@ function getBids({bidderCode, auctionId, bidderRequestId, adUnits, src, metrics}
bidRequestsCount: getRequestsCounter(adUnit.code),
bidderRequestsCount: getBidderRequestsCounter(adUnit.code, bid.bidder),
bidderWinsCount: getBidderWinsCounter(adUnit.code, bid.bidder),
deferBilling: !!adUnit.deferBilling
}));
return bids;
}, [])
Expand Down Expand Up @@ -644,7 +648,7 @@ function invokeBidderMethod(bidder, method, spec, fn, ...params) {
}

function tryCallBidderMethod(bidder, method, param) {
if (param?.src !== S2S.SRC) {
if (param?.source !== S2S.SRC) {
const target = getBidderMethod(bidder, method);
if (target != null) {
invokeBidderMethod(bidder, method, ...target, param);
Expand Down Expand Up @@ -673,9 +677,18 @@ adapterManager.callBidWonBidder = function(bidder, bid, adUnits) {
tryCallBidderMethod(bidder, 'onBidWon', bid);
};

adapterManager.callBidBillableBidder = function(bid) {
tryCallBidderMethod(bid.bidder, 'onBidBillable', bid);
};
adapterManager.triggerBilling = (() => {
const BILLED = new WeakSet();
return (bid) => {
if (!BILLED.has(bid)) {
BILLED.add(bid);
if (bid.source === S2S.SRC && bid.burl) {
internal.triggerPixel(bid.burl);
}
tryCallBidderMethod(bid.bidder, 'onBidBillable', bid);
}
}
})();

adapterManager.callSetTargetingBidder = function(bidder, bid) {
tryCallBidderMethod(bidder, 'onSetTargeting', bid);
Expand Down
2 changes: 2 additions & 0 deletions src/adapters/bidderFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ export function newBidder(spec) {
bid.originalCpm = bid.cpm;
bid.originalCurrency = bid.currency;
bid.meta = bid.meta || Object.assign({}, bid[bidRequest.bidder]);
bid.deferBilling = bidRequest.deferBilling;
bid.deferRendering = bid.deferBilling && (bid.deferRendering ?? typeof spec.onBidBillable !== 'function');
const prebidBid = Object.assign(createBid(STATUS.GOOD, bidRequest), bid, pick(bidRequest, TIDS));
addBidWithCode(bidRequest.adUnitCode, prebidBid);
} else {
Expand Down
7 changes: 3 additions & 4 deletions src/auction.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
*/

import {
callBurl,
deepAccess,
generateUUID,
getValue,
Expand Down Expand Up @@ -370,11 +369,11 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a
}

function addWinningBid(winningBid) {
const winningAd = adUnits.find(adUnit => adUnit.adUnitId === winningBid.adUnitId);
_winningBids = _winningBids.concat(winningBid);
callBurl(winningBid);
adapterManager.callBidWonBidder(winningBid.adapterCode || winningBid.bidder, winningBid, adUnits);
if (winningAd && !winningAd.deferBilling) adapterManager.callBidBillableBidder(winningBid);
if (!winningBid.deferBilling) {
adapterManager.triggerBilling(winningBid)
}
}

function setBidTargeting(bid) {
Expand Down
5 changes: 2 additions & 3 deletions src/auctionManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,10 @@ export function newAuctionManager() {
auctionManager.addWinningBid = function(bid) {
const metrics = useMetrics(bid.metrics);
metrics.checkpoint('bidWon');
metrics.timeBetween('auctionEnd', 'bidWon', 'render.pending');
metrics.timeBetween('requestBids', 'bidWon', 'render.e2e');
metrics.timeBetween('auctionEnd', 'bidWon', 'adserver.pending');
metrics.timeBetween('requestBids', 'bidWon', 'adserver.e2e');
const auction = getAuction(bid.auctionId);
if (auction) {
bid.status = BID_STATUS.RENDERED;
auction.addWinningBid(bid);
} else {
logWarn(`Auction not found when adding winning bid`);
Expand Down
Loading

0 comments on commit 4675958

Please sign in to comment.