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

prebid / Prebid.js / 19122650799

06 Nov 2025 02:17AM UTC coverage: 96.228% (-0.008%) from 96.236%
19122650799

push

github

web-flow
Medianet Analytics Adapter: pass ext from Prebid Server Response and Prebid 10 Updates (#13825)

* Pass ext values from Prebid Server bid response to bidderRequest and use pbjs.getUserIds instead of bid.userid

* Test cases added to verify Prebid Server ext response for an Auction

* Store Prebid Server Ext Fields in pbsExt and test case updated

* fix(lint): resolve linting issues causing pipeline failure

52738 of 64615 branches covered (81.62%)

58 of 58 new or added lines in 3 files covered. (100.0%)

104 existing lines in 13 files now uncovered.

201791 of 209700 relevant lines covered (96.23%)

124.96 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