• 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

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

8
import {logError, logInfo, isPlainObject} from '../src/utils.js';
9
import {ajax} from '../src/ajax.js';
10
import {submodule} from '../src/hook.js'
11
import {getStorageManager} from '../src/storageManager.js';
12
import {MODULE_TYPE_UID} from '../src/activities/modules.js';
13
import {uspDataHandler} from '../src/consentHandler.js';
14
import AES from 'crypto-js/aes.js';
15
import Utf8 from 'crypto-js/enc-utf8.js';
16
import {detectBrowser} from '../libraries/intentIqUtils/detectBrowserUtils.js';
17
import {appendVrrefAndFui} from '../libraries/intentIqUtils/getRefferer.js';
18
import {getGppValue} from '../libraries/intentIqUtils/getGppValue.js';
19
import {
20
  FIRST_PARTY_KEY,
21
  WITH_IIQ, WITHOUT_IIQ,
22
  NOT_YET_DEFINED,
23
  OPT_OUT,
24
  BLACK_LIST,
25
  CLIENT_HINTS_KEY,
26
  EMPTY,
27
  VERSION
28
} from '../libraries/intentIqConstants/intentIqConstants.js';
29

30
/**
31
 * @typedef {import('../modules/userId/index.js').Submodule} Submodule
32
 * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig
33
 * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse
34
 */
35

36
const PCID_EXPIRY = 365;
1✔
37

38
const MODULE_NAME = 'intentIqId';
1✔
39

40
const encoderCH = {
1✔
41
  brands: 0,
42
  mobile: 1,
43
  platform: 2,
44
  architecture: 3,
45
  bitness: 4,
46
  model: 5,
47
  platformVersion: 6,
48
  wow64: 7,
49
  fullVersionList: 8
50
};
51
const INVALID_ID = 'INVALID_ID';
1✔
52
const SUPPORTED_TYPES = ['html5', 'cookie']
1✔
53

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

56
/**
57
 * Generate standard UUID string
58
 * @return {string}
59
 */
60
function generateGUID() {
61
  let d = new Date().getTime();
18✔
62
  const guid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
18✔
63
    const r = (d + Math.random() * 16) % 16 | 0;
558✔
64
    d = Math.floor(d / 16);
558✔
65
    return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
558✔
66
  });
67
  return guid;
18✔
68
}
69

70
/**
71
 * Encrypts plaintext.
72
 * @param {string} plainText The plaintext to encrypt.
73
 * @returns {string} The encrypted text as a base64 string.
74
 */
75
export function encryptData(plainText) {
76
  return AES.encrypt(plainText, MODULE_NAME).toString();
5✔
77
}
78

79
/**
80
 * Decrypts ciphertext.
81
 * @param {string} encryptedText The encrypted text as a base64 string.
82
 * @returns {string} The decrypted plaintext.
83
 */
84
export function decryptData(encryptedText) {
85
  const bytes = AES.decrypt(encryptedText, MODULE_NAME);
3✔
86
  return bytes.toString(Utf8);
3✔
87
}
88

89
/**
90
 * Read Intent IQ data from local storage or cookie
91
 * @param key
92
 * @return {string}
93
 */
94
export function readData(key, allowedStorage) {
95
  try {
60✔
96
    if (storage.hasLocalStorage() && allowedStorage.includes('html5')) {
60✔
97
      return storage.getDataFromLocalStorage(key);
57✔
98
    }
99
    if (storage.cookiesAreEnabled() && allowedStorage.includes('cookie')) {
3!
100
      return storage.getCookie(key);
3✔
101
    }
102
  } catch (error) {
103
    logError(error);
×
104
  }
105
}
106

107
/**
108
 * Store Intent IQ data in cookie, local storage or both of them
109
 * expiration date: 365 days
110
 * @param key
111
 * @param {string} value IntentIQ ID value to sintentIqIdSystem_spec.jstore
112
 */
113
export function storeData(key, value, allowedStorage) {
114
  try {
114✔
115
    logInfo(MODULE_NAME + ': storing data: key=' + key + ' value=' + value);
114✔
116
    if (value) {
114!
117
      if (storage.hasLocalStorage() && allowedStorage.includes('html5')) {
114✔
118
        storage.setDataInLocalStorage(key, value);
107✔
119
      }
120
      if (storage.cookiesAreEnabled() && allowedStorage.includes('cookie')) {
114✔
121
        const expiresStr = (new Date(Date.now() + (PCID_EXPIRY * (60 * 60 * 24 * 1000)))).toUTCString();
7✔
122
        storage.setCookie(key, value, expiresStr, 'LAX');
7✔
123
      }
124
    }
125
  } catch (error) {
126
    logError(error);
×
127
  }
128
}
129

130
/**
131
 * Remove Intent IQ data from cookie or local storage
132
 * @param key
133
 */
134

135
export function removeDataByKey(key, allowedStorage) {
136
  try {
×
137
    if (storage.hasLocalStorage() && allowedStorage.includes('html5')) {
×
138
      storage.removeDataFromLocalStorage(key);
×
139
    }
140
    if (storage.cookiesAreEnabled() && allowedStorage.includes('cookie')) {
×
141
      const expiredDate = new Date(0).toUTCString();
×
142
      storage.setCookie(key, '', expiredDate, 'LAX');
×
143
    }
144
  } catch (error) {
145
    logError(error);
×
146
  }
147
}
148

149
/**
150
 * Parse json if possible, else return null
151
 * @param data
152
 */
153
function tryParse(data) {
154
  try {
56✔
155
    return JSON.parse(data);
56✔
156
  } catch (err) {
157
    logError(err);
2✔
158
    return null;
2✔
159
  }
160
}
161

162
/**
163
 * Configures and updates A/B testing group in Google Ad Manager (GAM).
164
 *
165
 * @param {object} gamObjectReference - Reference to the GAM object, expected to have a `cmd` queue and `pubads()` API.
166
 * @param {string} gamParameterName - The name of the GAM targeting parameter where the group value will be stored.
167
 * @param {string} userGroup - The A/B testing group assigned to the user (e.g., 'A', 'B', or a custom value).
168
 */
169
export function setGamReporting(gamObjectReference, gamParameterName, userGroup) {
170
  if (isPlainObject(gamObjectReference) && Array.isArray(gamObjectReference.cmd)) {
24✔
171
    gamObjectReference.cmd.push(() => {
8✔
172
      gamObjectReference
3✔
173
        .pubads()
174
        .setTargeting(gamParameterName, userGroup || NOT_YET_DEFINED);
5✔
175
    });
176
  }
177
}
178

179
/**
180
 * Processes raw client hints data into a structured format.
181
 * @param {object} clientHints - Raw client hints data
182
 * @return {string} A JSON string of processed client hints or an empty string if no hints
183
 */
184
export function handleClientHints(clientHints) {
185
  const chParams = {};
19✔
186
  for (const key in clientHints) {
19✔
187
    if (clientHints.hasOwnProperty(key) && clientHints[key] !== '') {
171✔
188
      if (['brands', 'fullVersionList'].includes(key)) {
152✔
189
        let handledParam = '';
38✔
190
        clientHints[key].forEach((element, index) => {
38✔
191
          const isNotLast = index < clientHints[key].length - 1;
106✔
192
          handledParam += `"${element.brand}";v="${element.version}"${isNotLast ? ', ' : ''}`;
106✔
193
        });
194
        chParams[encoderCH[key]] = handledParam;
38✔
195
      } else if (typeof clientHints[key] === 'boolean') {
114✔
196
        chParams[encoderCH[key]] = `?${clientHints[key] ? 1 : 0}`;
38!
197
      } else {
198
        chParams[encoderCH[key]] = `"${clientHints[key]}"`;
76✔
199
      }
200
    }
201
  }
202
  return Object.keys(chParams).length ? JSON.stringify(chParams) : '';
19!
203
}
204

205
function defineStorageType(params) {
206
  if (!params || !Array.isArray(params)) return ['html5']; // use locale storage be default
23✔
207
  const filteredArr = params.filter(item => SUPPORTED_TYPES.includes(item));
2✔
208
  return filteredArr.length ? filteredArr : ['html5'];
2✔
209
}
210

211
/** @type {Submodule} */
212
export const intentIqIdSubmodule = {
1✔
213
  /**
214
   * used to link submodule with config
215
   * @type {string}
216
   */
217
  name: MODULE_NAME,
218
  /**
219
   * decode the stored id value for passing to bid requests
220
   * @function
221
   * @param {{string}} value
222
   * @returns {{intentIqId: {string}}|undefined}
223
   */
224
  decode(value) {
225
    return value && value != '' && INVALID_ID != value ? {'intentIqId': value} : undefined;
3!
226
  },
227
  /**
228
   * performs action to obtain id and return a value in the callback's response argument
229
   * @function
230
   * @param {SubmoduleConfig} [config]
231
   * @returns {IdResponse|undefined}
232
   */
233
  getId(config) {
234
    const configParams = (config?.params) || {};
23!
235
    let decryptedData, callbackTimeoutID;
236
    let callbackFired = false;
23✔
237
    let runtimeEids = { eids: [] };
23✔
238
    let gamObjectReference = isPlainObject(configParams.gamObjectReference) ? configParams.gamObjectReference : undefined;
23✔
239
    let gamParameterName = configParams.gamParameterName ? configParams.gamParameterName : 'intent_iq_group';
23✔
240

241
    const allowedStorage = defineStorageType(config.enabledStorageTypes);
23✔
242

243
    let firstPartyData = tryParse(readData(FIRST_PARTY_KEY, allowedStorage));
23✔
244
    const isGroupB = firstPartyData?.group === WITHOUT_IIQ;
23✔
245
    setGamReporting(gamObjectReference, gamParameterName, firstPartyData?.group)
23✔
246

247
    const firePartnerCallback = () => {
23✔
248
      if (configParams.callback && !callbackFired) {
28✔
249
        callbackFired = true;
1✔
250
        if (callbackTimeoutID) clearTimeout(callbackTimeoutID);
1!
251
        if (isGroupB) runtimeEids = { eids: [] };
1!
252
        configParams.callback(runtimeEids, firstPartyData?.group || NOT_YET_DEFINED);
1✔
253
      }
254
    }
255

256
    callbackTimeoutID = setTimeout(() => {
23✔
257
      firePartnerCallback();
10✔
258
    }, configParams.timeoutInMillis || 500
46✔
259
    );
260

261
    if (typeof configParams.partner !== 'number') {
23✔
262
      logError('User ID - intentIqId submodule requires a valid partner to be defined');
3✔
263
      firePartnerCallback()
3✔
264
      return;
3✔
265
    }
266

267
    const FIRST_PARTY_DATA_KEY = `_iiq_fdata_${configParams.partner}`;
20✔
268

269
    let rrttStrtTime = 0;
20✔
270
    let partnerData = {};
20✔
271
    let shouldCallServer = false
20✔
272

273
    const currentBrowserLowerCase = detectBrowser();
20✔
274
    const browserBlackList = typeof configParams.browserBlackList === 'string' ? configParams.browserBlackList.toLowerCase() : '';
20✔
275

276
    // Check if current browser is in blacklist
277
    if (browserBlackList?.includes(currentBrowserLowerCase)) {
20✔
278
      logError('User ID - intentIqId submodule: browser is in blacklist!');
2✔
279
      if (configParams.callback) configParams.callback('', BLACK_LIST);
2✔
280
      return;
2✔
281
    }
282

283
    // Get consent information
284
    const cmpData = {};
18✔
285
    const uspData = uspDataHandler.getConsentData();
18✔
286
    const gppData = getGppValue();
18✔
287

288
    if (uspData) {
18✔
289
      cmpData.us_privacy = uspData;
1✔
290
    }
291

292
    cmpData.gpp = gppData.gppString;
18✔
293
    cmpData.gpi = gppData.gpi;
18✔
294

295
    // Read client hints from storage
296
    let clientHints = readData(CLIENT_HINTS_KEY, allowedStorage);
18✔
297

298
    // Get client hints and save to storage
299
    if (navigator.userAgentData) {
18!
300
      navigator.userAgentData
18✔
301
        .getHighEntropyValues([
302
          'brands',
303
          'mobile',
304
          'bitness',
305
          'wow64',
306
          'architecture',
307
          'model',
308
          'platform',
309
          'platformVersion',
310
          'fullVersionList'
311
        ])
312
        .then(ch => {
313
          clientHints = handleClientHints(ch);
18✔
314
          storeData(CLIENT_HINTS_KEY, clientHints, allowedStorage)
18✔
315
        });
316
    }
317

318
    if (!firstPartyData?.pcid) {
18!
319
      const firstPartyId = generateGUID();
18✔
320
      firstPartyData = {
18✔
321
        pcid: firstPartyId,
322
        pcidDate: Date.now(),
323
        group: NOT_YET_DEFINED,
324
        cttl: 0,
325
        uspapi_value: EMPTY,
326
        gpp_value: EMPTY,
327
        date: Date.now()
328
      };
329
      storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage);
18✔
330
    } else if (!firstPartyData.pcidDate) {
×
331
      firstPartyData.pcidDate = Date.now();
×
332
      storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage);
×
333
    }
334

335
    const savedData = tryParse(readData(FIRST_PARTY_DATA_KEY, allowedStorage))
18✔
336
    if (savedData) {
18✔
337
      partnerData = savedData;
2✔
338

339
      if (partnerData.wsrvcll) {
2!
340
        partnerData.wsrvcll = false;
×
341
        storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage);
×
342
      }
343
    }
344

345
    if (partnerData.data) {
18✔
346
      if (partnerData.data.length) { // encrypted data
1!
347
        decryptedData = tryParse(decryptData(partnerData.data));
1✔
348
        runtimeEids = decryptedData;
1✔
349
      }
350
    }
351

352
    if (!firstPartyData.cttl || Date.now() - firstPartyData.date > firstPartyData.cttl || firstPartyData.uspapi_value !== cmpData.us_privacy || firstPartyData.gpp_string_value !== cmpData.gpp) {
18!
353
      firstPartyData.uspapi_value = cmpData.us_privacy;
18✔
354
      firstPartyData.gpp_string_value = cmpData.gpp;
18✔
355
      firstPartyData.isOptedOut = false
18✔
356
      firstPartyData.cttl = 0
18✔
357
      shouldCallServer = true;
18✔
358
      partnerData.data = {}
18✔
359
      partnerData.eidl = -1
18✔
360
      storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage);
18✔
361
      storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage);
18✔
362
    } else if (firstPartyData.isOptedOut) {
×
363
      firePartnerCallback()
×
364
    }
365

366
    if (firstPartyData.group === WITHOUT_IIQ || (firstPartyData.group !== WITHOUT_IIQ && runtimeEids?.eids?.length)) {
18✔
367
      firePartnerCallback()
1✔
368
    }
369

370
    if (!shouldCallServer) {
18!
371
      if (isGroupB) runtimeEids = { eids: [] };
×
372
      firePartnerCallback();
×
373
      return { id: runtimeEids.eids };
×
374
    }
375

376
    // use protocol relative urls for http or https
377
    let url = `https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=${configParams.partner}&pt=17&dpn=1`;
18✔
378
    url += configParams.pcid ? '&pcid=' + encodeURIComponent(configParams.pcid) : '';
18✔
379
    url += configParams.pai ? '&pai=' + encodeURIComponent(configParams.pai) : '';
18✔
380
    url += firstPartyData.pcid ? '&iiqidtype=2&iiqpcid=' + encodeURIComponent(firstPartyData.pcid) : '';
18!
381
    url += firstPartyData.pid ? '&pid=' + encodeURIComponent(firstPartyData.pid) : '';
18!
382
    url += (partnerData.cttl) ? '&cttl=' + encodeURIComponent(partnerData.cttl) : '';
18✔
383
    url += (partnerData.rrtt) ? '&rrtt=' + encodeURIComponent(partnerData.rrtt) : '';
18✔
384
    url += firstPartyData.pcidDate ? '&iiqpciddate=' + encodeURIComponent(firstPartyData.pcidDate) : '';
18!
385
    url += cmpData.us_privacy ? '&pa=' + encodeURIComponent(cmpData.us_privacy) : '';
18✔
386
    url += cmpData.gpp ? '&gpp=' + encodeURIComponent(cmpData.gpp) : '';
18✔
387
    url += cmpData.gpi ? '&gpi=' + cmpData.gpi : '';
18✔
388
    url += clientHints ? '&uh=' + encodeURIComponent(clientHints) : '';
18!
389
    url += VERSION ? '&jsver=' + VERSION : '';
18!
390
    url += firstPartyData?.group ? '&testGroup=' + encodeURIComponent(firstPartyData.group) : '';
18!
391

392
    // Add vrref and fui to the URL
393
    url = appendVrrefAndFui(url, configParams.domainName);
18✔
394

395
    const storeFirstPartyData = () => {
18✔
396
      partnerData.eidl = runtimeEids?.eids?.length || -1
13✔
397
      storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage);
13✔
398
      storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage);
13✔
399
    }
400

401
    const resp = function (callback) {
18✔
402
      const callbacks = {
16✔
403
        success: response => {
404
          let respJson = tryParse(response);
14✔
405
          // If response is a valid json and should save is true
406
          if (respJson) {
14✔
407
            partnerData.date = Date.now();
13✔
408
            firstPartyData.date = Date.now();
13✔
409
            const defineEmptyDataAndFireCallback = () => {
13✔
410
              respJson.data = partnerData.data = runtimeEids = { eids: [] };
2✔
411
              storeFirstPartyData()
2✔
412
              firePartnerCallback()
2✔
413
              callback(runtimeEids)
2✔
414
            }
415
            if (callbackTimeoutID) clearTimeout(callbackTimeoutID)
13!
416
            if ('cttl' in respJson) {
13!
417
              firstPartyData.cttl = respJson.cttl;
×
418
            } else firstPartyData.cttl = 86400000;
13✔
419

420
            if ('tc' in respJson) {
13✔
421
              partnerData.terminationCause = respJson.tc;
2✔
422
              if (respJson.tc == 41) {
2!
423
                firstPartyData.group = WITHOUT_IIQ;
×
424
                storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage);
×
425
                defineEmptyDataAndFireCallback();
×
426
                if (gamObjectReference) setGamReporting(gamObjectReference, gamParameterName, firstPartyData.group);
×
427
                return
×
428
              } else {
429
                firstPartyData.group = WITH_IIQ;
2✔
430
                if (gamObjectReference) setGamReporting(gamObjectReference, gamParameterName, firstPartyData.group);
2✔
431
              }
432
            }
433
            if ('isOptedOut' in respJson) {
13✔
434
              if (respJson.isOptedOut !== firstPartyData.isOptedOut) {
1!
435
                firstPartyData.isOptedOut = respJson.isOptedOut;
×
436
              }
437
              if (respJson.isOptedOut === true) {
1!
438
                firstPartyData.group = OPT_OUT;
×
439
                storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage);
×
440
                defineEmptyDataAndFireCallback()
×
441
                return
×
442
              }
443
            }
444
            if ('pid' in respJson) {
13✔
445
              firstPartyData.pid = respJson.pid;
6✔
446
            }
447
            if ('ls' in respJson) {
13✔
448
              if (respJson.ls === false) {
7✔
449
                defineEmptyDataAndFireCallback()
2✔
450
                return
2✔
451
              }
452
              // If data is empty, means we should save as INVALID_ID
453
              if (respJson.data == '') {
5!
454
                respJson.data = INVALID_ID;
×
455
              } else {
456
                // If data is a single string, assume it is an id with source intentiq.com
457
                if (respJson.data && typeof respJson.data === 'string') {
5!
458
                  respJson.data = {eids: [respJson.data]}
5✔
459
                }
460
              }
461
              partnerData.data = respJson.data;
5✔
462
            }
463

464
            if ('ct' in respJson) {
11✔
465
              partnerData.ct = respJson.ct;
1✔
466
            }
467

468
            if ('sid' in respJson) {
11!
469
              partnerData.siteId = respJson.sid;
×
470
            }
471

472
            if (rrttStrtTime && rrttStrtTime > 0) {
11!
473
              partnerData.rrtt = Date.now() - rrttStrtTime;
11✔
474
            }
475

476
            if (respJson.data?.eids) {
11✔
477
              runtimeEids = respJson.data
5✔
478
              callback(respJson.data.eids);
5✔
479
              firePartnerCallback()
5✔
480
              const encryptedData = encryptData(JSON.stringify(respJson.data))
5✔
481
              partnerData.data = encryptedData;
5✔
482
            } else {
483
              callback(runtimeEids);
6✔
484
              firePartnerCallback()
6✔
485
            }
486
            storeFirstPartyData();
11✔
487
          } else {
488
            callback(runtimeEids);
1✔
489
            firePartnerCallback()
1✔
490
          }
491
        },
492
        error: error => {
493
          logError(MODULE_NAME + ': ID fetch encountered an error', error);
1✔
494
          callback(runtimeEids);
1✔
495
        }
496
      };
497
      rrttStrtTime = Date.now();
16✔
498

499
      partnerData.wsrvcll = true;
16✔
500
      storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage);
16✔
501
      ajax(url, callbacks, undefined, {method: 'GET', withCredentials: true});
16✔
502
    };
503
    const respObj = {callback: resp};
18✔
504

505
    if (runtimeEids?.eids?.length) respObj.id = runtimeEids.eids;
18✔
506
    return respObj
18✔
507
  },
508
  eids: {
509
    'intentIqId': {
510
      source: 'intentiq.com',
511
      atype: 1,
512
      getSource: function (data) {
513
        return data.source;
×
514
      },
515
      getValue: function (data) {
516
        if (data?.uids?.length) {
×
517
          return data.uids[0].id
×
518
        }
519
        return null
×
520
      },
521
      getUidExt: function (data) {
522
        if (data?.uids?.length) {
×
523
          return data.uids[0].ext;
×
524
        }
525
        return null
×
526
      }
527
    },
528
  }
529
};
530

531
submodule('userId', intentIqIdSubmodule);
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