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

prebid / Prebid.js / 17802262685

17 Sep 2025 03:13PM UTC coverage: 96.242%. Remained the same
17802262685

push

github

web-flow
feat(resetdigital): include schain in buildRequests payload (#13905)

Add support for passing schain (ORTB2 supply chain object) from
bidderRequest.ortb2.source.ext.schain into the request payload sent
by the ResetDigital adapter. Keeps backward compatibility when schain
is absent.

39849 of 48984 branches covered (81.35%)

16 of 16 new or added lines in 2 files covered. (100.0%)

15 existing lines in 10 files now uncovered.

198147 of 205884 relevant lines covered (96.24%)

124.78 hits per line

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

89.71
/modules/lotamePanoramaIdSystem.js
1
/**
1✔
2
 * This module adds LotamePanoramaId to the User ID module
3
 * The {@link module:modules/userId} module is required
4
 * @module modules/lotamePanoramaId
5
 * @requires module:modules/userId
6
 */
7
import {
8
  timestamp,
9
  isStr,
10
  logError,
11
  isBoolean,
12
  buildUrl,
13
  isEmpty,
14
  isArray
15
} from '../src/utils.js';
16
import { ajax } from '../src/ajax.js';
17
import { submodule } from '../src/hook.js';
18
import {getStorageManager} from '../src/storageManager.js';
19
import {MODULE_TYPE_UID} from '../src/activities/modules.js';
20

21
/**
22
 * @typedef {import('../modules/userId/index.js').Submodule} Submodule
23
 * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig
24
 * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData
25
 * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse
26
 */
27

28
const KEY_ID = 'panoramaId';
1✔
29
const KEY_EXPIRY = `${KEY_ID}_expiry`;
1✔
30
const KEY_PROFILE = '_cc_id';
1✔
31
const MODULE_NAME = 'lotamePanoramaId';
1✔
32
const NINE_MONTHS_MS = 23328000 * 1000;
1✔
33
const DAYS_TO_CACHE = 7;
1✔
34
const DAY_MS = 60 * 60 * 24 * 1000;
1✔
35
const MISSING_CORE_CONSENT = 111;
1✔
36
const GVLID = 95;
1✔
37
const ID_HOST = 'id.crwdcntrl.net';
1✔
38
const ID_HOST_COOKIELESS = 'c.ltmsphrcl.net';
1✔
39
const DO_NOT_HONOR_CONFIG = false;
1✔
40

41
export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME});
1✔
42
let cookieDomain;
43
const appliedConfig = {
1✔
44
  name: 'lotamePanoramaId',
45
  storage: {
46
    type: 'cookie&html5',
47
    name: 'panoramaId'
48
  }
49
};
50

51
/**
52
 * Set the Lotame First Party Profile ID in the first party namespace
53
 * @param {String} profileId
54
 */
55
function setProfileId(profileId) {
56
  if (cookiesAreEnabled()) {
16✔
57
    const expirationDate = new Date(timestamp() + NINE_MONTHS_MS).toUTCString();
16✔
58
    storage.setCookie(
16✔
59
      KEY_PROFILE,
60
      profileId,
61
      expirationDate,
62
      'Lax',
63
      cookieDomain,
64
      undefined
65
    );
66
  }
67
  if (localStorageIsEnabled()) {
16✔
68
    storage.setDataInLocalStorage(KEY_PROFILE, profileId, undefined);
16✔
69
  }
70
}
71

72
/**
73
 * Get the Lotame profile id by checking cookies first and then local storage
74
 */
75
function getProfileId() {
76
  let profileId;
77
  if (cookiesAreEnabled(DO_NOT_HONOR_CONFIG)) {
34✔
78
    profileId = storage.getCookie(KEY_PROFILE, undefined);
34✔
79
  }
80
  if (!profileId && localStorageIsEnabled(DO_NOT_HONOR_CONFIG)) {
34✔
81
    profileId = storage.getDataFromLocalStorage(KEY_PROFILE, undefined);
34✔
82
  }
83
  return profileId;
34✔
84
}
85

86
/**
87
 * Get a value from browser storage by checking cookies first and then local storage
88
 * @param {String} key
89
 */
90
function getFromStorage(key) {
91
  let value = null;
91✔
92
  if (cookiesAreEnabled(DO_NOT_HONOR_CONFIG)) {
91✔
93
    value = storage.getCookie(key, undefined);
91✔
94
  }
95
  if (value === null && localStorageIsEnabled(DO_NOT_HONOR_CONFIG)) {
91✔
96
    value = storage.getDataFromLocalStorage(key, undefined);
2✔
97
  }
98
  return value;
91✔
99
}
100

101
/**
102
 * Save a key/value pair to the browser cache (cookies and local storage)
103
 * @param {String} key
104
 * @param {String} value
105
 * @param {Number} expirationTimestamp
106
 */
107
function saveLotameCache(
108
  key,
109
  value,
110
  expirationTimestamp = timestamp() + DAYS_TO_CACHE * DAY_MS
46!
111
) {
112
  if (key && value) {
46✔
113
    const expirationDate = new Date(expirationTimestamp).toUTCString();
46✔
114
    if (cookiesAreEnabled()) {
46✔
115
      storage.setCookie(
46✔
116
        key,
117
        value,
118
        expirationDate,
119
        'Lax',
120
        cookieDomain,
121
        undefined
122
      );
123
    }
124
    if (localStorageIsEnabled()) {
46✔
125
      storage.setDataInLocalStorage(key, value, undefined);
46✔
126
    }
127
  }
128
}
129

130
/**
131
 * Retrieve all the cached values from cookies and/or local storage
132
 * @param {Number} clientId
133
 */
134
function getLotameLocalCache(clientId = undefined) {
40✔
135
  const cache = {
40✔
136
    data: getFromStorage(KEY_ID),
137
    expiryTimestampMs: 0,
138
    clientExpiryTimestampMs: 0,
139
  };
140

141
  try {
40✔
142
    if (clientId) {
40✔
143
      const rawClientExpiry = getFromStorage(`${KEY_EXPIRY}_${clientId}`);
11✔
144
      if (isStr(rawClientExpiry)) {
11✔
145
        cache.clientExpiryTimestampMs = parseInt(rawClientExpiry, 10);
2✔
146
      }
147
    }
148

149
    const rawExpiry = getFromStorage(KEY_EXPIRY);
40✔
150
    if (isStr(rawExpiry)) {
40✔
151
      cache.expiryTimestampMs = parseInt(rawExpiry, 10);
12✔
152
    }
153
  } catch (error) {
154
    logError(error);
×
155
  }
156

157
  return cache;
40✔
158
}
159

160
/**
161
 * Clear a cached value from cookies and local storage
162
 * @param {String} key
163
 */
164
function clearLotameCache(key) {
165
  if (key) {
14✔
166
    if (cookiesAreEnabled(DO_NOT_HONOR_CONFIG)) {
14✔
167
      const expirationDate = new Date(0).toUTCString();
14✔
168
      storage.setCookie(
14✔
169
        key,
170
        '',
171
        expirationDate,
172
        'Lax',
173
        cookieDomain,
174
        undefined
175
      );
176
    }
177
    if (localStorageIsEnabled(DO_NOT_HONOR_CONFIG)) {
14✔
178
      storage.removeDataFromLocalStorage(key, undefined);
14✔
179
    }
180
  }
181
}
182
/**
183
 * @param {boolean} honorConfig - false to override for reading or deleting old cookies
184
 * @returns {boolean} for whether we can write the cookie
185
 */
186
function cookiesAreEnabled(honorConfig = true) {
201✔
187
  if (honorConfig) {
201✔
188
    return storage.cookiesAreEnabled() && appliedConfig.storage.type.includes('cookie');
62✔
189
  }
190
  return storage.cookiesAreEnabled();
139✔
191
}
192
/**
193
 * @param {boolean} honorConfig - false to override for reading or deleting old stored items
194
 * @returns {boolean} for whether we can write the cookie
195
 */
196
function localStorageIsEnabled(honorConfig = true) {
112✔
197
  if (honorConfig) {
112✔
198
    return storage.hasLocalStorage() && appliedConfig.storage.type.includes('html5');
62✔
199
  }
200
  return storage.hasLocalStorage();
50✔
201
}
202
/**
203
 * @param {SubmoduleConfig} config
204
 * @returns {null|string} - string error if it finds one, null otherwise.
205
 */
206
function checkConfigHasErrorsAndReport(config) {
207
  let error = null;
40✔
208
  if (typeof config.storage !== 'undefined') {
40!
209
    Object.assign(appliedConfig.storage, appliedConfig.storage, config.storage);
×
210
    const READABLE_MODULE_NAME = 'Lotame ID module';
×
211
    const PERMITTED_STORAGE_TYPES = ['cookie', 'html5', 'cookie&html5'];
×
212
    if (typeof config.storage.name !== 'undefined' && config.storage.name !== KEY_ID) {
×
213
      logError(`Misconfigured ${READABLE_MODULE_NAME}, "storage.name" is expected to be "${KEY_ID}", actual is "${config.storage.name}"`);
×
214
      error = true;
×
215
    } else if (config.storage.type !== 'undefined' && !PERMITTED_STORAGE_TYPES.includes(config.storage.type)) {
×
216
      logError(`Misconfigured ${READABLE_MODULE_NAME}, "storage.type" is expected to be one of "${PERMITTED_STORAGE_TYPES.join(', ')}", actual is "${config.storage.type}"`);
×
217
    }
218
  }
219
  return error;
40✔
220
}
221
/** @type {Submodule} */
222
export const lotamePanoramaIdSubmodule = {
1✔
223
  /**
224
   * used to link submodule with config
225
   * @type {string}
226
   */
227
  name: MODULE_NAME,
228

229
  /**
230
   * Vendor id of Lotame
231
   * @type {Number}
232
   */
233
  gvlid: GVLID,
234

235
  /**
236
   * Decode the stored id value for passing to bid requests
237
   * @function decode
238
   * @param {(Object|string)} value
239
   * @param {SubmoduleConfig|undefined} config
240
   * @returns {(Object|undefined)}
241
   */
242
  decode(value, config) {
243
    return isStr(value) ? { lotamePanoramaId: value } : undefined;
×
244
  },
245

246
  /**
247
   * Retrieve the Lotame Panorama Id
248
   * @function
249
   * @param {SubmoduleConfig} [config]
250
   * @param {ConsentData} [consentData]
251
   * @param {(Object|undefined)} cacheIdObj
252
   * @returns {IdResponse|undefined}
253
   */
254
  getId(config, consentData, cacheIdObj) {
255
    if (checkConfigHasErrorsAndReport(config)) {
40!
256
      return;
×
257
    }
258
    cookieDomain = lotamePanoramaIdSubmodule.findRootDomain();
40✔
259
    const configParams = (config && config.params) || {};
40✔
260
    const clientId = configParams.clientId;
40✔
261
    const hasCustomClientId = !isEmpty(clientId);
40✔
262
    const localCache = getLotameLocalCache(clientId);
40✔
263

264
    const hasExpiredPanoId = Date.now() > localCache.expiryTimestampMs;
40✔
265

266
    if (hasCustomClientId) {
40✔
267
      const hasFreshClientNoConsent = Date.now() < localCache.clientExpiryTimestampMs;
11✔
268
      if (hasFreshClientNoConsent) {
11✔
269
        // There is no consent
270
        return {
2✔
271
          id: undefined,
272
          reason: 'NO_CLIENT_CONSENT',
273
        };
274
      }
275
    }
276

277
    if (!hasExpiredPanoId) {
38✔
278
      return {
4✔
279
        id: localCache.data,
280
      };
281
    }
282

283
    const storedUserId = getProfileId();
34✔
284

285
    const getRequestHost = function() {
34✔
286
      if (navigator.userAgent && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) {
34!
UNCOV
287
        return ID_HOST_COOKIELESS;
×
288
      }
289
      return ID_HOST;
34✔
290
    }
291

292
    const resolveIdFunction = function (callback) {
34✔
293
      const queryParams = {};
34✔
294
      if (storedUserId) {
34!
295
        queryParams.fp = storedUserId;
×
296
      }
297

298
      let consentString;
299
      if (consentData) {
34✔
300
        if (isBoolean(consentData.gdpr?.gdprApplies)) {
11✔
301
          queryParams.gdpr_applies = consentData.gdpr.gdprApplies;
9✔
302
        }
303
        consentString = consentData.gdpr?.consentString;
11✔
304
      }
305
      if (consentString) {
34✔
306
        queryParams.gdpr_consent = consentString;
2✔
307
      }
308

309
      // Add clientId to the url
310
      if (hasCustomClientId) {
34✔
311
        queryParams.c = clientId;
7✔
312
      }
313

314
      const url = buildUrl({
34✔
315
        protocol: 'https',
316
        host: getRequestHost(),
317
        pathname: '/id',
318
        search: isEmpty(queryParams) ? undefined : queryParams,
34✔
319
      });
320
      ajax(
34✔
321
        url,
322
        (response) => {
323
          let coreId;
324
          if (response) {
34✔
325
            try {
30✔
326
              const responseObj = JSON.parse(response);
30✔
327
              const hasNoConsentErrors = !(
30✔
328
                isArray(responseObj.errors) &&
41✔
329
                responseObj.errors.indexOf(MISSING_CORE_CONSENT) !== -1
330
              );
331

332
              if (hasCustomClientId) {
30✔
333
                if (hasNoConsentErrors) {
7✔
334
                  clearLotameCache(`${KEY_EXPIRY}_${clientId}`);
2✔
335
                } else if (isStr(responseObj.no_consent) && responseObj.no_consent === 'CLIENT') {
5✔
336
                  saveLotameCache(
5✔
337
                    `${KEY_EXPIRY}_${clientId}`,
338
                    responseObj.expiry_ts,
339
                    responseObj.expiry_ts
340
                  );
341

342
                  // End Processing
343
                  callback();
5✔
344
                  return;
5✔
345
                }
346
              }
347

348
              saveLotameCache(KEY_EXPIRY, responseObj.expiry_ts, responseObj.expiry_ts);
25✔
349

350
              if (isStr(responseObj.profile_id)) {
25✔
351
                if (hasNoConsentErrors) {
19✔
352
                  setProfileId(responseObj.profile_id);
16✔
353
                }
354

355
                if (isStr(responseObj.core_id)) {
19✔
356
                  saveLotameCache(
16✔
357
                    KEY_ID,
358
                    responseObj.core_id,
359
                    responseObj.expiry_ts
360
                  );
361
                  coreId = responseObj.core_id;
16✔
362
                } else {
363
                  clearLotameCache(KEY_ID);
3✔
364
                }
365
              } else {
366
                if (hasNoConsentErrors) {
6✔
367
                  clearLotameCache(KEY_PROFILE);
3✔
368
                }
369
                clearLotameCache(KEY_ID);
6✔
370
              }
371
            } catch (error) {
372
              logError(error);
×
373
            }
374
          }
375
          callback(coreId);
29✔
376
        },
377
        undefined,
378
        {
379
          method: 'GET',
380
          withCredentials: true,
381
        }
382
      );
383
    };
384

385
    return { callback: resolveIdFunction };
34✔
386
  },
387
  eids: {
388
    lotamePanoramaId: {
389
      source: 'crwdcntrl.net',
390
      atype: 1,
391
    },
392
  },
393
};
394

395
submodule('userId', lotamePanoramaIdSubmodule);
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