Skip to content

Commit 86bbf54

Browse files
committed
prebid#24 Adding cleanup transformations, improvements and tests
1 parent 049d5c0 commit 86bbf54

File tree

2 files changed

+340
-60
lines changed

2 files changed

+340
-60
lines changed

modules/id5AnalyticsAdapter.js

Lines changed: 155 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,41 @@ import { ajax } from '../src/ajax.js';
55
import { logWarn, logInfo, logError } from '../src/utils.js';
66
import events from '../src/events.js';
77

8+
const {
9+
EVENTS: {
10+
AUCTION_END,
11+
AUCTION_INIT,
12+
TCF2_ENFORCEMENT
13+
}
14+
} = CONSTANTS
15+
816
const GVLID = 131;
9-
const EVENTS_TO_TRACK = [
10-
CONSTANTS.EVENTS.AUCTION_END,
11-
CONSTANTS.EVENTS.AUCTION_INIT,
12-
CONSTANTS.EVENTS.TCF2_ENFORCEMENT,
17+
const STANDARD_EVENTS_TO_TRACK = [
18+
AUCTION_END,
19+
AUCTION_INIT,
20+
TCF2_ENFORCEMENT,
1321
];
1422
const FLUSH_EVENTS = [
15-
CONSTANTS.EVENTS.TCF2_ENFORCEMENT,
16-
CONSTANTS.EVENTS.AUCTION_END,
23+
TCF2_ENFORCEMENT,
24+
AUCTION_END,
1725
];
1826
// const CONFIG_URL_PREFIX = 'https://api.id5-sync.com/analytics'
1927
const CONFIG_URL_PREFIX = 'https://127.0.0.1:8443/analytics'
2028
const TZ = new Date().getTimezoneOffset();
2129
const PBJS_VERSION = $$PREBID_GLOBAL$$.version;
30+
const ID5_REDACTED = '__ID5_REDACTED__';
31+
const isArray = Array.isArray;
2232

2333
let id5Analytics = Object.assign(buildAdapter({analyticsType: 'endpoint'}), {
2434
// Keeps an array of events for each auction
2535
eventBuffer: {},
2636

37+
eventsToTrack: STANDARD_EVENTS_TO_TRACK,
38+
2739
track: (event) => {
2840
const _this = id5Analytics;
2941

30-
if (!event) {
42+
if (!event || !event.args) {
3143
return;
3244
}
3345

@@ -37,7 +49,7 @@ let id5Analytics = Object.assign(buildAdapter({analyticsType: 'endpoint'}), {
3749

3850
// Collect events and send them in a batch when the auction ends
3951
const que = _this.eventBuffer[auctionId];
40-
que.push(_this.makeEvent('pbjs_' + event.eventType, event.args));
52+
que.push(_this.makeEvent(event.eventType, event.args));
4153

4254
if (FLUSH_EVENTS.indexOf(event.eventType) >= 0) {
4355
// Auction ended. Send the batch of collected events
@@ -61,9 +73,12 @@ let id5Analytics = Object.assign(buildAdapter({analyticsType: 'endpoint'}), {
6173

6274
makeEvent: (event, payload) => {
6375
const _this = id5Analytics;
76+
const filteredPayload = deepTransformingClone(payload,
77+
transformFnFromCleanupRules(event));
6478
return {
79+
source: 'pbjs',
6580
event,
66-
payload,
81+
payload: filteredPayload,
6782
partnerId: _this.options.partnerId,
6883
meta: {
6984
sampling: _this.options.id5Sampling,
@@ -76,7 +91,7 @@ let id5Analytics = Object.assign(buildAdapter({analyticsType: 'endpoint'}), {
7691
sendErrorEvent: (error) => {
7792
const _this = id5Analytics;
7893
_this.sendEvents([
79-
_this.makeEvent('pbjs_analyticsError', {
94+
_this.makeEvent('analyticsError', {
8095
message: error.message,
8196
stack: error.stack,
8297
})
@@ -87,65 +102,94 @@ let id5Analytics = Object.assign(buildAdapter({analyticsType: 'endpoint'}), {
87102
random: () => Math.random(),
88103
});
89104

90-
// save the base class function
91-
id5Analytics.originEnableAnalytics = id5Analytics.enableAnalytics;
92-
93-
// Replace enableAnalytics
94105
const ENABLE_FUNCTION = (config) => {
95-
id5Analytics.options = typeof config === 'object' ? (config.options || {}) : {};
106+
const _this = id5Analytics;
107+
_this.options = (config && config.options) || {};
96108

97-
// We do our own sampling strategy (see AnalyticsAdapter.js)
98-
id5Analytics.options.sampling = undefined;
99-
100-
const partnerId = id5Analytics.options.partnerId;
109+
const partnerId = _this.options.partnerId;
101110
if (typeof partnerId !== 'number') {
102111
logWarn('id5Analytics: cannot find partnerId in config.options');
103112
return;
104113
}
105114

106115
ajax(`${CONFIG_URL_PREFIX}/${partnerId}/pbjs`, (result) => {
107-
const id5Config = JSON.parse(result);
108-
109-
// Store our sampling in id5Sampling as opposed to standard property sampling
110-
const sampling = id5Analytics.options.id5Sampling =
111-
typeof id5Config.sampling === 'number' ? id5Config.sampling : 0;
116+
const configFromServer = JSON.parse(result);
117+
const sampling = _this.options.id5Sampling =
118+
typeof configFromServer.sampling === 'number' ? configFromServer.sampling : 0;
112119

113-
if (typeof id5Config.ingestUrl !== 'string') {
120+
if (typeof configFromServer.ingestUrl !== 'string') {
114121
logWarn('id5Analytics: cannot find ingestUrl in config endpoint response');
115122
return;
116123
}
117-
id5Analytics.options.ingestUrl = id5Config.ingestUrl;
124+
_this.options.ingestUrl = configFromServer.ingestUrl;
125+
126+
// 3-way fallback for which events to track: server >= config >= standard
127+
_this.eventsToTrack = configFromServer.eventsToTrack || _this.options.eventsToTrack || STANDARD_EVENTS_TO_TRACK;
128+
_this.eventsToTrack = isArray(_this.eventsToTrack) ? _this.eventsToTrack : STANDARD_EVENTS_TO_TRACK;
118129

119-
logInfo('id5Analytics: Configuration is ' + JSON.stringify(id5Analytics.options));
120-
if (sampling > 0 && id5Analytics.random() < (1 / sampling)) {
130+
logInfo('id5Analytics: Configuration is ' + JSON.stringify(_this.options));
131+
if (sampling > 0 && _this.random() < (1 / sampling)) {
121132
// Init the module only if we got lucky
122133
logInfo('id5Analytics: Selected by sampling. Starting up!')
123134

124135
// Clean start
125-
id5Analytics.eventBuffer = {};
136+
_this.eventBuffer = {};
126137

127138
// Replay all events until now
128-
events.getEvents().forEach(event => {
129-
if (!!event && EVENTS_TO_TRACK.indexOf(event.eventType) >= 0) {
130-
id5Analytics.enqueue(event);
131-
}
132-
});
139+
if (!config.disablePastEventsProcessing) {
140+
events.getEvents().forEach(event => {
141+
if (!!event && _this.eventsToTrack.indexOf(event.eventType) >= 0) {
142+
_this.track(event);
143+
}
144+
});
145+
}
146+
147+
// Merge in additional cleanup rules
148+
if (configFromServer.additionalCleanupRules) {
149+
const newRules = configFromServer.additionalCleanupRules;
150+
_this.eventsToTrack.forEach((key) => {
151+
// Some protective checks in case we mess up server side
152+
if (
153+
isArray(newRules[key]) &&
154+
newRules[key].every((eventRules) =>
155+
isArray(eventRules.match) &&
156+
(eventRules.apply in TRANSFORM_FUNCTIONS))
157+
) {
158+
logInfo('id5Analytics: merging additional cleanup rules for event' + key);
159+
CLEANUP_RULES[key].push(...newRules[key]);
160+
}
161+
});
162+
}
133163

134164
// Register to the events of interest
135-
EVENTS_TO_TRACK.forEach((eventType) => {
136-
events.on(eventType, (event) => id5Analytics.enqueue(event));
165+
_this.handlers = {};
166+
_this.eventsToTrack.forEach((eventType) => {
167+
const handler = _this.handlers[eventType] = (args) =>
168+
_this.track({ eventType, args });
169+
events.on(eventType, handler);
137170
});
138171
}
139172
});
140173

141174
// Make only one init possible within a lifecycle
142-
id5Analytics.enableAnalytics = () => {};
175+
_this.enableAnalytics = () => {};
143176
};
144177

145178
id5Analytics.enableAnalytics = ENABLE_FUNCTION;
146179
id5Analytics.disableAnalytics = () => {
147-
// Make re-init possible
148-
id5Analytics.enableAnalytics = ENABLE_FUNCTION;
180+
const _this = id5Analytics;
181+
// Un-register to the events of interest
182+
_this.eventsToTrack.forEach((eventType) => {
183+
if (_this.handlers && _this.handlers[eventType]) {
184+
events.off(eventType, _this.handlers[eventType]);
185+
}
186+
});
187+
188+
// Make re-init possible. Work around the fact that past events cannot be forgotten
189+
_this.enableAnalytics = (config) => {
190+
config.disablePastEventsProcessing = true;
191+
ENABLE_FUNCTION(config);
192+
};
149193
};
150194

151195
adapterManager.registerAnalyticsAdapter({
@@ -155,3 +199,75 @@ adapterManager.registerAnalyticsAdapter({
155199
});
156200

157201
export default id5Analytics;
202+
203+
function redact(obj, key) {
204+
obj[key] = ID5_REDACTED;
205+
}
206+
207+
function erase(obj, key) {
208+
delete obj[key];
209+
}
210+
211+
// The transform function matches against a path and applies
212+
// required transformation if match is found.
213+
function deepTransformingClone(obj, transform, currentPath = []) {
214+
const result = isArray(obj) ? [] : {};
215+
const keys = Object.keys(obj);
216+
const recursable = typeof obj === 'object';
217+
if (keys.length > 0 && recursable) {
218+
keys.forEach((key) => {
219+
const newPath = currentPath.concat(key);
220+
result[key] = deepTransformingClone(obj[key], transform, newPath);
221+
transform(newPath, result, key);
222+
});
223+
return result;
224+
}
225+
return obj;
226+
}
227+
228+
// Every set of rules is an array where every entry is a pair (array) of
229+
// path to match and action to apply. The path to match is an array of
230+
// path parts, the function to apply takes (obj, prop)
231+
// Special character can be '*' (match any subproperty)
232+
const CLEANUP_RULES = {};
233+
CLEANUP_RULES[AUCTION_END] = [{
234+
match: ['adUnits', '*', 'bids', '*', 'userId', '*'],
235+
apply: 'redact'
236+
}, {
237+
match: ['adUnits', '*', 'bids', '*', 'userIdAsEids', '*', 'uids', '*', 'id'],
238+
apply: 'redact'
239+
}, {
240+
match: ['adUnits', '*', 'bidsReceived', '*', 'ad'],
241+
apply: 'erase'
242+
}];
243+
244+
const TRANSFORM_FUNCTIONS = {
245+
'redact': redact,
246+
'erase': erase,
247+
};
248+
249+
// Builds a rule function depending on the event type
250+
function transformFnFromCleanupRules(eventType) {
251+
const rules = CLEANUP_RULES[eventType] || [];
252+
return (path, obj, key) => {
253+
for (let i = 0; i < rules.length; i++) {
254+
let match = true;
255+
const ruleMatcher = rules[i].match;
256+
const transformation = rules[i].apply;
257+
if (ruleMatcher.length !== path.length) {
258+
continue;
259+
}
260+
for (let fragment = 0; fragment < ruleMatcher.length && match; fragment++) {
261+
if (path[fragment] !== ruleMatcher[fragment] && ruleMatcher[fragment] !== '*') {
262+
match = false;
263+
}
264+
}
265+
if (match) {
266+
logInfo('id5Analytics: transforming', path, transformation);
267+
const transformfn = TRANSFORM_FUNCTIONS[transformation];
268+
transformfn(obj, key);
269+
break;
270+
}
271+
}
272+
};
273+
}

0 commit comments

Comments
 (0)