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

prebid / Prebid.js / #302

11 Jun 2025 02:18AM UTC coverage: 90.359% (+0.1%) from 90.216%
#302

push

travis-ci

prebidjs-release
Prebid 9.49.1 release

43098 of 54130 branches covered (79.62%)

63790 of 70596 relevant lines covered (90.36%)

175.1 hits per line

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

92.11
/modules/id5IdSystem.js
1
/**
1✔
2
 * This module adds ID5 to the User ID module
3
 * The {@link module:modules/userId} module is required
4
 * @module modules/id5IdSystem
5
 * @requires module:modules/userId
6
 */
7

8
import {
9
  deepAccess,
10
  deepSetValue,
11
  isEmpty,
12
  isEmptyStr,
13
  isPlainObject,
14
  logError,
15
  logInfo,
16
  logWarn
17
} from '../src/utils.js';
18
import {fetch} from '../src/ajax.js';
19
import {submodule} from '../src/hook.js';
20
import {getRefererInfo} from '../src/refererDetection.js';
21
import {getStorageManager} from '../src/storageManager.js';
22
import {MODULE_TYPE_UID} from '../src/activities/modules.js';
23
import {PbPromise} from '../src/utils/promise.js';
24
import {loadExternalScript} from '../src/adloader.js';
25

26
/**
27
 * @typedef {import('../modules/userId/index.js').Submodule} Submodule
28
 * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig
29
 * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData
30
 * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse
31
 */
32

33
const MODULE_NAME = 'id5Id';
1✔
34
const GVLID = 131;
1✔
35
export const ID5_STORAGE_NAME = 'id5id';
1✔
36
const LOG_PREFIX = 'User ID - ID5 submodule: ';
1✔
37
const ID5_API_CONFIG_URL = 'https://id5-sync.com/api/config/prebid';
1✔
38
const ID5_DOMAIN = 'id5-sync.com';
1✔
39
const TRUE_LINK_SOURCE = 'true-link-id5-sync.com';
1✔
40

41
export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME});
1✔
42

43
/**
44
 * @typedef {Object} IdResponse
45
 * @property {string} [universal_uid] - The encrypted ID5 ID to pass to bidders
46
 * @property {Object} [ext] - The extensions object to pass to bidders
47
 * @property {Object} [ab_testing] - A/B testing configuration
48
 */
49

50
/**
51
 * @typedef {Object} FetchCallConfig
52
 * @property {string} [url] - The URL for the fetch endpoint
53
 * @property {Object} [overrides] - Overrides to apply to fetch parameters
54
 */
55

56
/**
57
 * @typedef {Object} ExtensionsCallConfig
58
 * @property {string} [url] - The URL for the extensions endpoint
59
 * @property {string} [method] - Overrides the HTTP method to use to make the call
60
 * @property {Object} [body] - Specifies a body to pass to the extensions endpoint
61
 */
62

63
/**
64
 * @typedef {Object} DynamicConfig
65
 * @property {FetchCallConfig} [fetchCall] - The fetch call configuration
66
 * @property {ExtensionsCallConfig} [extensionsCall] - The extensions call configuration
67
 */
68

69
/**
70
 * @typedef {Object} ABTestingConfig
71
 * @property {boolean} enabled - Tells whether A/B testing is enabled for this instance
72
 * @property {number} controlGroupPct - A/B testing probability
73
 */
74

75
/**
76
 * @typedef {Object} Multiplexing
77
 * @property {boolean} [disabled] - Disable multiplexing (instance will work in single mode)
78
 */
79

80
/**
81
 * @typedef {Object} Diagnostics
82
 * @property {boolean} [publishingDisabled] - Disable diagnostics publishing
83
 * @property {number} [publishAfterLoadInMsec] - Delay in ms after script load after which collected diagnostics are published
84
 * @property {boolean} [publishBeforeWindowUnload] - When true, diagnostics publishing is triggered on Window 'beforeunload' event
85
 * @property {number} [publishingSampleRatio] - Diagnostics publishing sample ratio
86
 */
87

88
/**
89
 * @typedef {Object} Segment
90
 * @property {string} [destination] - GVL ID or ID5-XX Partner ID. Mandatory
91
 * @property {Array<string>} [ids] - The segment IDs to push. Must contain at least one segment ID.
92
 */
93

94
/**
95
 * @typedef {Object} Id5PrebidConfig
96
 * @property {number} partner - The ID5 partner ID
97
 * @property {string} pd - The ID5 partner data string
98
 * @property {ABTestingConfig} abTesting - The A/B testing configuration
99
 * @property {boolean} disableExtensions - Disabled extensions call
100
 * @property {string} [externalModuleUrl] - URL for the id5 prebid external module
101
 * @property {Multiplexing} [multiplexing] - Multiplexing options. Only supported when loading the external module.
102
 * @property {Diagnostics} [diagnostics] - Diagnostics options. Supported only in multiplexing
103
 * @property {Array<Segment>} [segments] - A list of segments to push to partners. Supported only in multiplexing.
104
 * @property {boolean} [disableUaHints] - When true, look up of high entropy values through user agent hints is disabled.
105
 */
106

107
const DEFAULT_EIDS = {
1✔
108
  'id5id': {
109
    getValue: function (data) {
110
      return data.uid;
12✔
111
    },
112
    source: ID5_DOMAIN,
113
    atype: 1,
114
    getUidExt: function (data) {
115
      if (data.ext) {
12✔
116
        return data.ext;
10✔
117
      }
118
    }
119
  },
120
  'euid': {
121
    getValue: function (data) {
122
      return data.uid;
2✔
123
    },
124
    getSource: function (data) {
125
      return data.source;
2✔
126
    },
127
    atype: 3,
128
    getUidExt: function (data) {
129
      if (data.ext) {
2!
130
        return data.ext;
2✔
131
      }
132
    }
133
  },
134
  'trueLinkId': {
135
    getValue: function (data) {
136
      return data.uid;
2✔
137
    },
138
    getSource: function (data) {
139
      return TRUE_LINK_SOURCE;
2✔
140
    },
141
    atype: 1,
142
    getUidExt: function (data) {
143
      if (data.ext) {
2!
144
        return data.ext;
×
145
      }
146
    }
147
  }
148
};
149

150
/** @type {Submodule} */
151
export const id5IdSubmodule = {
1✔
152
  /**
153
   * used to link submodule with config
154
   * @type {string}
155
   */
156
  name: 'id5Id',
157

158
  /**
159
   * Vendor id of ID5
160
   * @type {Number}
161
   */
162
  gvlid: GVLID,
163

164
  /**
165
   * decode the stored id value for passing to bid requests
166
   * @function decode
167
   * @param {(Object|string)} value
168
   * @param {SubmoduleConfig|undefined} config
169
   * @returns {(Object|undefined)}
170
   */
171
  decode(value, config) {
172
    if (value && value.ids !== undefined) {
19✔
173
      const responseObj = {};
6✔
174
      const eids = {};
6✔
175
      Object.entries(value.ids).forEach(([key, value]) => {
6✔
176
        let eid = value.eid;
11✔
177
        let uid = eid?.uids?.[0]
11✔
178
        responseObj[key] = {
11✔
179
          uid: uid?.id,
180
          ext: uid?.ext
181
        };
182
        eids[key] = function () {
11✔
183
          return eid;
14✔
184
        }; // register function to get eid for each id (key) decoded
185
      });
186
      this.eids = eids; // overwrite global eids
6✔
187
      return responseObj;
6✔
188
    }
189

190
    let universalUid, publisherTrueLinkId;
191
    let ext = {};
13✔
192

193
    if (value && typeof value.universal_uid === 'string') {
13✔
194
      universalUid = value.universal_uid;
12✔
195
      ext = value.ext || ext;
12!
196
      publisherTrueLinkId = value.publisherTrueLinkId;
12✔
197
    } else {
198
      return undefined;
1✔
199
    }
200
    this.eids = DEFAULT_EIDS;
12✔
201
    let responseObj = {
12✔
202
      id5id: {
203
        uid: universalUid,
204
        ext: ext
205
      }
206
    };
207

208
    if (isPlainObject(ext.euid)) {
12✔
209
      responseObj.euid = {
2✔
210
        uid: ext.euid.uids[0].id,
211
        source: ext.euid.source,
212
        ext: {provider: ID5_DOMAIN}
213
      };
214
    }
215

216
    if (publisherTrueLinkId) {
12✔
217
      responseObj.trueLinkId = {
2✔
218
        uid: publisherTrueLinkId
219
      };
220
    }
221

222
    const abTestingResult = deepAccess(value, 'ab_testing.result');
12✔
223
    switch (abTestingResult) {
12✔
224
      case 'control':
225
        // A/B Testing is enabled and user is in the Control Group
226
        logInfo(LOG_PREFIX + 'A/B Testing - user is in the Control Group: ID5 ID is NOT exposed');
1✔
227
        deepSetValue(responseObj, 'id5id.ext.abTestingControlGroup', true);
1✔
228
        break;
1✔
229
      case 'error':
230
        // A/B Testing is enabled, but configured improperly, so skip A/B testing
231
        logError(LOG_PREFIX + 'A/B Testing ERROR! controlGroupPct must be a number >= 0 and <= 1');
1✔
232
        break;
1✔
233
      case 'normal':
234
        // A/B Testing is enabled but user is not in the Control Group, so ID5 ID is shared
235
        logInfo(LOG_PREFIX + 'A/B Testing - user is NOT in the Control Group');
1✔
236
        deepSetValue(responseObj, 'id5id.ext.abTestingControlGroup', false);
1✔
237
        break;
1✔
238
    }
239

240
    logInfo(LOG_PREFIX + 'Decoded ID', responseObj);
12✔
241

242
    return responseObj;
12✔
243
  },
244

245
  /**
246
   * performs action to obtain id and return a value in the callback's response argument
247
   * @function getId
248
   * @param {SubmoduleConfig} submoduleConfig
249
   * @param {ConsentData} consentData
250
   * @param {(Object|undefined)} cacheIdObj
251
   * @returns {IdResponse|undefined}
252
   */
253
  getId(submoduleConfig, consentData, cacheIdObj) {
254
    if (!validateConfig(submoduleConfig)) {
57✔
255
      return undefined;
27✔
256
    }
257

258
    if (!hasWriteConsentToLocalStorage(consentData?.gdpr)) {
30!
259
      logInfo(LOG_PREFIX + 'Skipping ID5 local storage write because no consent given.');
×
260
      return undefined;
×
261
    }
262

263
    const resp = function (cbFunction) {
30✔
264
      const fetchFlow = new IdFetchFlow(submoduleConfig, consentData?.gdpr, cacheIdObj, consentData?.usp, consentData?.gpp);
27✔
265
      fetchFlow.execute()
27✔
266
        .then(response => {
267
          cbFunction(response);
27✔
268
        })
269
        .catch(error => {
270
          logError(LOG_PREFIX + 'getId fetch encountered an error', error);
×
271
          cbFunction();
×
272
        });
273
    };
274
    return {callback: resp};
30✔
275
  },
276

277
  /**
278
   * Similar to Submodule#getId, this optional method returns response to for id that exists already.
279
   *  If IdResponse#id is defined, then it will be written to the current active storage even if it exists already.
280
   *  If IdResponse#callback is defined, then it'll called at the end of auction.
281
   *  It's permissible to return neither, one, or both fields.
282
   * @function extendId
283
   * @param {SubmoduleConfig} config
284
   * @param {ConsentData|undefined} consentData
285
   * @param {Object} cacheIdObj - existing id, if any
286
   * @return {IdResponse} A response object that contains id.
287
   */
288
  extendId(config, consentData, cacheIdObj) {
289
    if (!hasWriteConsentToLocalStorage(consentData?.gdpr)) {
25✔
290
      logInfo(LOG_PREFIX + 'No consent given for ID5 local storage writing, skipping nb increment.');
9✔
291
      return cacheIdObj;
9✔
292
    }
293

294
    logInfo(LOG_PREFIX + 'using cached ID', cacheIdObj);
16✔
295
    if (cacheIdObj) {
16✔
296
      cacheIdObj.nbPage = incrementNb(cacheIdObj);
7✔
297
    }
298
    return cacheIdObj;
16✔
299
  },
300
  primaryIds: ['id5id', 'trueLinkId'],
301
  eids: DEFAULT_EIDS,
302
  _reset() {
303
    this.eids = DEFAULT_EIDS;
1✔
304
  }
305
};
306

307
export class IdFetchFlow {
308
  constructor(submoduleConfig, gdprConsentData, cacheIdObj, usPrivacyData, gppData) {
309
    this.submoduleConfig = submoduleConfig;
27✔
310
    this.gdprConsentData = gdprConsentData;
27✔
311
    this.cacheIdObj = cacheIdObj;
27✔
312
    this.usPrivacyData = usPrivacyData;
27✔
313
    this.gppData = gppData;
27✔
314
  }
315

316
  /**
317
   * Calls the ID5 Servers to fetch an ID5 ID
318
   * @returns {Promise<IdResponse>} The result of calling the server side
319
   */
320
  async execute() {
321
    const configCallPromise = this.#callForConfig();
27✔
322
    if (this.#isExternalModule()) {
27✔
323
      try {
2✔
324
        return await this.#externalModuleFlow(configCallPromise);
2✔
325
      } catch (error) {
326
        logError(LOG_PREFIX + 'Error while performing ID5 external module flow. Continuing with regular flow.', error);
1✔
327
        return this.#regularFlow(configCallPromise);
1✔
328
      }
329
    } else {
330
      return this.#regularFlow(configCallPromise);
25✔
331
    }
332
  }
333

334
  #isExternalModule() {
335
    return typeof this.submoduleConfig.params.externalModuleUrl === 'string';
27✔
336
  }
337

338
  async #externalModuleFlow(configCallPromise) {
339
    await loadExternalModule(this.submoduleConfig.params.externalModuleUrl);
2✔
340
    const fetchFlowConfig = await configCallPromise;
2✔
341

342
    return this.#getExternalIntegration().fetchId5Id(fetchFlowConfig, this.submoduleConfig.params, getRefererInfo(), this.gdprConsentData, this.usPrivacyData, this.gppData);
2✔
343
  }
344

345
  #getExternalIntegration() {
346
    return window.id5Prebid && window.id5Prebid.integration;
2✔
347
  }
348

349
  async #regularFlow(configCallPromise) {
350
    const fetchFlowConfig = await configCallPromise;
26✔
351
    const extensionsData = await this.#callForExtensions(fetchFlowConfig.extensionsCall);
26✔
352
    const fetchCallResponse = await this.#callId5Fetch(fetchFlowConfig.fetchCall, extensionsData);
26✔
353
    return this.#processFetchCallResponse(fetchCallResponse);
26✔
354
  }
355

356
  async #callForConfig() {
357
    let url = this.submoduleConfig.params.configUrl || ID5_API_CONFIG_URL; // override for debug/test purposes only
27✔
358
    const response = await fetch(url, {
27✔
359
      method: 'POST',
360
      body: JSON.stringify({
361
        ...this.submoduleConfig,
362
        bounce: true
363
      }),
364
      credentials: 'include'
365
    });
366
    if (!response.ok) {
27!
367
      throw new Error('Error while calling config endpoint: ', response);
×
368
    }
369
    const dynamicConfig = await response.json();
27✔
370
    logInfo(LOG_PREFIX + 'config response received from the server', dynamicConfig);
27✔
371
    return dynamicConfig;
27✔
372
  }
373

374
  async #callForExtensions(extensionsCallConfig) {
375
    if (extensionsCallConfig === undefined) {
26✔
376
      return undefined;
24✔
377
    }
378
    const extensionsUrl = extensionsCallConfig.url;
2✔
379
    const method = extensionsCallConfig.method || 'GET';
2!
380
    const body = method === 'GET' ? undefined : JSON.stringify(extensionsCallConfig.body || {});
2!
381
    const response = await fetch(extensionsUrl, {method, body});
2✔
382
    if (!response.ok) {
2!
383
      throw new Error('Error while calling extensions endpoint: ', response);
×
384
    }
385
    const extensions = await response.json();
2✔
386
    logInfo(LOG_PREFIX + 'extensions response received from the server', extensions);
2✔
387
    return extensions;
2✔
388
  }
389

390
  async #callId5Fetch(fetchCallConfig, extensionsData) {
391
    const fetchUrl = fetchCallConfig.url;
26✔
392
    const additionalData = fetchCallConfig.overrides || {};
26✔
393
    const body = JSON.stringify({
26✔
394
      ...this.#createFetchRequestData(),
395
      ...additionalData,
396
      extensions: extensionsData
397
    });
398
    const response = await fetch(fetchUrl, {method: 'POST', body, credentials: 'include'});
26✔
399
    if (!response.ok) {
26!
400
      throw new Error('Error while calling fetch endpoint: ', response);
×
401
    }
402
    const fetchResponse = await response.json();
26✔
403
    logInfo(LOG_PREFIX + 'fetch response received from the server', fetchResponse);
26✔
404
    return fetchResponse;
26✔
405
  }
406

407
  #createFetchRequestData() {
408
    const params = this.submoduleConfig.params;
26✔
409
    const hasGdpr = (this.gdprConsentData && typeof this.gdprConsentData.gdprApplies === 'boolean' && this.gdprConsentData.gdprApplies) ? 1 : 0;
26✔
410
    const referer = getRefererInfo();
26✔
411
    const signature = this.cacheIdObj ? this.cacheIdObj.signature : undefined;
26✔
412
    const nbPage = incrementNb(this.cacheIdObj);
26✔
413
    const trueLinkInfo = window.id5Bootstrap ? window.id5Bootstrap.getTrueLinkInfo() : {booted: false};
26✔
414

415
    const data = {
26✔
416
      'partner': params.partner,
417
      'gdpr': hasGdpr,
418
      'nbPage': nbPage,
419
      'o': 'pbjs',
420
      'tml': referer.topmostLocation,
421
      'ref': referer.ref,
422
      'cu': referer.canonicalUrl,
423
      'top': referer.reachedTop ? 1 : 0,
26!
424
      'u': referer.stack[0] || window.location.href,
26!
425
      'v': '$prebid.version$',
426
      'storage': this.submoduleConfig.storage,
427
      'localStorage': storage.localStorageIsEnabled() ? 1 : 0,
26✔
428
      'true_link': trueLinkInfo
429
    };
430

431
    // pass in optional data, but only if populated
432
    if (hasGdpr && this.gdprConsentData.consentString !== undefined && !isEmpty(this.gdprConsentData.consentString) && !isEmptyStr(this.gdprConsentData.consentString)) {
26✔
433
      data.gdpr_consent = this.gdprConsentData.consentString;
2✔
434
    }
435
    if (this.usPrivacyData !== undefined && !isEmpty(this.usPrivacyData) && !isEmptyStr(this.usPrivacyData)) {
26✔
436
      data.us_privacy = this.usPrivacyData;
1✔
437
    }
438
    if (this.gppData) {
26✔
439
      data.gpp_string = this.gppData.gppString;
1✔
440
      data.gpp_sid = this.gppData.applicableSections;
1✔
441
    }
442

443
    if (signature !== undefined && !isEmptyStr(signature)) {
26✔
444
      data.s = signature;
13✔
445
    }
446
    if (params.pd !== undefined && !isEmptyStr(params.pd)) {
26✔
447
      data.pd = params.pd;
1✔
448
    }
449
    if (params.provider !== undefined && !isEmptyStr(params.provider)) {
26!
450
      data.provider = params.provider;
×
451
    }
452
    const abTestingConfig = params.abTesting || {enabled: false};
26✔
453

454
    if (abTestingConfig.enabled) {
26✔
455
      data.ab_testing = {
1✔
456
        enabled: true, control_group_pct: abTestingConfig.controlGroupPct // The server validates
457
      };
458
    }
459
    return data;
26✔
460
  }
461

462
  #processFetchCallResponse(fetchCallResponse) {
463
    try {
26✔
464
      if (fetchCallResponse.privacy) {
26!
465
        if (window.id5Bootstrap && window.id5Bootstrap.setPrivacy) {
×
466
          window.id5Bootstrap.setPrivacy(fetchCallResponse.privacy);
×
467
        }
468
      }
469
    } catch (error) {
470
      logError(LOG_PREFIX + 'Error while writing privacy info into local storage.', error);
×
471
    }
472
    return fetchCallResponse;
26✔
473
  }
474
}
475

476
async function loadExternalModule(url) {
477
  return new PbPromise((resolve, reject) => {
2✔
478
    if (window.id5Prebid) {
2✔
479
      // Already loaded
480
      resolve();
1✔
481
    } else {
482
      try {
1✔
483
        loadExternalScript(url, MODULE_TYPE_UID, 'id5', resolve);
1✔
484
      } catch (error) {
485
        reject(error);
×
486
      }
487
    }
488
  });
489
}
490

491
function validateConfig(config) {
492
  if (!config || !config.params || !config.params.partner) {
57✔
493
    logError(LOG_PREFIX + 'partner required to be defined');
22✔
494
    return false;
22✔
495
  }
496

497
  const partner = config.params.partner;
35✔
498
  if (typeof partner === 'string' || partner instanceof String) {
35✔
499
    let parsedPartnerId = parseInt(partner);
2✔
500
    if (isNaN(parsedPartnerId) || parsedPartnerId < 0) {
2✔
501
      logError(LOG_PREFIX + 'partner required to be a number or a String parsable to a positive integer');
1✔
502
      return false;
1✔
503
    } else {
504
      config.params.partner = parsedPartnerId;
1✔
505
    }
506
  } else if (typeof partner !== 'number') {
33!
507
    logError(LOG_PREFIX + 'partner required to be a number or a String parsable to a positive integer');
×
508
    return false;
×
509
  }
510

511
  if (!config.storage || !config.storage.type || !config.storage.name) {
34✔
512
    logError(LOG_PREFIX + 'storage required to be set');
4✔
513
    return false;
4✔
514
  }
515
  if (config.storage.name !== ID5_STORAGE_NAME) {
30✔
516
    logWarn(LOG_PREFIX + `storage name recommended to be '${ID5_STORAGE_NAME}'.`);
1✔
517
  }
518

519
  return true;
30✔
520
}
521

522
function incrementNb(cachedObj) {
523
  if (cachedObj && cachedObj.nbPage !== undefined) {
33✔
524
    return cachedObj.nbPage + 1;
6✔
525
  } else {
526
    return 1;
27✔
527
  }
528
}
529

530
/**
531
 * Check to see if we can write to local storage based on purpose consent 1, and that we have vendor consent (ID5=131)
532
 * @param {ConsentData} consentData
533
 * @returns {boolean}
534
 */
535
function hasWriteConsentToLocalStorage(consentData) {
536
  const hasGdpr = consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies;
55✔
537
  const localstorageConsent = deepAccess(consentData, `vendorData.purpose.consents.1`);
55✔
538
  const id5VendorConsent = deepAccess(consentData, `vendorData.vendor.consents.${GVLID.toString()}`);
55✔
539
  if (hasGdpr && (!localstorageConsent || !id5VendorConsent)) {
55✔
540
    return false;
9✔
541
  }
542
  return true;
46✔
543
}
544

545
submodule('userId', id5IdSubmodule);
1✔
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