• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

prebid / Prebid.js / 19836249188

01 Dec 2025 08:19PM UTC coverage: 96.234% (+0.004%) from 96.23%
19836249188

push

github

web-flow
clickioBidAdapter: add IAB GVL ID and TCFEU support (#14224)

53490 of 65500 branches covered (81.66%)

1 of 1 new or added line in 1 file covered. (100.0%)

87 existing lines in 10 files now uncovered.

204130 of 212118 relevant lines covered (96.23%)

72.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.3
/modules/id5AnalyticsAdapter.js
1
import buildAdapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js';
1✔
2
import {EVENTS} from '../src/constants.js';
3
import adapterManager from '../src/adapterManager.js';
4
import {ajax} from '../src/ajax.js';
5
import {compressDataWithGZip, isGzipCompressionSupported, logError, logInfo} from '../src/utils.js';
6
import * as events from '../src/events.js';
7

8
const {
9
  AUCTION_END,
10
  TCF2_ENFORCEMENT,
11
  BID_WON
12
} = EVENTS
1✔
13

14
const GVLID = 131;
1✔
15
const COMPRESSION_THRESHOLD = 2048;
1✔
16

17
const STANDARD_EVENTS_TO_TRACK = [
1✔
18
  AUCTION_END,
19
  TCF2_ENFORCEMENT,
20
  BID_WON,
21
];
22

23
const CONFIG_URL_PREFIX = 'https://api.id5-sync.com/analytics'
1✔
24
const TZ = new Date().getTimezoneOffset();
1✔
25
const PBJS_VERSION = 'v' + '$prebid.version$';
1✔
26
const ID5_REDACTED = '__ID5_REDACTED__';
1✔
27
const isArray = Array.isArray;
1✔
28

29
const id5Analytics = Object.assign(buildAdapter({analyticsType: 'endpoint'}), {
1✔
30
  eventsToTrack: STANDARD_EVENTS_TO_TRACK,
31

32
  track: (event) => {
33
    const _this = id5Analytics;
14✔
34

35
    if (!event || !event.args) {
14!
UNCOV
36
      return;
×
37
    }
38

39
    try {
14✔
40
      _this.sendEvent(_this.makeEvent(event.eventType, event.args));
14✔
41
    } catch (error) {
42
      logError('id5Analytics: ERROR', error);
×
UNCOV
43
      _this.sendErrorEvent(error);
×
44
    }
45
  },
46

47
  sendEvent: (eventToSend) => {
48
    const serializedEvent = JSON.stringify(eventToSend);
14✔
49
    if (!id5Analytics.options.compressionDisabled && isGzipCompressionSupported() && serializedEvent.length > COMPRESSION_THRESHOLD) {
14✔
50
      compressDataWithGZip(serializedEvent).then(compressedData => {
1✔
51
        ajax(id5Analytics.options.ingestUrl, null, compressedData, {
1✔
52
          contentType: 'application/json',
53
          customHeaders: {
54
            'Content-Encoding': 'gzip'
55
          }
56
        });
57
      })
58
    } else {
59
      // By giving some content this will be automatically a POST
60
      ajax(id5Analytics.options.ingestUrl, null, serializedEvent);
13✔
61
    }
62
  },
63

64
  makeEvent: (event, payload) => {
65
    const _this = id5Analytics;
14✔
66
    const filteredPayload = deepTransformingClone(payload,
14✔
67
      transformFnFromCleanupRules(event));
68
    return {
14✔
69
      source: 'pbjs',
70
      event,
71
      payload: filteredPayload,
72
      partnerId: _this.options.partnerId,
73
      meta: {
74
        sampling: _this.options.id5Sampling,
75
        pbjs: PBJS_VERSION,
76
        tz: TZ,
77
      }
78
    };
79
  },
80

81
  sendErrorEvent: (error) => {
UNCOV
82
    const _this = id5Analytics;
×
UNCOV
83
    _this.sendEvent([
×
84
      _this.makeEvent('analyticsError', {
85
        message: error.message,
86
        stack: error.stack,
87
      })
88
    ]);
89
  },
90

91
  random: () => Math.random(),
7✔
92
});
93

94
const ENABLE_FUNCTION = (config) => {
1✔
95
  const _this = id5Analytics;
11✔
96
  _this.options = (config && config.options) || {};
11✔
97

98
  const partnerId = _this.options.partnerId;
11✔
99
  if (typeof partnerId !== 'number') {
11✔
100
    logError('id5Analytics: partnerId in config.options must be a number representing the id5 partner ID');
3✔
101
    return;
3✔
102
  }
103

104
  ajax(`${CONFIG_URL_PREFIX}/${partnerId}/pbjs`, (result) => {
8✔
105
    logInfo('id5Analytics: Received from configuration endpoint', result);
8✔
106

107
    const configFromServer = JSON.parse(result);
8✔
108

109
    const sampling = _this.options.id5Sampling =
8✔
110
      typeof configFromServer.sampling === 'number' ? configFromServer.sampling : 0;
8!
111

112
    if (typeof configFromServer.ingestUrl !== 'string') {
8!
UNCOV
113
      logError('id5Analytics: cannot find ingestUrl in config endpoint response; no analytics will be available');
×
UNCOV
114
      return;
×
115
    }
116
    _this.options.ingestUrl = configFromServer.ingestUrl;
8✔
117

118
    // 3-way fallback for which events to track: server > config > standard
119
    _this.eventsToTrack = configFromServer.eventsToTrack || _this.options.eventsToTrack || STANDARD_EVENTS_TO_TRACK;
8✔
120
    _this.eventsToTrack = isArray(_this.eventsToTrack) ? _this.eventsToTrack : STANDARD_EVENTS_TO_TRACK;
8!
121

122
    logInfo('id5Analytics: Configuration is', _this.options);
8✔
123
    logInfo('id5Analytics: Tracking events', _this.eventsToTrack);
8✔
124
    if (sampling > 0 && _this.random() < (1 / sampling)) {
8✔
125
      // Init the module only if we got lucky
126
      logInfo('id5Analytics: Selected by sampling. Starting up!');
7✔
127

128
      // allow for replacing cleanup rules - remove existing ones and apply from server
129
      if (configFromServer.replaceCleanupRules) {
7✔
130
        cleanupRules = {};
1✔
131
      }
132
      // Merge in additional cleanup rules
133
      if (configFromServer.additionalCleanupRules) {
7✔
134
        const newRules = configFromServer.additionalCleanupRules;
2✔
135
        _this.eventsToTrack.forEach((key) => {
2✔
136
          // Some protective checks in case we mess up server side
137
          if (
6✔
138
            isArray(newRules[key]) &&
8✔
139
            newRules[key].every((eventRules) =>
140
              isArray(eventRules.match) &&
2✔
141
              (eventRules.apply in TRANSFORM_FUNCTIONS))
142
          ) {
143
            logInfo('id5Analytics: merging additional cleanup rules for event ' + key);
2✔
144
            if (!Array.isArray(cleanupRules[key])) {
2✔
145
              cleanupRules[key] = newRules[key];
1✔
146
            } else {
147
              cleanupRules[key].push(...newRules[key]);
1✔
148
            }
149
          }
150
        });
151
      }
152

153
      // Replay all events until now
154
      if (!config.disablePastEventsProcessing) {
7!
UNCOV
155
        events.getEvents().forEach((event) => {
×
UNCOV
156
          if (event && _this.eventsToTrack.indexOf(event.eventType) >= 0) {
×
UNCOV
157
            _this.track(event);
×
158
          }
159
        });
160
      }
161

162
      // Register to the events of interest
163
      _this.handlers = {};
7✔
164
      _this.eventsToTrack.forEach((eventType) => {
7✔
165
        const handler = _this.handlers[eventType] = (args) =>
19✔
166
          _this.track({ eventType, args });
14✔
167
        events.on(eventType, handler);
19✔
168
      });
169
    }
170
  });
171

172
  // Make only one init possible within a lifecycle
173
  _this.enableAnalytics = () => {};
8✔
174
};
175

176
id5Analytics.enableAnalytics = ENABLE_FUNCTION;
1✔
177
id5Analytics.disableAnalytics = () => {
1✔
178
  const _this = id5Analytics;
9✔
179
  // Un-register to the events of interest
180
  _this.eventsToTrack.forEach((eventType) => {
9✔
181
    if (_this.handlers && _this.handlers[eventType]) {
25✔
182
      events.off(eventType, _this.handlers[eventType]);
19✔
183
    }
184
  });
185

186
  // Make re-init possible. Work around the fact that past events cannot be forgotten
187
  _this.enableAnalytics = (config) => {
9✔
188
    config.disablePastEventsProcessing = true;
8✔
189
    ENABLE_FUNCTION(config);
8✔
190
  };
191
};
192

193
adapterManager.registerAnalyticsAdapter({
1✔
194
  adapter: id5Analytics,
195
  code: 'id5Analytics',
196
  gvlid: GVLID
197
});
198

199
export default id5Analytics;
200

201
function redact(obj, key) {
202
  obj[key] = ID5_REDACTED;
15✔
203
}
204

205
function erase(obj, key) {
206
  delete obj[key];
7✔
207
}
208

209
// The transform function matches against a path and applies
210
// required transformation if match is found.
211
function deepTransformingClone(obj, transform, currentPath = []) {
394✔
212
  const result = isArray(obj) ? [] : {};
394✔
213
  const recursable = typeof obj === 'object' && obj !== null;
394✔
214
  if (recursable) {
394✔
215
    const keys = Object.keys(obj);
185✔
216
    if (keys.length > 0) {
185✔
217
      keys.forEach((key) => {
143✔
218
        const newPath = currentPath.concat(key);
380✔
219
        result[key] = deepTransformingClone(obj[key], transform, newPath);
380✔
220
        transform(newPath, result, key);
380✔
221
      });
222
      return result;
143✔
223
    }
224
  }
225
  return obj;
251✔
226
}
227

228
// Every set of rules is an object where "match" is an array and
229
// "apply" is the function to apply in case of match. The function to apply
230
// takes (obj, prop) and transforms property "prop" in object "obj".
231
// The "match" is an array of path parts. Each part is either a string or an array.
232
// In case of array, it represents alternatives which all would match.
233
// Special path part '*' matches any subproperty or array index.
234
// Prefixing a part with "!" makes it negative match (doesn't work with multiple alternatives)
235
let cleanupRules = {};
1✔
236
cleanupRules[AUCTION_END] = [{
1✔
237
  match: [['adUnits', 'bidderRequests'], '*', 'bids', '*', ['userId', 'crumbs'], '!id5id'],
238
  apply: 'redact'
239
}, {
240
  match: [['adUnits', 'bidderRequests'], '*', 'bids', '*', ['userId', 'crumbs'], 'id5id', 'uid'],
241
  apply: 'redact'
242
}, {
243
  match: [['adUnits', 'bidderRequests'], '*', 'bids', '*', 'userIdAsEids', '*', 'uids', '*', ['id', 'ext']],
244
  apply: 'redact'
245
}, {
246
  match: ['bidderRequests', '*', 'gdprConsent', 'vendorData'],
247
  apply: 'erase'
248
}, {
249
  match: ['bidsReceived', '*', ['ad', 'native']],
250
  apply: 'erase'
251
}, {
252
  match: ['noBids', '*', ['userId', 'crumbs'], '*'],
253
  apply: 'redact'
254
}, {
255
  match: ['noBids', '*', 'userIdAsEids', '*', 'uids', '*', ['id', 'ext']],
256
  apply: 'redact'
257
}];
258

259
cleanupRules[BID_WON] = [{
1✔
260
  match: [['ad', 'native']],
261
  apply: 'erase'
262
}];
263

264
const TRANSFORM_FUNCTIONS = {
1✔
265
  'redact': redact,
266
  'erase': erase,
267
};
268

269
// Builds a rule function depending on the event type
270
function transformFnFromCleanupRules(eventType) {
271
  const rules = cleanupRules[eventType] || [];
14✔
272
  return (path, obj, key) => {
14✔
273
    for (let i = 0; i < rules.length; i++) {
380✔
274
      let match = true;
2,073✔
275
      const ruleMatcher = rules[i].match;
2,073✔
276
      const transformation = rules[i].apply;
2,073✔
277
      if (ruleMatcher.length !== path.length) {
2,073✔
278
        continue;
1,719✔
279
      }
280
      for (let fragment = 0; fragment < ruleMatcher.length && match; fragment++) {
354✔
281
        const choices = makeSureArray(ruleMatcher[fragment]);
975✔
282
        match = !choices.every((choice) => choice !== '*' &&
1,107✔
283
          (choice.charAt(0) === '!'
807✔
284
            ? path[fragment] === choice.substring(1)
285
            : path[fragment] !== choice));
286
      }
287
      if (match) {
354✔
288
        const transformfn = TRANSFORM_FUNCTIONS[transformation];
22✔
289
        transformfn(obj, key);
22✔
290
        break;
22✔
291
      }
292
    }
293
  };
294
}
295

296
function makeSureArray(object) {
297
  return isArray(object) ? object : [object];
975✔
298
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc