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

prebid / Prebid.js / #285

03 Mar 2025 04:58PM UTC coverage: 90.452% (-0.07%) from 90.523%
#285

push

travis-ci

prebidjs-release
Prebid 9.33.0 release

42181 of 52890 branches covered (79.75%)

62679 of 69295 relevant lines covered (90.45%)

222.46 hits per line

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

86.45
/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, isPlainObject} from '../src/utils.js';
9
import {ajax} from '../src/ajax.js';
10
import {submodule} from '../src/hook.js'
11
import AES from 'crypto-js/aes.js';
12
import Utf8 from 'crypto-js/enc-utf8.js';
13
import {detectBrowser} from '../libraries/intentIqUtils/detectBrowserUtils.js';
14
import {appendVrrefAndFui} from '../libraries/intentIqUtils/getRefferer.js';
15
import { getCmpData } from '../libraries/intentIqUtils/getCmpData.js';
16
import {readData, storeData, defineStorageType, removeDataByKey} from '../libraries/intentIqUtils/storageUtils.js';
17
import {
18
  FIRST_PARTY_KEY,
19
  WITH_IIQ, WITHOUT_IIQ,
20
  NOT_YET_DEFINED,
21
  BLACK_LIST,
22
  CLIENT_HINTS_KEY,
23
  EMPTY,
24
  GVLID,
25
  VERSION,
26
} from '../libraries/intentIqConstants/intentIqConstants.js';
27

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

34
const MODULE_NAME = 'intentIqId';
1✔
35

36
const encoderCH = {
1✔
37
  brands: 0,
38
  mobile: 1,
39
  platform: 2,
40
  architecture: 3,
41
  bitness: 4,
42
  model: 5,
43
  platformVersion: 6,
44
  wow64: 7,
45
  fullVersionList: 8
46
};
47
const INVALID_ID = 'INVALID_ID';
1✔
48
const ENDPOINT = 'https://api.intentiq.com';
1✔
49
const GDPR_ENDPOINT = 'https://api-gdpr.intentiq.com';
1✔
50
export let firstPartyData;
51

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

66
/**
67
 * Encrypts plaintext.
68
 * @param {string} plainText The plaintext to encrypt.
69
 * @returns {string} The encrypted text as a base64 string.
70
 */
71
export function encryptData(plainText) {
72
  return AES.encrypt(plainText, MODULE_NAME).toString();
4✔
73
}
74

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

85
/**
86
 * Parse json if possible, else return null
87
 * @param data
88
 */
89
function tryParse(data) {
90
  try {
61✔
91
    return JSON.parse(data);
61✔
92
  } catch (err) {
93
    logError(err);
2✔
94
    return null;
2✔
95
  }
96
}
97

98
/**
99
 * Configures and updates A/B testing group in Google Ad Manager (GAM).
100
 *
101
 * @param {object} gamObjectReference - Reference to the GAM object, expected to have a `cmd` queue and `pubads()` API.
102
 * @param {string} gamParameterName - The name of the GAM targeting parameter where the group value will be stored.
103
 * @param {string} userGroup - The A/B testing group assigned to the user (e.g., 'A', 'B', or a custom value).
104
 */
105
export function setGamReporting(gamObjectReference, gamParameterName, userGroup) {
106
  if (isPlainObject(gamObjectReference) && gamObjectReference.cmd) {
26✔
107
    gamObjectReference.cmd.push(() => {
10✔
108
      gamObjectReference
3✔
109
        .pubads()
110
        .setTargeting(gamParameterName, userGroup || NOT_YET_DEFINED);
5✔
111
    });
112
  }
113
}
114

115
/**
116
 * Processes raw client hints data into a structured format.
117
 * @param {object} clientHints - Raw client hints data
118
 * @return {string} A JSON string of processed client hints or an empty string if no hints
119
 */
120
export function handleClientHints(clientHints) {
121
  const chParams = {};
21✔
122
  for (const key in clientHints) {
21✔
123
    if (clientHints.hasOwnProperty(key) && clientHints[key] !== '') {
189✔
124
      if (['brands', 'fullVersionList'].includes(key)) {
168✔
125
        let handledParam = '';
42✔
126
        clientHints[key].forEach((element, index) => {
42✔
127
          const isNotLast = index < clientHints[key].length - 1;
114✔
128
          handledParam += `"${element.brand}";v="${element.version}"${isNotLast ? ', ' : ''}`;
114✔
129
        });
130
        chParams[encoderCH[key]] = handledParam;
42✔
131
      } else if (typeof clientHints[key] === 'boolean') {
126✔
132
        chParams[encoderCH[key]] = `?${clientHints[key] ? 1 : 0}`;
42!
133
      } else {
134
        chParams[encoderCH[key]] = `"${clientHints[key]}"`;
84✔
135
      }
136
    }
137
  }
138
  return Object.keys(chParams).length ? JSON.stringify(chParams) : '';
21!
139
}
140

141
export function isCMPStringTheSame(fpData, cmpData) {
142
  const firstPartyDataCPString = `${fpData.gdprString}${fpData.gppString}${fpData.uspString}`;
×
143
  const cmpDataString = `${cmpData.gdprString}${cmpData.gppString}${cmpData.uspString}`;
×
144
  return firstPartyDataCPString === cmpDataString;
×
145
}
146

147
/** @type {Submodule} */
148
export const intentIqIdSubmodule = {
1✔
149
  /**
150
   * used to link submodule with config
151
   * @type {string}
152
   */
153
  name: MODULE_NAME,
154
  gvlid: GVLID,
155
  /**
156
   * decode the stored id value for passing to bid requests
157
   * @function
158
   * @param {{string}} value
159
   * @returns {{intentIqId: {string}}|undefined}
160
   */
161
  decode(value) {
162
    return value && value != '' && INVALID_ID != value ? {'intentIqId': value} : undefined;
3!
163
  },
164
  /**
165
   * performs action to obtain id and return a value in the callback's response argument
166
   * @function
167
   * @param {SubmoduleConfig} [config]
168
   * @returns {IdResponse|undefined}
169
   */
170
  getId(config) {
171
    const configParams = (config?.params) || {};
25!
172
    let decryptedData, callbackTimeoutID;
173
    let callbackFired = false;
25✔
174
    let runtimeEids = { eids: [] };
25✔
175

176
    let gamObjectReference = isPlainObject(configParams.gamObjectReference) ? configParams.gamObjectReference : undefined;
25✔
177
    let gamParameterName = configParams.gamParameterName ? configParams.gamParameterName : 'intent_iq_group';
25✔
178

179
    const allowedStorage = defineStorageType(config.enabledStorageTypes);
25✔
180

181
    let rrttStrtTime = 0;
25✔
182
    let partnerData = {};
25✔
183
    let shouldCallServer = false;
25✔
184
    const FIRST_PARTY_DATA_KEY = `${FIRST_PARTY_KEY}_${configParams.partner}`;
25✔
185
    const cmpData = getCmpData();
25✔
186
    const gdprDetected = cmpData.gdprString;
25✔
187
    firstPartyData = tryParse(readData(FIRST_PARTY_KEY, allowedStorage));
25✔
188
    const isGroupB = firstPartyData?.group === WITHOUT_IIQ;
25✔
189
    setGamReporting(gamObjectReference, gamParameterName, firstPartyData?.group)
25✔
190

191
    const firePartnerCallback = () => {
25✔
192
      if (configParams.callback && !callbackFired) {
30✔
193
        callbackFired = true;
1✔
194
        if (callbackTimeoutID) clearTimeout(callbackTimeoutID);
1!
195
        if (isGroupB) runtimeEids = { eids: [] };
1!
196
        configParams.callback(runtimeEids, firstPartyData?.group || NOT_YET_DEFINED);
1!
197
      }
198
    }
199

200
    callbackTimeoutID = setTimeout(() => {
25✔
201
      firePartnerCallback();
11✔
202
    }, configParams.timeoutInMillis || 500
50✔
203
    );
204

205
    if (typeof configParams.partner !== 'number') {
25✔
206
      logError('User ID - intentIqId submodule requires a valid partner to be defined');
3✔
207
      firePartnerCallback()
3✔
208
      return;
3✔
209
    }
210

211
    const currentBrowserLowerCase = detectBrowser();
22✔
212
    const browserBlackList = typeof configParams.browserBlackList === 'string' ? configParams.browserBlackList.toLowerCase() : '';
22✔
213

214
    // Check if current browser is in blacklist
215
    if (browserBlackList?.includes(currentBrowserLowerCase)) {
22✔
216
      logError('User ID - intentIqId submodule: browser is in blacklist!');
2✔
217
      if (configParams.callback) configParams.callback('', BLACK_LIST);
2✔
218
      return;
2✔
219
    }
220

221
    if (!firstPartyData?.pcid) {
20!
222
      const firstPartyId = generateGUID();
20✔
223
      firstPartyData = {
20✔
224
        pcid: firstPartyId,
225
        pcidDate: Date.now(),
226
        group: NOT_YET_DEFINED,
227
        cttl: 0,
228
        uspString: EMPTY,
229
        gppString: EMPTY,
230
        gdprString: EMPTY,
231
        date: Date.now()
232
      };
233
      storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData);
20✔
234
    } else if (!firstPartyData.pcidDate) {
×
235
      firstPartyData.pcidDate = Date.now();
×
236
      storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData);
×
237
    }
238

239
    if (gdprDetected && !('isOptedOut' in firstPartyData)) {
20✔
240
      firstPartyData.isOptedOut = true;
4✔
241
    }
242

243
    // Read client hints from storage
244
    let clientHints = readData(CLIENT_HINTS_KEY, allowedStorage);
20✔
245

246
    // Get client hints and save to storage
247
    if (navigator?.userAgentData?.getHighEntropyValues) {
20!
248
      navigator.userAgentData
20✔
249
        .getHighEntropyValues([
250
          'brands',
251
          'mobile',
252
          'bitness',
253
          'wow64',
254
          'architecture',
255
          'model',
256
          'platform',
257
          'platformVersion',
258
          'fullVersionList'
259
        ])
260
        .then(ch => {
261
          clientHints = handleClientHints(ch);
20✔
262
          storeData(CLIENT_HINTS_KEY, clientHints, allowedStorage, firstPartyData)
20✔
263
        });
264
    }
265

266
    const savedData = tryParse(readData(FIRST_PARTY_DATA_KEY, allowedStorage))
20✔
267
    if (savedData) {
20✔
268
      partnerData = savedData;
3✔
269

270
      if (partnerData.wsrvcll) {
3!
271
        partnerData.wsrvcll = false;
×
272
        storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData);
×
273
      }
274
    }
275

276
    if (partnerData.data) {
20✔
277
      if (partnerData.data.length) { // encrypted data
1!
278
        decryptedData = tryParse(decryptData(partnerData.data));
1✔
279
        runtimeEids = decryptedData;
1✔
280
      }
281
    }
282

283
    if (!firstPartyData.cttl || Date.now() - firstPartyData.date > firstPartyData.cttl || !isCMPStringTheSame(firstPartyData, cmpData)) {
20!
284
      firstPartyData.uspString = cmpData.uspString;
20✔
285
      firstPartyData.gppString = cmpData.gppString;
20✔
286
      firstPartyData.gdprString = cmpData.gdprString;
20✔
287
      firstPartyData.date = Date.now();
20✔
288
      shouldCallServer = true;
20✔
289
      storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData);
20✔
290
      storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData);
20✔
291
    } else if (firstPartyData.isOptedOut) {
×
292
      partnerData.data = runtimeEids = { eids: [] };
×
293
      firePartnerCallback()
×
294
    }
295

296
    if (firstPartyData.group === WITHOUT_IIQ || (firstPartyData.group !== WITHOUT_IIQ && runtimeEids?.eids?.length)) {
20✔
297
      firePartnerCallback()
1✔
298
    }
299

300
    if (!shouldCallServer) {
20!
301
      if (isGroupB) runtimeEids = { eids: [] };
×
302
      firePartnerCallback();
×
303
      return { id: runtimeEids.eids };
×
304
    }
305

306
    // use protocol relative urls for http or https
307
    let url = `${gdprDetected ? GDPR_ENDPOINT : ENDPOINT}/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=${configParams.partner}&pt=17&dpn=1`;
20✔
308
    url += configParams.pcid ? '&pcid=' + encodeURIComponent(configParams.pcid) : '';
20✔
309
    url += configParams.pai ? '&pai=' + encodeURIComponent(configParams.pai) : '';
20✔
310
    url += firstPartyData.pcid ? '&iiqidtype=2&iiqpcid=' + encodeURIComponent(firstPartyData.pcid) : '';
20!
311
    url += firstPartyData.pid ? '&pid=' + encodeURIComponent(firstPartyData.pid) : '';
20!
312
    url += (partnerData.cttl) ? '&cttl=' + encodeURIComponent(partnerData.cttl) : '';
20✔
313
    url += (partnerData.rrtt) ? '&rrtt=' + encodeURIComponent(partnerData.rrtt) : '';
20✔
314
    url += firstPartyData.pcidDate ? '&iiqpciddate=' + encodeURIComponent(firstPartyData.pcidDate) : '';
20!
315
    url += cmpData.uspString ? '&us_privacy=' + encodeURIComponent(cmpData.uspString) : '';
20✔
316
    url += cmpData.gppString ? '&gpp=' + encodeURIComponent(cmpData.gppString) : '';
20✔
317
    url += cmpData.gdprApplies
20✔
318
      ? '&gdpr_consent=' + encodeURIComponent(cmpData.gdprString) + '&gdpr=1'
319
      : '&gdpr=0';
320
    url += clientHints ? '&uh=' + encodeURIComponent(clientHints) : '';
20✔
321
    url += VERSION ? '&jsver=' + VERSION : '';
20!
322
    url += firstPartyData?.group ? '&testGroup=' + encodeURIComponent(firstPartyData.group) : '';
20!
323

324
    // Add vrref and fui to the URL
325
    url = appendVrrefAndFui(url, configParams.domainName);
20✔
326

327
    const storeFirstPartyData = () => {
20✔
328
      partnerData.eidl = runtimeEids?.eids?.length || -1
13✔
329
      storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData);
13✔
330
      storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData);
13✔
331
    }
332

333
    const resp = function (callback) {
20✔
334
      const callbacks = {
18✔
335
        success: response => {
336
          let respJson = tryParse(response);
15✔
337
          // If response is a valid json and should save is true
338
          if (respJson) {
15✔
339
            partnerData.date = Date.now();
14✔
340
            firstPartyData.date = Date.now();
14✔
341
            const defineEmptyDataAndFireCallback = () => {
14✔
342
              respJson.data = partnerData.data = runtimeEids = { eids: [] };
1✔
343
              storeFirstPartyData()
1✔
344
              firePartnerCallback()
1✔
345
              callback(runtimeEids)
1✔
346
            }
347
            if (callbackTimeoutID) clearTimeout(callbackTimeoutID)
14!
348
            if ('cttl' in respJson) {
14!
349
              firstPartyData.cttl = respJson.cttl;
×
350
            } else firstPartyData.cttl = 86400000;
14✔
351

352
            if ('tc' in respJson) {
14✔
353
              partnerData.terminationCause = respJson.tc;
2✔
354
              if (respJson.tc == 41) {
2!
355
                firstPartyData.group = WITHOUT_IIQ;
×
356
                storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData);
×
357
                defineEmptyDataAndFireCallback();
×
358
                if (gamObjectReference) setGamReporting(gamObjectReference, gamParameterName, firstPartyData.group);
×
359
                return
×
360
              } else {
361
                firstPartyData.group = WITH_IIQ;
2✔
362
                if (gamObjectReference) setGamReporting(gamObjectReference, gamParameterName, firstPartyData.group);
2✔
363
              }
364
            }
365
            if ('isOptedOut' in respJson) {
14✔
366
              if (respJson.isOptedOut !== firstPartyData.isOptedOut) {
3✔
367
                firstPartyData.isOptedOut = respJson.isOptedOut;
2✔
368
              }
369
              if (respJson.isOptedOut === true) {
3✔
370
                respJson.data = partnerData.data = runtimeEids = { eids: [] };
1✔
371

372
                const keysToRemove = [
1✔
373
                  FIRST_PARTY_DATA_KEY,
374
                  CLIENT_HINTS_KEY
375
                ];
376

377
                keysToRemove.forEach(key => removeDataByKey(key, allowedStorage));
2✔
378

379
                storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData);
1✔
380
                firePartnerCallback();
1✔
381
                callback(runtimeEids);
1✔
382
                return
1✔
383
              }
384
            }
385
            if ('pid' in respJson) {
13✔
386
              firstPartyData.pid = respJson.pid;
4✔
387
            }
388
            if ('ls' in respJson) {
13✔
389
              if (respJson.ls === false) {
5✔
390
                defineEmptyDataAndFireCallback()
1✔
391
                return
1✔
392
              }
393
              // If data is empty, means we should save as INVALID_ID
394
              if (respJson.data == '') {
4!
395
                respJson.data = INVALID_ID;
×
396
              } else {
397
                // If data is a single string, assume it is an id with source intentiq.com
398
                if (respJson.data && typeof respJson.data === 'string') {
4!
399
                  respJson.data = {eids: [respJson.data]}
4✔
400
                }
401
              }
402
              partnerData.data = respJson.data;
4✔
403
            }
404

405
            if ('ct' in respJson) {
12✔
406
              partnerData.ct = respJson.ct;
1✔
407
            }
408

409
            if ('sid' in respJson) {
12!
410
              partnerData.siteId = respJson.sid;
×
411
            }
412

413
            if (rrttStrtTime && rrttStrtTime > 0) {
12!
414
              partnerData.rrtt = Date.now() - rrttStrtTime;
12✔
415
            }
416

417
            if (respJson.data?.eids) {
12✔
418
              runtimeEids = respJson.data
4✔
419
              callback(respJson.data.eids);
4✔
420
              firePartnerCallback()
4✔
421
              const encryptedData = encryptData(JSON.stringify(respJson.data))
4✔
422
              partnerData.data = encryptedData;
4✔
423
            } else {
424
              callback(runtimeEids);
8✔
425
              firePartnerCallback()
8✔
426
            }
427
            storeFirstPartyData();
12✔
428
          } else {
429
            callback(runtimeEids);
1✔
430
            firePartnerCallback()
1✔
431
          }
432
        },
433
        error: error => {
434
          logError(MODULE_NAME + ': ID fetch encountered an error', error);
1✔
435
          callback(runtimeEids);
1✔
436
        }
437
      };
438
      rrttStrtTime = Date.now();
18✔
439

440
      partnerData.wsrvcll = true;
18✔
441
      storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData);
18✔
442
      ajax(url, callbacks, undefined, {method: 'GET', withCredentials: true});
18✔
443
    };
444
    const respObj = {callback: resp};
20✔
445

446
    if (runtimeEids?.eids?.length) respObj.id = runtimeEids.eids;
20✔
447
    return respObj
20✔
448
  },
449
  eids: {
450
    'intentIqId': {
451
      source: 'intentiq.com',
452
      atype: 1,
453
      getSource: function (data) {
454
        return data.source;
×
455
      },
456
      getValue: function (data) {
457
        if (data?.uids?.length) {
×
458
          return data.uids[0].id
×
459
        }
460
        return null
×
461
      },
462
      getUidExt: function (data) {
463
        if (data?.uids?.length) {
×
464
          return data.uids[0].ext;
×
465
        }
466
        return null
×
467
      }
468
    },
469
  }
470
};
471

472
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