mk9-prebid
Version:
Header Bidding Management Library
312 lines (269 loc) • 9.55 kB
JavaScript
import buildAdapter from '../src/AnalyticsAdapter.js';
import CONSTANTS from '../src/constants.json';
import adapterManager from '../src/adapterManager.js';
import { ajax } from '../src/ajax.js';
import { logInfo, logError } from '../src/utils.js';
import events from '../src/events.js';
const {
EVENTS: {
AUCTION_END,
TCF2_ENFORCEMENT,
BID_WON,
BID_VIEWABLE,
AD_RENDER_FAILED
}
} = CONSTANTS
const GVLID = 131;
const STANDARD_EVENTS_TO_TRACK = [
AUCTION_END,
TCF2_ENFORCEMENT,
BID_WON,
];
// These events cause the buffered events to be sent over
const FLUSH_EVENTS = [
TCF2_ENFORCEMENT,
AUCTION_END,
BID_WON,
BID_VIEWABLE,
AD_RENDER_FAILED
];
const CONFIG_URL_PREFIX = 'https://api.id5-sync.com/analytics'
const TZ = new Date().getTimezoneOffset();
const PBJS_VERSION = $$PREBID_GLOBAL$$.version;
const ID5_REDACTED = '__ID5_REDACTED__';
const isArray = Array.isArray;
let id5Analytics = Object.assign(buildAdapter({analyticsType: 'endpoint'}), {
// Keeps an array of events for each auction
eventBuffer: {},
eventsToTrack: STANDARD_EVENTS_TO_TRACK,
track: (event) => {
const _this = id5Analytics;
if (!event || !event.args) {
return;
}
try {
const auctionId = event.args.auctionId;
_this.eventBuffer[auctionId] = _this.eventBuffer[auctionId] || [];
// Collect events and send them in a batch when the auction ends
const que = _this.eventBuffer[auctionId];
que.push(_this.makeEvent(event.eventType, event.args));
if (FLUSH_EVENTS.indexOf(event.eventType) >= 0) {
// Auction ended. Send the batch of collected events
_this.sendEvents(que);
// From now on just send events to server side as they come
que.push = (pushedEvent) => _this.sendEvents([pushedEvent]);
}
} catch (error) {
logError('id5Analytics: ERROR', error);
_this.sendErrorEvent(error);
}
},
sendEvents: (eventsToSend) => {
const _this = id5Analytics;
// By giving some content this will be automatically a POST
eventsToSend.forEach((event) =>
ajax(_this.options.ingestUrl, null, JSON.stringify(event)));
},
makeEvent: (event, payload) => {
const _this = id5Analytics;
const filteredPayload = deepTransformingClone(payload,
transformFnFromCleanupRules(event));
return {
source: 'pbjs',
event,
payload: filteredPayload,
partnerId: _this.options.partnerId,
meta: {
sampling: _this.options.id5Sampling,
pbjs: PBJS_VERSION,
tz: TZ,
}
};
},
sendErrorEvent: (error) => {
const _this = id5Analytics;
_this.sendEvents([
_this.makeEvent('analyticsError', {
message: error.message,
stack: error.stack,
})
]);
},
random: () => Math.random(),
});
const ENABLE_FUNCTION = (config) => {
const _this = id5Analytics;
_this.options = (config && config.options) || {};
const partnerId = _this.options.partnerId;
if (typeof partnerId !== 'number') {
logError('id5Analytics: partnerId in config.options must be a number representing the id5 partner ID');
return;
}
ajax(`${CONFIG_URL_PREFIX}/${partnerId}/pbjs`, (result) => {
logInfo('id5Analytics: Received from configuration endpoint', result);
const configFromServer = JSON.parse(result);
const sampling = _this.options.id5Sampling =
typeof configFromServer.sampling === 'number' ? configFromServer.sampling : 0;
if (typeof configFromServer.ingestUrl !== 'string') {
logError('id5Analytics: cannot find ingestUrl in config endpoint response; no analytics will be available');
return;
}
_this.options.ingestUrl = configFromServer.ingestUrl;
// 3-way fallback for which events to track: server > config > standard
_this.eventsToTrack = configFromServer.eventsToTrack || _this.options.eventsToTrack || STANDARD_EVENTS_TO_TRACK;
_this.eventsToTrack = isArray(_this.eventsToTrack) ? _this.eventsToTrack : STANDARD_EVENTS_TO_TRACK;
logInfo('id5Analytics: Configuration is', _this.options);
logInfo('id5Analytics: Tracking events', _this.eventsToTrack);
if (sampling > 0 && _this.random() < (1 / sampling)) {
// Init the module only if we got lucky
logInfo('id5Analytics: Selected by sampling. Starting up!')
// Clean start
_this.eventBuffer = {};
// Replay all events until now
if (!config.disablePastEventsProcessing) {
events.getEvents().forEach((event) => {
if (event && _this.eventsToTrack.indexOf(event.eventType) >= 0) {
_this.track(event);
}
});
}
// Merge in additional cleanup rules
if (configFromServer.additionalCleanupRules) {
const newRules = configFromServer.additionalCleanupRules;
_this.eventsToTrack.forEach((key) => {
// Some protective checks in case we mess up server side
if (
isArray(newRules[key]) &&
newRules[key].every((eventRules) =>
isArray(eventRules.match) &&
(eventRules.apply in TRANSFORM_FUNCTIONS))
) {
logInfo('id5Analytics: merging additional cleanup rules for event ' + key);
CLEANUP_RULES[key].push(...newRules[key]);
}
});
}
// Register to the events of interest
_this.handlers = {};
_this.eventsToTrack.forEach((eventType) => {
const handler = _this.handlers[eventType] = (args) =>
_this.track({ eventType, args });
events.on(eventType, handler);
});
}
});
// Make only one init possible within a lifecycle
_this.enableAnalytics = () => {};
};
id5Analytics.enableAnalytics = ENABLE_FUNCTION;
id5Analytics.disableAnalytics = () => {
const _this = id5Analytics;
// Un-register to the events of interest
_this.eventsToTrack.forEach((eventType) => {
if (_this.handlers && _this.handlers[eventType]) {
events.off(eventType, _this.handlers[eventType]);
}
});
// Make re-init possible. Work around the fact that past events cannot be forgotten
_this.enableAnalytics = (config) => {
config.disablePastEventsProcessing = true;
ENABLE_FUNCTION(config);
};
};
adapterManager.registerAnalyticsAdapter({
adapter: id5Analytics,
code: 'id5Analytics',
gvlid: GVLID
});
export default id5Analytics;
function redact(obj, key) {
obj[key] = ID5_REDACTED;
}
function erase(obj, key) {
delete obj[key];
}
// The transform function matches against a path and applies
// required transformation if match is found.
function deepTransformingClone(obj, transform, currentPath = []) {
const result = isArray(obj) ? [] : {};
const recursable = typeof obj === 'object' && obj !== null;
if (recursable) {
const keys = Object.keys(obj);
if (keys.length > 0) {
keys.forEach((key) => {
const newPath = currentPath.concat(key);
result[key] = deepTransformingClone(obj[key], transform, newPath);
transform(newPath, result, key);
});
return result;
}
}
return obj;
}
// Every set of rules is an object where "match" is an array and
// "apply" is the function to apply in case of match. The function to apply
// takes (obj, prop) and transforms property "prop" in object "obj".
// The "match" is an array of path parts. Each part is either a string or an array.
// In case of array, it represents alternatives which all would match.
// Special path part '*' matches any subproperty or array index.
// Prefixing a part with "!" makes it negative match (doesn't work with multiple alternatives)
const CLEANUP_RULES = {};
CLEANUP_RULES[AUCTION_END] = [{
match: [['adUnits', 'bidderRequests'], '*', 'bids', '*', ['userId', 'crumbs'], '!id5id'],
apply: 'redact'
}, {
match: [['adUnits', 'bidderRequests'], '*', 'bids', '*', ['userId', 'crumbs'], 'id5id', 'uid'],
apply: 'redact'
}, {
match: [['adUnits', 'bidderRequests'], '*', 'bids', '*', 'userIdAsEids', '*', 'uids', '*', ['id', 'ext']],
apply: 'redact'
}, {
match: ['bidderRequests', '*', 'gdprConsent', 'vendorData'],
apply: 'erase'
}, {
match: ['bidsReceived', '*', ['ad', 'native']],
apply: 'erase'
}, {
match: ['noBids', '*', ['userId', 'crumbs'], '*'],
apply: 'redact'
}, {
match: ['noBids', '*', 'userIdAsEids', '*', 'uids', '*', ['id', 'ext']],
apply: 'redact'
}];
CLEANUP_RULES[BID_WON] = [{
match: [['ad', 'native']],
apply: 'erase'
}];
const TRANSFORM_FUNCTIONS = {
'redact': redact,
'erase': erase,
};
// Builds a rule function depending on the event type
function transformFnFromCleanupRules(eventType) {
const rules = CLEANUP_RULES[eventType] || [];
return (path, obj, key) => {
for (let i = 0; i < rules.length; i++) {
let match = true;
const ruleMatcher = rules[i].match;
const transformation = rules[i].apply;
if (ruleMatcher.length !== path.length) {
continue;
}
for (let fragment = 0; fragment < ruleMatcher.length && match; fragment++) {
const choices = makeSureArray(ruleMatcher[fragment]);
match = !choices.every((choice) => choice !== '*' &&
(choice.charAt(0) === '!'
? path[fragment] === choice.substring(1)
: path[fragment] !== choice));
}
if (match) {
const transformfn = TRANSFORM_FUNCTIONS[transformation];
transformfn(obj, key);
break;
}
}
};
}
function makeSureArray(object) {
return isArray(object) ? object : [object];
}