• 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

91.67
/modules/connectIdSystem.js
1
/**
1✔
2
 * This module adds support for Yahoo ConnectID to the user ID module system.
3
 * The {@link module:modules/userId} module is required
4
 * @module modules/connectIdSystem
5
 * @requires module:modules/userId
6
 */
7

8
import {ajax} from '../src/ajax.js';
9
import {submodule} from '../src/hook.js';
10
import {includes} from '../src/polyfill.js';
11
import {getRefererInfo} from '../src/refererDetection.js';
12
import {getStorageManager} from '../src/storageManager.js';
13
import {formatQS, isNumber, isPlainObject, logError, parseUrl} from '../src/utils.js';
14
import {MODULE_TYPE_UID} from '../src/activities/modules.js';
15

16
/**
17
 * @typedef {import('../modules/userId/index.js').Submodule} Submodule
18
 * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig
19
 * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData
20
 * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse
21
 */
22

23
const MODULE_NAME = 'connectId';
1✔
24
const STORAGE_EXPIRY_DAYS = 365;
1✔
25
const STORAGE_DURATION = 60 * 60 * 24 * 1000 * STORAGE_EXPIRY_DAYS;
1✔
26
const ID_EXPIRY_DAYS = 14;
1✔
27
const VALID_ID_DURATION = 60 * 60 * 24 * 1000 * ID_EXPIRY_DAYS;
1✔
28
const PUID_EXPIRY_DAYS = 30;
1✔
29
const PUID_EXPIRY = 60 * 60 * 24 * 1000 * PUID_EXPIRY_DAYS;
1✔
30
const VENDOR_ID = 25;
1✔
31
const PLACEHOLDER = '__PIXEL_ID__';
1✔
32
const UPS_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PLACEHOLDER}/fed`;
1✔
33
const OVERRIDE_OPT_OUT_KEY = 'connectIdOptOut';
1✔
34
const INPUT_PARAM_KEYS = ['pixelId', 'he', 'puid'];
1✔
35
const O_AND_O_DOMAINS = [
1✔
36
  'yahoo.com',
37
  'aol.com',
38
  'aol.ca',
39
  'aol.de',
40
  'aol.co.uk',
41
  'engadget.com',
42
  'techcrunch.com',
43
  'autoblog.com',
44
];
45
export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME});
1✔
46

47
/**
48
 * @function
49
 * @param {Object} obj
50
 */
51
function storeObject(obj) {
52
  const expires = Date.now() + STORAGE_DURATION;
8✔
53
  if (storage.cookiesAreEnabled()) {
8✔
54
    setEtldPlusOneCookie(MODULE_NAME, JSON.stringify(obj), new Date(expires), getSiteHostname());
7✔
55
  }
56
  if (storage.localStorageIsEnabled()) {
8!
57
    storage.setDataInLocalStorage(MODULE_NAME, JSON.stringify(obj));
8✔
58
  }
59
}
60

61
/**
62
 * Attempts to store a cookie on eTLD + 1
63
 *
64
 * @function
65
 * @param {String} key
66
 * @param {String} value
67
 * @param {Date} expirationDate
68
 * @param {String} hostname
69
 */
70
function setEtldPlusOneCookie(key, value, expirationDate, hostname) {
71
  const subDomains = hostname.split('.');
11✔
72
  for (let i = 0; i < subDomains.length; ++i) {
11✔
73
    const domain = subDomains.slice(subDomains.length - i - 1, subDomains.length).join('.');
11✔
74
    try {
11✔
75
      storage.setCookie(key, value, expirationDate.toUTCString(), null, '.' + domain);
11✔
76
      const storedCookie = storage.getCookie(key);
11✔
77
      if (storedCookie && storedCookie === value) {
11!
78
        break;
×
79
      }
80
    } catch (error) {}
81
  }
82
}
83

84
function getIdFromCookie() {
85
  if (storage.cookiesAreEnabled()) {
41✔
86
    try {
40✔
87
      return JSON.parse(storage.getCookie(MODULE_NAME));
40✔
88
    } catch {}
89
  }
90
  return null;
26✔
91
}
92

93
function getIdFromLocalStorage() {
94
  if (storage.localStorageIsEnabled()) {
30!
95
    let storedIdData = storage.getDataFromLocalStorage(MODULE_NAME);
30✔
96
    if (storedIdData) {
30✔
97
      try {
9✔
98
        storedIdData = JSON.parse(storedIdData);
9✔
99
      } catch (e) {
100
        logError(`${MODULE_NAME} module: error while reading the local storage data.`);
7✔
101
      }
102
      if (isPlainObject(storedIdData) && storedIdData.__expires &&
9✔
103
          storedIdData.__expires <= Date.now()) {
104
        storage.removeDataFromLocalStorage(MODULE_NAME);
1✔
105
        return null;
1✔
106
      }
107
      return storedIdData;
8✔
108
    }
109
  }
110
  return null;
21✔
111
}
112

113
function syncLocalStorageToCookie() {
114
  if (!storage.cookiesAreEnabled()) {
4!
115
    return;
×
116
  }
117
  const value = getIdFromLocalStorage();
4✔
118
  const newCookieExpireTime = Date.now() + STORAGE_DURATION;
4✔
119
  setEtldPlusOneCookie(MODULE_NAME, JSON.stringify(value), new Date(newCookieExpireTime), getSiteHostname());
4✔
120
}
121

122
function isStale(storedIdData) {
123
  if (isOAndOTraffic()) {
45✔
124
    return true;
1✔
125
  } else if (isPlainObject(storedIdData) && storedIdData.lastSynced) {
44✔
126
    const validTTL = storedIdData.ttl || VALID_ID_DURATION;
11✔
127
    return storedIdData.lastSynced + validTTL <= Date.now();
11✔
128
  }
129
  return false;
33✔
130
}
131

132
function getStoredId() {
133
  let storedId = getIdFromCookie();
41✔
134
  if (!storedId) {
41✔
135
    storedId = getIdFromLocalStorage();
26✔
136
    if (storedId && !isStale(storedId)) {
26✔
137
      syncLocalStorageToCookie();
4✔
138
    }
139
  }
140
  return storedId;
41✔
141
}
142

143
function getSiteHostname() {
144
  const pageInfo = parseUrl(getRefererInfo().page);
11✔
145
  return pageInfo.hostname;
11✔
146
}
147

148
function isOAndOTraffic() {
149
  let referer = getRefererInfo().ref;
45✔
150

151
  if (referer) {
45✔
152
    referer = parseUrl(referer).hostname;
1✔
153
    const subDomains = referer.split('.');
1✔
154
    referer = subDomains.slice(subDomains.length - 2, subDomains.length).join('.');
1✔
155
  }
156
  return O_AND_O_DOMAINS.indexOf(referer) >= 0;
45✔
157
}
158

159
/** @type {Submodule} */
160
export const connectIdSubmodule = {
1✔
161
  /**
162
   * used to link submodule with config
163
   * @type {string}
164
   */
165
  name: MODULE_NAME,
166
  /**
167
   * @type {Number}
168
   */
169
  gvlid: VENDOR_ID,
170
  /**
171
   * decode the stored id value for passing to bid requests
172
   * @function
173
   * @returns {{connectId: string} | undefined}
174
   */
175
  decode(value) {
176
    if (connectIdSubmodule.userHasOptedOut()) {
6✔
177
      return undefined;
1✔
178
    }
179
    return (isPlainObject(value) && (value.connectId || value.connectid))
5✔
180
      ? {connectId: value.connectId || value.connectid} : undefined;
3✔
181
  },
182
  /**
183
   * Gets the Yahoo ConnectID
184
   * @function
185
   * @param {SubmoduleConfig} [config]
186
   * @param {ConsentData} [consentData]
187
   * @returns {IdResponse|undefined}
188
   */
189
  getId(config, consentData) {
190
    if (connectIdSubmodule.userHasOptedOut()) {
44✔
191
      return;
1✔
192
    }
193
    const params = config.params || {};
43!
194
    if (!params ||
43✔
195
        (typeof params.pixelId === 'undefined' && typeof params.endpoint === 'undefined')) {
196
      logError(`${MODULE_NAME} module: configuration requires the 'pixelId'.`);
2✔
197
      return;
2✔
198
    }
199

200
    const storedId = getStoredId();
41✔
201

202
    let shouldResync = isStale(storedId);
41✔
203

204
    if (storedId) {
41✔
205
      if (isPlainObject(storedId) && storedId.puid && storedId.lastUsed && !params.puid &&
19✔
206
        (storedId.lastUsed + PUID_EXPIRY) <= Date.now()) {
207
        delete storedId.puid;
2✔
208
        shouldResync = true;
2✔
209
      }
210
      if ((params.he && params.he !== storedId.he) ||
19✔
211
        (params.puid && params.puid !== storedId.puid)) {
212
        shouldResync = true;
10✔
213
      }
214
      if (!shouldResync) {
19✔
215
        storedId.lastUsed = Date.now();
6✔
216
        storeObject(storedId);
6✔
217
        return {id: storedId};
6✔
218
      }
219
    }
220

221
    const uspString = consentData.usp || '';
35!
222
    const data = {
35✔
223
      v: '1',
224
      '1p': includes([1, '1', true], params['1p']) ? '1' : '0',
35✔
225
      gdpr: connectIdSubmodule.isEUConsentRequired(consentData?.gdpr) ? '1' : '0',
35✔
226
      gdpr_consent: connectIdSubmodule.isEUConsentRequired(consentData?.gdpr) ? consentData.gdpr.consentString : '',
35✔
227
      us_privacy: uspString
228
    };
229

230
    const gppConsent = consentData.gpp;
35✔
231
    if (gppConsent) {
35✔
232
      data.gpp = `${gppConsent.gppString ? gppConsent.gppString : ''}`;
34!
233
      if (Array.isArray(gppConsent.applicableSections)) {
34!
234
        data.gpp_sid = gppConsent.applicableSections.join(',');
34✔
235
      }
236
    }
237

238
    let topmostLocation = getRefererInfo().topmostLocation;
35✔
239
    if (typeof topmostLocation === 'string') {
35✔
240
      data.url = topmostLocation.split('?')[0];
34✔
241
    }
242

243
    INPUT_PARAM_KEYS.forEach(key => {
35✔
244
      if (typeof params[key] != 'undefined') {
105✔
245
        data[key] = params[key];
64✔
246
      }
247
    });
248

249
    const hashedEmail = params.he || storedId?.he;
35✔
250
    if (hashedEmail) {
35✔
251
      data.he = hashedEmail;
30✔
252
    }
253
    if (!data.puid && storedId?.puid) {
35✔
254
      data.puid = storedId.puid;
3✔
255
    }
256

257
    const resp = function (callback) {
35✔
258
      const callbacks = {
35✔
259
        success: response => {
260
          let responseObj;
261
          if (response) {
2!
262
            try {
2✔
263
              responseObj = JSON.parse(response);
2✔
264
              if (isPlainObject(responseObj) && Object.keys(responseObj).length > 0 &&
2!
265
                 (!!responseObj.connectId || !!responseObj.connectid)) {
266
                responseObj.he = params.he;
2✔
267
                responseObj.puid = params.puid || responseObj.puid;
2!
268
                responseObj.lastSynced = Date.now();
2✔
269
                responseObj.lastUsed = Date.now();
2✔
270
                if (isNumber(responseObj.ttl)) {
2!
271
                  let validTTLMiliseconds = responseObj.ttl * 60 * 60 * 1000;
×
272
                  if (validTTLMiliseconds > VALID_ID_DURATION) {
×
273
                    validTTLMiliseconds = VALID_ID_DURATION;
×
274
                  }
275
                  responseObj.ttl = validTTLMiliseconds;
×
276
                }
277
                storeObject(responseObj);
2✔
278
              } else {
279
                logError(`${MODULE_NAME} module: UPS response returned an invalid payload ${response}`);
×
280
              }
281
            } catch (error) {
282
              logError(error);
×
283
            }
284
          }
285
          callback(responseObj);
2✔
286
        },
287
        error: error => {
288
          logError(`${MODULE_NAME} module: ID fetch encountered an error`, error);
×
289
          callback();
×
290
        }
291
      };
292
      const endpoint = UPS_ENDPOINT.replace(PLACEHOLDER, params.pixelId);
35✔
293
      let url = `${params.endpoint || endpoint}?${formatQS(data)}`;
35✔
294
      connectIdSubmodule.getAjaxFn()(url, callbacks, null, {method: 'GET', withCredentials: true});
35✔
295
    };
296
    const result = {callback: resp};
35✔
297
    if (shouldResync && storedId) {
35✔
298
      result.id = storedId;
13✔
299
    }
300

301
    return result;
35✔
302
  },
303

304
  /**
305
   * Utility function that returns a boolean flag indicating if the opportunity
306
   * is subject to GDPR
307
   * @returns {Boolean}
308
   */
309
  isEUConsentRequired(consentData) {
310
    return !!(consentData?.gdprApplies);
74✔
311
  },
312

313
  /**
314
   * Utility function that returns a boolean flag indicating if the user
315
   * has opted out via the Yahoo easy-opt-out mechanism.
316
   * @returns {Boolean}
317
   */
318
  userHasOptedOut() {
319
    try {
47✔
320
      if (storage.localStorageIsEnabled()) {
47!
321
        return storage.getDataFromLocalStorage(OVERRIDE_OPT_OUT_KEY) === '1';
47✔
322
      } else {
323
        return true;
×
324
      }
325
    } catch {
326
      return false;
×
327
    }
328
  },
329

330
  /**
331
   * Return the function used to perform XHR calls.
332
   * Utilised for each of testing.
333
   * @returns {Function}
334
   */
335
  getAjaxFn() {
336
    return ajax;
3✔
337
  },
338
  eids: {
339
    'connectId': {
340
      source: 'yahoo.com',
341
      atype: 3
342
    },
343
  }
344
};
345

346
submodule('userId', connectIdSubmodule);
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