forked from prebid/Prebid.js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgenericAnalyticsAdapter.js
152 lines (141 loc) · 4.4 KB
/
genericAnalyticsAdapter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import AnalyticsAdapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js';
import {prefixLog, isPlainObject} from '../src/utils.js';
import {has as hasEvent} from '../src/events.js';
import adapterManager from '../src/adapterManager.js';
import {ajaxBuilder} from '../src/ajax.js';
const DEFAULTS = {
batchSize: 1,
batchDelay: 100,
method: 'POST'
}
const TYPES = {
handler: 'function',
batchSize: 'number',
batchDelay: 'number',
gvlid: 'number',
}
const MAX_CALL_DEPTH = 20;
export function GenericAnalytics() {
const parent = AnalyticsAdapter({analyticsType: 'endpoint'});
const {logError, logWarn} = prefixLog('Generic analytics:');
let batch = [];
let callDepth = 0;
let options, handler, timer, translate;
function optionsAreValid(options) {
if (!options.url && !options.handler) {
logError('options must specify either `url` or `handler`')
return false;
}
if (options.hasOwnProperty('method') && !['GET', 'POST'].includes(options.method)) {
logError('options.method must be GET or POST');
return false;
}
for (const [field, type] of Object.entries(TYPES)) {
// eslint-disable-next-line valid-typeof
if (options.hasOwnProperty(field) && typeof options[field] !== type) {
logError(`options.${field} must be a ${type}`);
return false;
}
}
if (options.hasOwnProperty('events')) {
if (!isPlainObject(options.events)) {
logError('options.events must be an object');
return false;
}
for (const [event, handler] of Object.entries(options.events)) {
if (!hasEvent(event)) {
logWarn(`options.events.${event} does not match any known Prebid event`);
}
if (typeof handler !== 'function') {
logError(`options.events.${event} must be a function`);
return false;
}
}
}
return true;
}
function processBatch() {
const currentBatch = batch;
batch = [];
callDepth++;
try {
// the pub-provided handler may inadvertently cause an infinite chain of events;
// even just logging an exception from it may cause an AUCTION_DEBUG event, that
// gets back to the handler, that throws another exception etc.
// to avoid the issue, put a cap on recursion
if (callDepth === MAX_CALL_DEPTH) {
logError('detected probable infinite recursion, discarding events', currentBatch);
}
if (callDepth >= MAX_CALL_DEPTH) {
return;
}
try {
handler(currentBatch);
} catch (e) {
logError('error executing options.handler', e);
}
} finally {
callDepth--;
}
}
function translator(eventHandlers) {
if (!eventHandlers) {
return (data) => data;
}
return function ({eventType, args}) {
if (eventHandlers.hasOwnProperty(eventType)) {
try {
return eventHandlers[eventType](args);
} catch (e) {
logError(`error executing options.events.${eventType}`, e);
}
}
}
}
return Object.assign(
Object.create(parent),
{
gvlid(config) {
return config?.options?.gvlid
},
enableAnalytics(config) {
if (optionsAreValid(config?.options || {})) {
options = Object.assign({}, DEFAULTS, config.options);
handler = options.handler || defaultHandler(options);
translate = translator(options.events);
parent.enableAnalytics.call(this, config);
}
},
track(event) {
const datum = translate(event);
if (datum != null) {
batch.push(datum);
if (timer != null) {
clearTimeout(timer);
timer = null;
}
if (batch.length >= options.batchSize) {
processBatch();
} else {
timer = setTimeout(processBatch, options.batchDelay);
}
}
}
}
)
}
export function defaultHandler({url, method, batchSize, ajax = ajaxBuilder()}) {
const callbacks = {
success() {},
error() {}
}
const extract = batchSize > 1 ? (events) => events : (events) => events[0];
const serialize = method === 'GET' ? (data) => ({data: JSON.stringify(data)}) : (data) => JSON.stringify(data);
return function (events) {
ajax(url, callbacks, serialize(extract(events)), {method, keepalive: true})
}
}
adapterManager.registerAnalyticsAdapter({
adapter: GenericAnalytics(),
code: 'generic',
});