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

prebid / Prebid.js / #283

20 Feb 2025 06:08PM UTC coverage: 90.519% (-0.005%) from 90.524%
#283

push

travis-ci

prebidjs-release
Prebid 9.31.0 release

41886 of 52458 branches covered (79.85%)

62085 of 68588 relevant lines covered (90.52%)

203.0 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 {GreedyPromise} 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
  // eslint-disable-next-line no-dupe-class-members
339
  async #externalModuleFlow(configCallPromise) {
340
    await loadExternalModule(this.submoduleConfig.params.externalModuleUrl);
2✔
341
    const fetchFlowConfig = await configCallPromise;
2✔
342

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

346
  // eslint-disable-next-line no-dupe-class-members
347
  #getExternalIntegration() {
348
    return window.id5Prebid && window.id5Prebid.integration;
2✔
349
  }
350

351
  // eslint-disable-next-line no-dupe-class-members
352
  async #regularFlow(configCallPromise) {
353
    const fetchFlowConfig = await configCallPromise;
26✔
354
    const extensionsData = await this.#callForExtensions(fetchFlowConfig.extensionsCall);
26✔
355
    const fetchCallResponse = await this.#callId5Fetch(fetchFlowConfig.fetchCall, extensionsData);
26✔
356
    return this.#processFetchCallResponse(fetchCallResponse);
26✔
357
  }
358

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

378
  // eslint-disable-next-line no-dupe-class-members
379
  async #callForExtensions(extensionsCallConfig) {
380
    if (extensionsCallConfig === undefined) {
26✔
381
      return undefined;
24✔
382
    }
383
    const extensionsUrl = extensionsCallConfig.url;
2✔
384
    const method = extensionsCallConfig.method || 'GET';
2!
385
    const body = method === 'GET' ? undefined : JSON.stringify(extensionsCallConfig.body || {});
2!
386
    const response = await fetch(extensionsUrl, {method, body});
2✔
387
    if (!response.ok) {
2!
388
      throw new Error('Error while calling extensions endpoint: ', response);
×
389
    }
390
    const extensions = await response.json();
2✔
391
    logInfo(LOG_PREFIX + 'extensions response received from the server', extensions);
2✔
392
    return extensions;
2✔
393
  }
394

395
  // eslint-disable-next-line no-dupe-class-members
396
  async #callId5Fetch(fetchCallConfig, extensionsData) {
397
    const fetchUrl = fetchCallConfig.url;
26✔
398
    const additionalData = fetchCallConfig.overrides || {};
26✔
399
    const body = JSON.stringify({
26✔
400
      ...this.#createFetchRequestData(),
401
      ...additionalData,
402
      extensions: extensionsData
403
    });
404
    const response = await fetch(fetchUrl, {method: 'POST', body, credentials: 'include'});
26✔
405
    if (!response.ok) {
26!
406
      throw new Error('Error while calling fetch endpoint: ', response);
×
407
    }
408
    const fetchResponse = await response.json();
26✔
409
    logInfo(LOG_PREFIX + 'fetch response received from the server', fetchResponse);
26✔
410
    return fetchResponse;
26✔
411
  }
412

413
  // eslint-disable-next-line no-dupe-class-members
414
  #createFetchRequestData() {
415
    const params = this.submoduleConfig.params;
26✔
416
    const hasGdpr = (this.gdprConsentData && typeof this.gdprConsentData.gdprApplies === 'boolean' && this.gdprConsentData.gdprApplies) ? 1 : 0;
26✔
417
    const referer = getRefererInfo();
26✔
418
    const signature = this.cacheIdObj ? this.cacheIdObj.signature : undefined;
26✔
419
    const nbPage = incrementNb(this.cacheIdObj);
26✔
420
    const trueLinkInfo = window.id5Bootstrap ? window.id5Bootstrap.getTrueLinkInfo() : {booted: false};
26✔
421

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

438
    // pass in optional data, but only if populated
439
    if (hasGdpr && this.gdprConsentData.consentString !== undefined && !isEmpty(this.gdprConsentData.consentString) && !isEmptyStr(this.gdprConsentData.consentString)) {
26✔
440
      data.gdpr_consent = this.gdprConsentData.consentString;
2✔
441
    }
442
    if (this.usPrivacyData !== undefined && !isEmpty(this.usPrivacyData) && !isEmptyStr(this.usPrivacyData)) {
26✔
443
      data.us_privacy = this.usPrivacyData;
1✔
444
    }
445
    if (this.gppData) {
26✔
446
      data.gpp_string = this.gppData.gppString;
1✔
447
      data.gpp_sid = this.gppData.applicableSections;
1✔
448
    }
449

450
    if (signature !== undefined && !isEmptyStr(signature)) {
26✔
451
      data.s = signature;
13✔
452
    }
453
    if (params.pd !== undefined && !isEmptyStr(params.pd)) {
26✔
454
      data.pd = params.pd;
1✔
455
    }
456
    if (params.provider !== undefined && !isEmptyStr(params.provider)) {
26!
457
      data.provider = params.provider;
×
458
    }
459
    const abTestingConfig = params.abTesting || {enabled: false};
26✔
460

461
    if (abTestingConfig.enabled) {
26✔
462
      data.ab_testing = {
1✔
463
        enabled: true, control_group_pct: abTestingConfig.controlGroupPct // The server validates
464
      };
465
    }
466
    return data;
26✔
467
  }
468

469
  // eslint-disable-next-line no-dupe-class-members
470
  #processFetchCallResponse(fetchCallResponse) {
471
    try {
26✔
472
      if (fetchCallResponse.privacy) {
26!
473
        if (window.id5Bootstrap && window.id5Bootstrap.setPrivacy) {
×
474
          window.id5Bootstrap.setPrivacy(fetchCallResponse.privacy);
×
475
        }
476
      }
477
    } catch (error) {
478
      logError(LOG_PREFIX + 'Error while writing privacy info into local storage.', error);
×
479
    }
480
    return fetchCallResponse;
26✔
481
  }
482
}
483

484
async function loadExternalModule(url) {
485
  return new GreedyPromise((resolve, reject) => {
2✔
486
    if (window.id5Prebid) {
2✔
487
      // Already loaded
488
      resolve();
1✔
489
    } else {
490
      try {
1✔
491
        loadExternalScript(url, MODULE_TYPE_UID, 'id5', resolve);
1✔
492
      } catch (error) {
493
        reject(error);
×
494
      }
495
    }
496
  });
497
}
498

499
function validateConfig(config) {
500
  if (!config || !config.params || !config.params.partner) {
57✔
501
    logError(LOG_PREFIX + 'partner required to be defined');
22✔
502
    return false;
22✔
503
  }
504

505
  const partner = config.params.partner;
35✔
506
  if (typeof partner === 'string' || partner instanceof String) {
35✔
507
    let parsedPartnerId = parseInt(partner);
2✔
508
    if (isNaN(parsedPartnerId) || parsedPartnerId < 0) {
2✔
509
      logError(LOG_PREFIX + 'partner required to be a number or a String parsable to a positive integer');
1✔
510
      return false;
1✔
511
    } else {
512
      config.params.partner = parsedPartnerId;
1✔
513
    }
514
  } else if (typeof partner !== 'number') {
33!
515
    logError(LOG_PREFIX + 'partner required to be a number or a String parsable to a positive integer');
×
516
    return false;
×
517
  }
518

519
  if (!config.storage || !config.storage.type || !config.storage.name) {
34✔
520
    logError(LOG_PREFIX + 'storage required to be set');
4✔
521
    return false;
4✔
522
  }
523
  if (config.storage.name !== ID5_STORAGE_NAME) {
30✔
524
    logWarn(LOG_PREFIX + `storage name recommended to be '${ID5_STORAGE_NAME}'.`);
1✔
525
  }
526

527
  return true;
30✔
528
}
529

530
function incrementNb(cachedObj) {
531
  if (cachedObj && cachedObj.nbPage !== undefined) {
33✔
532
    return cachedObj.nbPage + 1;
6✔
533
  } else {
534
    return 1;
27✔
535
  }
536
}
537

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

553
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