@@ -5,29 +5,41 @@ import { ajax } from '../src/ajax.js';
5
5
import { logWarn , logInfo , logError } from '../src/utils.js' ;
6
6
import events from '../src/events.js' ;
7
7
8
+ const {
9
+ EVENTS : {
10
+ AUCTION_END ,
11
+ AUCTION_INIT ,
12
+ TCF2_ENFORCEMENT
13
+ }
14
+ } = CONSTANTS
15
+
8
16
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 ,
13
21
] ;
14
22
const FLUSH_EVENTS = [
15
- CONSTANTS . EVENTS . TCF2_ENFORCEMENT ,
16
- CONSTANTS . EVENTS . AUCTION_END ,
23
+ TCF2_ENFORCEMENT ,
24
+ AUCTION_END ,
17
25
] ;
18
26
// const CONFIG_URL_PREFIX = 'https://api.id5-sync.com/analytics'
19
27
const CONFIG_URL_PREFIX = 'https://127.0.0.1:8443/analytics'
20
28
const TZ = new Date ( ) . getTimezoneOffset ( ) ;
21
29
const PBJS_VERSION = $$PREBID_GLOBAL$$ . version ;
30
+ const ID5_REDACTED = '__ID5_REDACTED__' ;
31
+ const isArray = Array . isArray ;
22
32
23
33
let id5Analytics = Object . assign ( buildAdapter ( { analyticsType : 'endpoint' } ) , {
24
34
// Keeps an array of events for each auction
25
35
eventBuffer : { } ,
26
36
37
+ eventsToTrack : STANDARD_EVENTS_TO_TRACK ,
38
+
27
39
track : ( event ) => {
28
40
const _this = id5Analytics ;
29
41
30
- if ( ! event ) {
42
+ if ( ! event || ! event . args ) {
31
43
return ;
32
44
}
33
45
@@ -37,7 +49,7 @@ let id5Analytics = Object.assign(buildAdapter({analyticsType: 'endpoint'}), {
37
49
38
50
// Collect events and send them in a batch when the auction ends
39
51
const que = _this . eventBuffer [ auctionId ] ;
40
- que . push ( _this . makeEvent ( 'pbjs_' + event . eventType , event . args ) ) ;
52
+ que . push ( _this . makeEvent ( event . eventType , event . args ) ) ;
41
53
42
54
if ( FLUSH_EVENTS . indexOf ( event . eventType ) >= 0 ) {
43
55
// Auction ended. Send the batch of collected events
@@ -61,9 +73,12 @@ let id5Analytics = Object.assign(buildAdapter({analyticsType: 'endpoint'}), {
61
73
62
74
makeEvent : ( event , payload ) => {
63
75
const _this = id5Analytics ;
76
+ const filteredPayload = deepTransformingClone ( payload ,
77
+ transformFnFromCleanupRules ( event ) ) ;
64
78
return {
79
+ source : 'pbjs' ,
65
80
event,
66
- payload,
81
+ payload : filteredPayload ,
67
82
partnerId : _this . options . partnerId ,
68
83
meta : {
69
84
sampling : _this . options . id5Sampling ,
@@ -76,7 +91,7 @@ let id5Analytics = Object.assign(buildAdapter({analyticsType: 'endpoint'}), {
76
91
sendErrorEvent : ( error ) => {
77
92
const _this = id5Analytics ;
78
93
_this . sendEvents ( [
79
- _this . makeEvent ( 'pbjs_analyticsError ' , {
94
+ _this . makeEvent ( 'analyticsError ' , {
80
95
message : error . message ,
81
96
stack : error . stack ,
82
97
} )
@@ -87,65 +102,94 @@ let id5Analytics = Object.assign(buildAdapter({analyticsType: 'endpoint'}), {
87
102
random : ( ) => Math . random ( ) ,
88
103
} ) ;
89
104
90
- // save the base class function
91
- id5Analytics . originEnableAnalytics = id5Analytics . enableAnalytics ;
92
-
93
- // Replace enableAnalytics
94
105
const ENABLE_FUNCTION = ( config ) => {
95
- id5Analytics . options = typeof config === 'object' ? ( config . options || { } ) : { } ;
106
+ const _this = id5Analytics ;
107
+ _this . options = ( config && config . options ) || { } ;
96
108
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 ;
101
110
if ( typeof partnerId !== 'number' ) {
102
111
logWarn ( 'id5Analytics: cannot find partnerId in config.options' ) ;
103
112
return ;
104
113
}
105
114
106
115
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 ;
112
119
113
- if ( typeof id5Config . ingestUrl !== 'string' ) {
120
+ if ( typeof configFromServer . ingestUrl !== 'string' ) {
114
121
logWarn ( 'id5Analytics: cannot find ingestUrl in config endpoint response' ) ;
115
122
return ;
116
123
}
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 ;
118
129
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 ) ) {
121
132
// Init the module only if we got lucky
122
133
logInfo ( 'id5Analytics: Selected by sampling. Starting up!' )
123
134
124
135
// Clean start
125
- id5Analytics . eventBuffer = { } ;
136
+ _this . eventBuffer = { } ;
126
137
127
138
// 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
+ }
133
163
134
164
// 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 ) ;
137
170
} ) ;
138
171
}
139
172
} ) ;
140
173
141
174
// Make only one init possible within a lifecycle
142
- id5Analytics . enableAnalytics = ( ) => { } ;
175
+ _this . enableAnalytics = ( ) => { } ;
143
176
} ;
144
177
145
178
id5Analytics . enableAnalytics = ENABLE_FUNCTION ;
146
179
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
+ } ;
149
193
} ;
150
194
151
195
adapterManager . registerAnalyticsAdapter ( {
@@ -155,3 +199,75 @@ adapterManager.registerAnalyticsAdapter({
155
199
} ) ;
156
200
157
201
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