Skip to content

Commit

Permalink
Core & PBS Adapter: support eventtrackers, and normalize burl / `…
Browse files Browse the repository at this point in the history
…ext.prebid.events.win` into it (#12711)

* Extract native event tracker parsing logic

* ortbConverter: set response eventtrackers and translate PBS burl, events.win

* fire impression trackers on billing, win trackers on render

* clean up pbs wurl logic

* more cleanup

* rename analytics to events in markWinningBidAsUsed

* lint fixes

* try to appease jsdoc

* add PBS test case

---------

Co-authored-by: mkomorski <[email protected]>
  • Loading branch information
dgirardi and mkomorski authored Feb 19, 2025
1 parent c64cbbd commit 01a73c1
Show file tree
Hide file tree
Showing 15 changed files with 268 additions and 157 deletions.
3 changes: 3 additions & 0 deletions libraries/ortbConverter/processors/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ export const DEFAULT_PROCESSORS = {
if (bid.attr) {
bidResponse.meta.attr = bid.attr;
}
if (bid.ext?.eventtrackers) {
bidResponse.eventtrackers = (bidResponse.eventtrackers ?? []).concat(bid.ext.eventtrackers);
}
}
}
}
Expand Down
18 changes: 18 additions & 0 deletions libraries/pbsExtensions/processors/eventTrackers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {EVENT_TYPE_IMPRESSION, EVENT_TYPE_WIN, TRACKER_METHOD_IMG} from '../../../src/eventTrackers.js';

export function addEventTrackers(bidResponse, bid) {
bidResponse.eventtrackers = bidResponse.eventtrackers || [];
[
[bid.burl, EVENT_TYPE_IMPRESSION], // core used to fire burl directly, but only for bids coming from PBS
[bid?.ext?.prebid?.events?.win, EVENT_TYPE_WIN]
].filter(([winUrl, type]) => winUrl && bidResponse.eventtrackers.find(
({method, event, url}) => event === type && method === TRACKER_METHOD_IMG && url === winUrl
) == null)
.forEach(([url, event]) => {
bidResponse.eventtrackers.push({
method: TRACKER_METHOD_IMG,
event,
url
})
})
}
12 changes: 4 additions & 8 deletions libraries/pbsExtensions/processors/pbs.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {setImpBidParams} from './params.js';
import {setImpAdUnitCode} from './adUnitCode.js';
import {setRequestExtPrebid, setRequestExtPrebidChannel} from './requestExtPrebid.js';
import {setBidResponseVideoCache} from './video.js';
import {addEventTrackers} from './eventTrackers.js';

export const PBS_PROCESSORS = {
[REQUEST]: {
Expand Down Expand Up @@ -74,14 +75,9 @@ export const PBS_PROCESSORS = {
bidResponse.meta = mergeDeep({}, deepAccess(bid, 'ext.prebid.meta'), bidResponse.meta);
}
},
pbsWurl: {
// sets bidResponse.pbsWurl from ext.prebid.events.win
fn(bidResponse, bid) {
const wurl = deepAccess(bid, 'ext.prebid.events.win');
if (isStr(wurl)) {
bidResponse.pbsWurl = wurl;
}
}
pbsWinTrackers: {
// converts "legacy" burl and ext.prebid.events.win into eventtrackers
fn: addEventTrackers
},
},
[RESPONSE]: {
Expand Down
64 changes: 0 additions & 64 deletions modules/prebidServerBidAdapter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,64 +367,6 @@ function doClientSideSyncs(bidders, gdprConsent, uspConsent, gppConsent) {
});
}

/**
* map wurl to auction id and adId for use in the BID_WON event
*/
let wurlMap = {};

/**
* @param {string} auctionId
* @param {string} adId generated value set to bidObject.adId by bidderFactory Bid()
* @param {string} wurl events.winurl passed from prebidServer as wurl
*/
function addWurl(auctionId, adId, wurl) {
if ([auctionId, adId].every(isStr)) {
wurlMap[`${auctionId}${adId}`] = wurl;
}
}

/**
* @param {string} auctionId
* @param {string} adId generated value set to bidObject.adId by bidderFactory Bid()
*/
function removeWurl(auctionId, adId) {
if ([auctionId, adId].every(isStr)) {
wurlMap[`${auctionId}${adId}`] = undefined;
}
}
/**
* @param {string} auctionId
* @param {string} adId generated value set to bidObject.adId by bidderFactory Bid()
* @return {(string|undefined)} events.winurl which was passed as wurl
*/
function getWurl(auctionId, adId) {
if ([auctionId, adId].every(isStr)) {
return wurlMap[`${auctionId}${adId}`];
}
}

/**
* remove all cached wurls
*/
export function resetWurlMap() {
wurlMap = {};
}

/**
* BID_WON event to request the wurl
* @param {Bid} bid the winning bid object
*/
function bidWonHandler(bid) {
const wurl = getWurl(bid.auctionId, bid.adId);
if (isStr(wurl)) {
logMessage(`Invoking image pixel for wurl on BID_WIN: "${wurl}"`);
triggerPixel(wurl);

// remove from wurl cache, since the wurl url was called
removeWurl(bid.auctionId, bid.adId);
}
}

function getMatchingConsentUrl(urlProp, gdprConsent) {
const hasPurpose = hasPurpose1Consent(gdprConsent);
const url = hasPurpose ? urlProp.p1Consent : urlProp.noP1Consent
Expand Down Expand Up @@ -519,9 +461,6 @@ export function PrebidServer() {
} else {
if (metrics.measureTime('addBidResponse.validate', () => isValid(adUnit, bid))) {
addBidResponse(adUnit, bid);
if (bid.pbsWurl) {
addWurl(bid.auctionId, bid.adId, bid.pbsWurl);
}
} else {
addBidResponse.reject(adUnit, bid, REJECTION_REASON.INVALID);
}
Expand All @@ -536,9 +475,6 @@ export function PrebidServer() {
}
};

// Listen for bid won to call wurl
events.on(EVENTS.BID_WON, bidWonHandler);

return Object.assign(this, {
callBids: baseAdapter.callBids,
setBidderCode: baseAdapter.setBidderCode,
Expand Down
5 changes: 4 additions & 1 deletion src/adRendering.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
insertElement,
logError,
logWarn,
replaceMacros
replaceMacros, triggerPixel
} from './utils.js';
import * as events from './events.js';
import {AD_RENDER_FAILED_REASON, BID_STATUS, EVENTS, MESSAGES, PB_LOCATOR} from './constants.js';
Expand All @@ -20,6 +20,7 @@ import {GreedyPromise} from './utils/promise.js';
import adapterManager from './adapterManager.js';
import {useMetrics} from './utils/perfMetrics.js';
import {filters} from './targeting.js';
import {EVENT_TYPE_WIN, parseEventTrackers, TRACKER_METHOD_IMG} from './eventTrackers.js';

const { AD_RENDER_FAILED, AD_RENDER_SUCCEEDED, STALE_RENDER, BID_WON, EXPIRED_RENDER } = EVENTS;
const { EXCEPTION } = AD_RENDER_FAILED_REASON;
Expand All @@ -31,6 +32,8 @@ export const getBidToRender = hook('sync', function (adId, forRender = true, ove
})

export const markWinningBid = hook('sync', function (bid) {
(parseEventTrackers(bid.eventtrackers)[EVENT_TYPE_WIN]?.[TRACKER_METHOD_IMG] || [])
.forEach(url => triggerPixel(url));
events.emit(BID_WON, bid);
auctionManager.addWinningBid(bid);
})
Expand Down
6 changes: 3 additions & 3 deletions src/adapterManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {isActivityAllowed} from './activities/rules.js';
import {ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from './activities/activities.js';
import {ACTIVITY_PARAM_ANL_CONFIG, ACTIVITY_PARAM_S2S_NAME, activityParamsBuilder} from './activities/params.js';
import {redactor} from './activities/redactor.js';
import {EVENT_TYPE_IMPRESSION, parseEventTrackers, TRACKER_METHOD_IMG} from './eventTrackers.js';

export {gdprDataHandler, gppDataHandler, uspDataHandler, coppaDataHandler} from './consentHandler.js';

Expand Down Expand Up @@ -689,9 +690,8 @@ adapterManager.triggerBilling = (() => {
return (bid) => {
if (!BILLED.has(bid)) {
BILLED.add(bid);
if (bid.source === S2S.SRC && bid.burl) {
internal.triggerPixel(bid.burl);
}
(parseEventTrackers(bid.eventtrackers)[EVENT_TYPE_IMPRESSION]?.[TRACKER_METHOD_IMG] || [])
.forEach((url) => internal.triggerPixel(url));
tryCallBidderMethod(bid.bidder, 'onBidBillable', bid);
}
}
Expand Down
22 changes: 22 additions & 0 deletions src/eventTrackers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const TRACKER_METHOD_IMG = 1;
export const TRACKER_METHOD_JS = 2;
export const EVENT_TYPE_IMPRESSION = 1;
export const EVENT_TYPE_WIN = 500;

/**
* Returns a map from event type (EVENT_TYPE_*)
* to a map from tracker method (TRACKER_METHOD_*)
* to an array of tracking URLs
*
* @param {{}[]} eventTrackers an array of "Event Tracker Response Object" as defined
* in the ORTB native 1.2 spec (https://www.iab.com/wp-content/uploads/2018/03/OpenRTB-Native-Ads-Specification-Final-1.2.pdf, section 5.8)
* @returns {{[type: string]: {[method: string]: string[]}}}
*/
export function parseEventTrackers(eventTrackers) {
return (eventTrackers ?? []).reduce((tally, {event, method, url}) => {
const trackersForType = tally[event] = tally[event] ?? {};
const trackersForMethod = trackersForType[method] = trackersForType[method] ?? [];
trackersForMethod.push(url);
return tally;
}, {})
}
35 changes: 8 additions & 27 deletions src/native.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {NATIVE_ASSET_TYPES, NATIVE_IMAGE_TYPES, PREBID_NATIVE_DATA_KEYS_TO_ORTB,
import {NATIVE} from './mediaTypes.js';
import {getRenderingData} from './adRendering.js';
import {getCreativeRendererSource} from './creativeRenderers.js';
import {EVENT_TYPE_IMPRESSION, parseEventTrackers, TRACKER_METHOD_IMG, TRACKER_METHOD_JS} from './eventTrackers.js';

/**
* @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest
Expand Down Expand Up @@ -89,20 +90,6 @@ const SUPPORTED_TYPES = {
const PREBID_NATIVE_DATA_KEYS_TO_ORTB_INVERSE = inverse(PREBID_NATIVE_DATA_KEYS_TO_ORTB);
const NATIVE_ASSET_TYPES_INVERSE = inverse(NATIVE_ASSET_TYPES);

const TRACKER_METHODS = {
img: 1,
js: 2,
1: 'img',
2: 'js'
}

const TRACKER_EVENTS = {
impression: 1,
'viewable-mrc50': 2,
'viewable-mrc100': 3,
'viewable-video50': 4,
}

export function isNativeResponse(bidResponse) {
// check for native data and not mediaType; it's possible
// to treat banner responses as native
Expand Down Expand Up @@ -289,15 +276,9 @@ export function fireNativeTrackers(message, bidResponse) {
}

export function fireImpressionTrackers(nativeResponse, {runMarkup = (mkup) => insertHtmlIntoIframe(mkup), fetchURL = triggerPixel} = {}) {
const impTrackers = (nativeResponse.eventtrackers || [])
.filter(tracker => tracker.event === TRACKER_EVENTS.impression);

let {img, js} = impTrackers.reduce((tally, tracker) => {
if (TRACKER_METHODS.hasOwnProperty(tracker.method)) {
tally[TRACKER_METHODS[tracker.method]].push(tracker.url)
}
return tally;
}, {img: [], js: []});
let {[TRACKER_METHOD_IMG]: img = [], [TRACKER_METHOD_JS]: js = []} = parseEventTrackers(
nativeResponse.eventtrackers || []
)[EVENT_TYPE_IMPRESSION] || {};

if (nativeResponse.imptrackers) {
img = img.concat(nativeResponse.imptrackers);
Expand Down Expand Up @@ -726,8 +707,8 @@ export function legacyPropertiesToOrtbNative(legacyNative) {
case 'impressionTrackers':
(Array.isArray(value) ? value : [value]).forEach(url => {
response.eventtrackers.push({
event: TRACKER_EVENTS.impression,
method: TRACKER_METHODS.img,
event: EVENT_TYPE_IMPRESSION,
method: TRACKER_METHOD_IMG,
url
});
});
Expand Down Expand Up @@ -830,10 +811,10 @@ export function toLegacyResponse(ortbResponse, ortbRequest) {
legacyResponse.impressionTrackers.push(...ortbResponse.imptrackers);
}
for (const eventTracker of ortbResponse?.eventtrackers || []) {
if (eventTracker.event === TRACKER_EVENTS.impression && eventTracker.method === TRACKER_METHODS.img) {
if (eventTracker.event === EVENT_TYPE_IMPRESSION && eventTracker.method === TRACKER_METHOD_IMG) {
legacyResponse.impressionTrackers.push(eventTracker.url);
}
if (eventTracker.event === TRACKER_EVENTS.impression && eventTracker.method === TRACKER_METHODS.js) {
if (eventTracker.event === EVENT_TYPE_IMPRESSION && eventTracker.method === TRACKER_METHOD_JS) {
jsTrackers.push(eventTracker.url);
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/prebid.js
Original file line number Diff line number Diff line change
Expand Up @@ -929,10 +929,12 @@ if (FEATURES.VIDEO) {
* @typedef {Object} MarkBidRequest
* @property {string} adUnitCode The ad unit code
* @property {string} adId The id representing the ad we want to mark
* @property {boolean} events If true, fires tracking pixels and BID_WON handlers
* @property {boolean} analytics alias of `events` (for backwards compat)
*
* @alias module:pbjs.markWinningBidAsUsed
*/
pbjsInstance.markWinningBidAsUsed = function ({adId, adUnitCode, analytics = false}) {
pbjsInstance.markWinningBidAsUsed = function ({adId, adUnitCode, analytics = false, events = false}) {
let bids;
if (adUnitCode && adId == null) {
bids = targeting.getWinningBids(adUnitCode);
Expand All @@ -942,7 +944,7 @@ if (FEATURES.VIDEO) {
logWarn('Improper use of markWinningBidAsUsed. It needs an adUnitCode or an adId to function.');
}
if (bids.length > 0) {
if (analytics) {
if (analytics || events) {
markWinningBid(bids[0]);
} else {
auctionManager.addWinningBid(bids[0]);
Expand Down
Loading

0 comments on commit 01a73c1

Please sign in to comment.