diff --git a/allowedModules.js b/allowedModules.js index e66b8e24098..142bb4b6c4d 100644 --- a/allowedModules.js +++ b/allowedModules.js @@ -1,4 +1,3 @@ - const sharedWhiteList = [ "core-js/library/fn/array/find", // no ie11 "core-js/library/fn/array/includes", // no ie11 @@ -12,7 +11,8 @@ module.exports = { 'modules': [ ...sharedWhiteList, 'jsencrypt', - 'crypto-js' + 'crypto-js', + 'ulid' ], 'src': [ ...sharedWhiteList, diff --git a/integrationExamples/gpt/liveConnect_example.html b/integrationExamples/gpt/liveConnect_example.html new file mode 100644 index 00000000000..84a57158c78 --- /dev/null +++ b/integrationExamples/gpt/liveConnect_example.html @@ -0,0 +1,70 @@ + + + + + + + + + + LiveConnect + + + +

Prebid.js LiveConnect

+ + diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 1e2a6648145..a89dcce9673 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -376,6 +376,7 @@ export function setConsentConfig(config) { } if (!addedConsentHook) { $$PREBID_GLOBAL$$.requestBids.before(requestBidsHook, 50); + if ($$PREBID_GLOBAL$$.liveConnectHook) $$PREBID_GLOBAL$$.liveConnectHook.before(requestBidsHook, 50); } addedConsentHook = true; } diff --git a/modules/liveConnect.js b/modules/liveConnect.js new file mode 100644 index 00000000000..c7a1a07e7cf --- /dev/null +++ b/modules/liveConnect.js @@ -0,0 +1,395 @@ +/** + * This module adds tracking support for LiveIntent's LiveConnect + * @module modules/liveConnect + */ + +/** + * @typedef {Object} LiveConnectStorageConfig + * @property {string} type - specifies where to store the liveConnect identifier. Allowed values: "cookie" or "html5" (local storage). Default: "cookie" + * @property {number} expires - number of days to store the liveConnect identifier. Default: 30 + */ + +/** + * @typedef {Object} LiveConnectConfig + * @property {(string[]|undefined)} scrapedIdentifiers - cookie names or local storage item names to be sent along with the tracking request + * @property {(string|undefined)} providedFirstPartyIdentifier - the cookie name or the local storage item name of the provided first party identifier + * @property {LiveConnectStorageConfig} storage - specifies way to store the liveConnect identifier + */ + +/** + * @typedef {Object} NotValidatedLiveConnectStorageConfig + * @property {string|undefined} type - specifies where to store the liveConnect identifier. Allowed values: "cookie" or "html5" (local storage) + * @property {string|undefined} name - specifies the name of the liveConnect identifier + * @property {number|undefined} expires - number of days to store the liveConnect identifier + */ + +/** + * @typedef {Object} NotValidatedLiveConnectConfig + * @property {(string[]|undefined)} scrapedIdentifiers - cookie names or local storage item names to be sent along with the tracking request + * @property {(string|undefined)} providedFirstPartyIdentifier - the cookie name or the local storage item name of the provided first party identifier + * @property {NotValidatedLiveConnectStorageConfig} storage - specifies way to store the liveConnect identifier + */ + +import {hook} from '../src/hook'; +import {config} from '../src/config'; +import * as utils from '../src/utils'; +import {gdprDataHandler} from '../src/adapterManager'; +import $$PREBID_GLOBAL$$ from '../src/prebid'; +import {hasGDPRConsent} from './userId/gdprUtils'; +import {detectPrng, factory} from 'ulid/dist/index'; + +const prng = detectPrng(true); +/** Generator of ULID identifiers. Falls back to insecure `Math.random` identifiers when `window.crypto` is not present in browser. */ +const ulid = factory(prng); + +const COOKIE = 'cookie'; +const LOCAL_STORAGE = 'html5'; +const LIVE_PIXEL_URL = 'https://rp.liadm.com/p'; +const DUID_NAME = '_lc2_duid'; +const SHARED_DUID_NAME = '_li_duid'; + +/** @type {LiveConnectConfig} */ +const CONFIG = { + ADDITIONAL_USER_IDENTIFIERS: 'additionalUserIdentifiers', + USER_IDENTIFIER: 'userIdentifier', + STORAGE: { + TYPE: { + KEY: 'type', + ALLOWED: [COOKIE, LOCAL_STORAGE], + DEFAULT: COOKIE + }, + EXPIRES: { + KEY: 'expires', + DEFAULT: 730 + } + } +}; + +// init the liveConnect hook to happen before the `requestBids` hook +init(); + +/** + * Adds the liveConnect hook to happen before the `requestBids` hook and after the `consentManagement` hook + */ +export function init() { + // priority value 40 will load after consentManagement with a priority of 50 + $$PREBID_GLOBAL$$.requestBids.before(liveConnectHook, 40); +} + +/** + * The global function to trigger the liveConnect pixel call. + */ +$$PREBID_GLOBAL$$.liveConnect = function () { + $$PREBID_GLOBAL$$.liveConnectHook({}); +}; + +/** + * The liveConnect hook that triggers a pixel call. Happens either after `$$PREBID_GLOBAL$$.liveConnect` or `$$PREBID_GLOBAL$$.requestBids` + */ +$$PREBID_GLOBAL$$.liveConnectHook = hook('async', function () { + liveConnectHook(); +}, 'liveConnect'); + +/** + * The flag that is used to ensure that pixel is called only once per script load. + * @type {boolean} + */ +let isPixelFired = false; + +/** + * Resets the flag to enable multiple calls to liveConnect pixel. This function is used in tests. + */ +export function resetPixel() { + isPixelFired = false; +} + +/** + * This function sends the liveConnect pixel call. + * It does not send the pixel call if the pixel has already been triggered or the gdpr consent has not been given. + */ +function liveConnectHook() { + if (!isPixelFired) { + if (hasGDPRConsent(gdprDataHandler.getConsentData())) { + const duid = sendLiveConnectPixel(); + trySettingSharedDuid(duid); + } + isPixelFired = true; + } +} + +/** + * This function sends the liveConnect pixel call. + */ +function sendLiveConnectPixel() { + const validConfig = validateConfig(config.getConfig('liveConnect')); + const pageUrl = encodeURIComponent(getPageUrl()); + const duid = getDuid(validConfig); + let pixelUri = `${LIVE_PIXEL_URL}?duid=${duid}&tna=$prebid.version$&pu=${pageUrl}`; + + const providedFpiQueryParams = getProvidedFpiQueryParams(validConfig); + if (providedFpiQueryParams) { + pixelUri += `&${providedFpiQueryParams}`; + } + + const scrapedIdentifiersQeuryParams = getScrapedIdentifiers(validConfig); + if (scrapedIdentifiersQeuryParams) { + pixelUri += `&${scrapedIdentifiersQeuryParams}`; + } + + const legacyDuidQueryParam = getLegacyDuidQueryParam(); + if (legacyDuidQueryParam) { + pixelUri += `&${legacyDuidQueryParam}`; + } + + utils.triggerPixel(pixelUri); + + return duid; +} + +/** + * Validates the liveConnect config. Sets the config values to be default values when they are not provided or invalid. + * @param {NotValidatedLiveConnectConfig} config + * @returns {LiveConnectConfig} + */ +function validateConfig(config) { + const validConfig = { + storage: {} + }; + validConfig.storage[CONFIG.STORAGE.TYPE.KEY] = CONFIG.STORAGE.TYPE.DEFAULT; + validConfig.storage[CONFIG.STORAGE.EXPIRES.KEY] = CONFIG.STORAGE.EXPIRES.DEFAULT; + + if (!config) return validConfig; + + validConfig[CONFIG.ADDITIONAL_USER_IDENTIFIERS] = validOrDefault(config[CONFIG.ADDITIONAL_USER_IDENTIFIERS], isArrayOfStrings, []); + validConfig[CONFIG.USER_IDENTIFIER] = validOrDefault(config[CONFIG.USER_IDENTIFIER], utils.isStr, null); + + if (utils.isPlainObject(config.storage)) { + validConfig.storage[CONFIG.STORAGE.TYPE.KEY] = validOrDefault( + config.storage[CONFIG.STORAGE.TYPE.KEY], + v => utils.isStr(v) && CONFIG.STORAGE.TYPE.ALLOWED.includes(v), + CONFIG.STORAGE.TYPE.DEFAULT + ); + validConfig.storage[CONFIG.STORAGE.EXPIRES.KEY] = validOrDefault(config.storage[CONFIG.STORAGE.EXPIRES.KEY], utils.isNumber, CONFIG.STORAGE.EXPIRES.DEFAULT); + } + return validConfig; +} + +/** + * Validates the given value against the function. + * If the value is valid - the function returns this value. + * If the value is invalid - the function returns the default value. + * @param {*} val value to check + * @param {function} check function that validates the value + * @param {*} defaultVal the default value + * @returns {*} or the default value + */ +function validOrDefault(val, check, defaultVal) { + return check(val) ? val : defaultVal; +} + +/** + * Checks if the argument is an array of strings. + * @param val value to be validated + * @returns {Boolean} true - if the value is an array of string. Else - otherwise + */ +function isArrayOfStrings(val) { + return (utils.isArray(val)) && (val.every(v => utils.isStr(v))); +} + +/** + * The url of the current page + * @returns {string} The current page url or the referrer if the js is executed inside an iFrame + */ +function getPageUrl() { + return utils.inIframe() ? document.referrer : window.location.href; +} + +/** + * Get the stored duid (liveConnect id) value from cookie or local storage. + * Stores the new duid if it has not been stored. Updates the expiration if the duid has been set. + * @param {LiveConnectConfig} validConfig + * @returns {string} duid + */ +function getDuid(validConfig) { + let duid = getStoredDuid(validConfig.storage); + if (!duid) { + duid = ulid(); + } + storeDuid(validConfig.storage, duid); + return duid; +} + +/** + * Get the stored duid (liveConnect id) value from cookie or local storage. + * @param {LiveConnectStorageConfig} storage + * @returns {string} duid + */ +function getStoredDuid(storage) { + let storedValue; + try { + if (storage[CONFIG.STORAGE.TYPE.KEY] === COOKIE) { + storedValue = utils.getCookie(DUID_NAME); + } else if (storage[CONFIG.STORAGE.TYPE.KEY] === LOCAL_STORAGE) { + storedValue = getFromLocalStorage(DUID_NAME); + } + } catch (e) { + utils.logError(e); + } + return storedValue; +} + +/** + * Get a value from local storage by name if it is not expired + * @param {string} name local storage item name + * @returns {string} + */ +function getFromLocalStorage(name) { + const storedValueExp = localStorage.getItem(`${name}_exp`); + let storedValue; + // empty string means no expiration set + if (storedValueExp === '') { + storedValue = localStorage.getItem(name); + } else if (storedValueExp) { + if ((new Date(storedValueExp)).getTime() - Date.now() > 0) { + storedValue = decodeURIComponent(localStorage.getItem(name)); + } + } + return storedValue; +} + +/** + * Stores the duid into cookie or local storage + * @param {LiveConnectStorageConfig} storage + * @param {string} value duid value + */ +function storeDuid(storage, value) { + try { + const expiresStr = expiresString(storage[CONFIG.STORAGE.EXPIRES.KEY]); + if (storage[CONFIG.STORAGE.TYPE.KEY] === COOKIE) { + storeCookieOnEtldPlus1(DUID_NAME, value, expiresStr); + } else if (storage[CONFIG.STORAGE.TYPE.KEY] === LOCAL_STORAGE) { + localStorage.setItem(`${DUID_NAME}_exp`, expiresStr); + localStorage.setItem(DUID_NAME, encodeURIComponent(value)); + } + } catch (error) { + utils.logError(error); + } +} + +/** + * UTC formatted string of the expiration date + * @param {number} expirationDays ttl of the cookie/local storage item + * @returns {string} formatted date + */ +function expiresString(expirationDays) { + let expiration = new Date(); + expiration.setDate(expiration.getDate() + expirationDays); + return expiration.toUTCString(); +} + +/** + * Stores the cookie on the apex domain. + * @param {string} name cookie name + * @param {string} value cookie value + * @param {string} expiresStr UTC formatted expiration date string + */ +function storeCookieOnEtldPlus1(name, value, expiresStr) { + const hostParts = window.location.hostname.split('.'); + if (!utils.cookiesAreEnabled()) { + return; + } + for (let i = hostParts.length - 1; i >= 0; i--) { + let domain = '.' + hostParts.slice(i).join('.'); + utils.setCookie(name, value, expiresStr, 'Lax', domain); + const storedCookie = utils.getCookie(name); + if (storedCookie === value) { + return; + } + } +} + +/** + * Gets pixel query params that contains first party identifiers. + * @param {LiveConnectConfig} validConfig + * @returns {string|undefined} concatenated query params + */ +function getProvidedFpiQueryParams(validConfig) { + let fpi; + if (validConfig[CONFIG.USER_IDENTIFIER]) { + const providedFirstPartyIdentifier = getFromCookieOrLocalStorage(validConfig[CONFIG.USER_IDENTIFIER]); + if (providedFirstPartyIdentifier) { + fpi = `pfpi=${providedFirstPartyIdentifier}&fpn=${validConfig[CONFIG.USER_IDENTIFIER]}`; + } + } + return fpi; +} + +/** + * Reads an identifier from cookie or local storage. + * @param {string} identifierName name of the identifier + * @returns {string | null} identifier value + */ +function getFromCookieOrLocalStorage(identifierName) { + let identifierValue = utils.getCookie(identifierName); + if (!identifierValue) { + identifierValue = localStorage.getItem(identifierName); + } + return identifierValue; +} + +/** + * Gets pixel query params that contain scraped identifiers. + * @param {LiveConnectConfig} validConfig + * @returns {string|null} concatenated query params + */ +function getScrapedIdentifiers(validConfig) { + let identifiers; + if (validConfig[CONFIG.ADDITIONAL_USER_IDENTIFIERS]) { + identifiers = validConfig[CONFIG.ADDITIONAL_USER_IDENTIFIERS] + .map(identifierName => { + let identifierValue = getFromCookieOrLocalStorage(identifierName); + return identifierValue ? `ext_${identifierName}=${identifierValue}` : ''; + }) + .filter(param => param && param.length > 0) + .join('&'); + } + + return identifiers && identifiers.length > 0 ? identifiers : null; +} + +/** + * Gets pixel query param that contains a legacy duid. + * @returns {string} query param + */ +function getLegacyDuidQueryParam() { + let queryParam = ''; + const legacyDuid = getLegacyDuid(); + if (legacyDuid) { + queryParam = `lduid=${legacyDuid}`; + } + return queryParam; +} + +/** + * Gets a value from _li_duid local storage item. If it contains uuid, then this is the legacy duid + * @returns {string|undefined} Legacy duid + */ +function getLegacyDuid() { + const duid = localStorage.getItem(SHARED_DUID_NAME); + let legacyDuid; + if (duid && duid.includes('-')) { + legacyDuid = duid; + } + return legacyDuid; +} + +/** + * Update _li_duid value with the ulid duid if the previous value is not a legacy duid + * @param {string} duid + */ +function trySettingSharedDuid(duid) { + const legacyDuid = getLegacyDuid(); + if (!legacyDuid) { + localStorage.setItem(SHARED_DUID_NAME, duid); + } +} diff --git a/modules/liveConnect.md b/modules/liveConnect.md new file mode 100644 index 00000000000..3116811f871 --- /dev/null +++ b/modules/liveConnect.md @@ -0,0 +1,53 @@ +# Overview + +**Module Name**: LiveConnect +**Module Type**: Tracker +**Maintainer**: dev-berlin@liveintent.com + +# Description + +Send a tracking pixel request to LiveConnect pixel service. + +# Usage +To trigger the pixel, call `pbjs.liveConnect()`. +Make sure that `pbjs.setConfig` is done before the pixel call. +```javascript +pbjs.que.push(function () { + pbjs.setConfig({}); + pbjs.liveConnect(); +}); +``` +Alternatively, the pixel is also triggered upon `pbjs.requestBids` +```javascript +pbjs.que.push(function () { + pbjs.setConfig({}); + pbjs.requestBids({}); +}); +``` + +# Example configuration +Example showing configuration for scraped identifiers and provided identifier. +LiveConnect identifier has a changed name and expiration. +```javascript +pbjs.setConfig({ + liveConnect: { + userIdentifier: "pubcid", + additionalUserIdentifiers: ["_parrable_eid"], + storage: { + type: "cookie", + expires: 60 + } + } +}); +``` +Example showing configuration to store identifiers in local storage. +```javascript +pbjs.setConfig({ + liveConnect: { + storage: { + type: "html5", + expires: 60 + } + } +}); +``` diff --git a/modules/userId/gdprUtils.js b/modules/userId/gdprUtils.js new file mode 100644 index 00000000000..b622d9e304b --- /dev/null +++ b/modules/userId/gdprUtils.js @@ -0,0 +1,16 @@ +/** + * test if consent module is present, applies, and is valid for local storage or cookies (purpose 1) + * @param {ConsentData} consentData + * @returns {boolean} + */ +export function hasGDPRConsent(consentData) { + if (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) { + if (!consentData.consentString) { + return false; + } + if (consentData.vendorData && consentData.vendorData.purposeConsents && consentData.vendorData.purposeConsents['1'] === false) { + return false; + } + } + return true; +} diff --git a/modules/userId/index.js b/modules/userId/index.js index ac96fd2cec8..5ec21fad973 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -106,6 +106,7 @@ import CONSTANTS from '../../src/constants.json'; import {module} from '../../src/hook'; import {unifiedIdSubmodule} from './unifiedIdSystem.js'; import {pubCommonIdSubmodule} from './pubCommonIdSystem.js'; +import {hasGDPRConsent} from './gdprUtils'; const MODULE_NAME = 'User ID'; const COOKIE = 'cookie'; @@ -199,23 +200,6 @@ function getStoredValue(storage, key = undefined) { return storedValue; } -/** - * test if consent module is present, applies, and is valid for local storage or cookies (purpose 1) - * @param {ConsentData} consentData - * @returns {boolean} - */ -function hasGDPRConsent(consentData) { - if (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) { - if (!consentData.consentString) { - return false; - } - if (consentData.vendorData && consentData.vendorData.purposeConsents && consentData.vendorData.purposeConsents['1'] === false) { - return false; - } - } - return true; -} - /** * @param {SubmoduleContainer[]} submodules * @param {function} cb - callback for after processing is done. diff --git a/package-lock.json b/package-lock.json index c7256dd8e2d..11c1b393311 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "2.35.0-pre", + "version": "2.38.0-pre", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3397,29 +3397,51 @@ "dev": true }, "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "dev": true, "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^4.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" } } } @@ -6174,9 +6196,9 @@ }, "dependencies": { "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", "dev": true } } @@ -6411,7 +6433,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -6432,12 +6455,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6452,17 +6477,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -6579,7 +6607,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -6591,6 +6620,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6605,6 +6635,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6612,12 +6643,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -6636,6 +6669,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -6716,7 +6750,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -6728,6 +6763,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -6813,7 +6849,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -6849,6 +6886,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6868,6 +6906,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6911,12 +6950,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -8624,12 +8665,6 @@ "loose-envify": "^1.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, "ipaddr.js": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", @@ -9679,15 +9714,6 @@ "readable-stream": "^2.0.5" } }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, "lcov-parse": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", @@ -10167,15 +10193,6 @@ "kind-of": "^6.0.2" } }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -10366,25 +10383,6 @@ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - }, - "dependencies": { - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - } - } - }, "memoizee": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz", @@ -10598,12 +10596,6 @@ "mime-db": "1.40.0" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -11414,17 +11406,6 @@ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -11437,12 +11418,6 @@ "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", "dev": true }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -11566,9 +11541,9 @@ } }, "mocha": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.0.tgz", - "integrity": "sha512-qwfFgY+7EKAAUAdv7VYMZQknI7YJSGesxHyhn6qD52DV8UcSZs5XwCifcZGMVIE4a5fbmhvbotxC0DLQ0oKohQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.2.tgz", + "integrity": "sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A==", "dev": true, "requires": { "ansi-colors": "3.2.3", @@ -11591,9 +11566,9 @@ "supports-color": "6.0.0", "which": "1.3.1", "wide-align": "1.1.3", - "yargs": "13.2.2", - "yargs-parser": "13.0.0", - "yargs-unparser": "1.5.0" + "yargs": "13.3.0", + "yargs-parser": "13.1.1", + "yargs-unparser": "1.6.0" } }, "ms": { @@ -11632,22 +11607,21 @@ } }, "yargs": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", - "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", "dev": true, "requires": { - "cliui": "^4.0.0", + "cliui": "^5.0.0", "find-up": "^3.0.0", "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" + "yargs-parser": "^13.1.1" } } } @@ -14054,6 +14028,21 @@ } } }, + "tldjs": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tldjs/-/tldjs-2.3.1.tgz", + "integrity": "sha512-W/YVH/QczLUxVjnQhFC61Iq232NWu3TqDdO0S/MtXVz4xybejBov4ud+CIwN9aYqjOecEqIy0PscGkwpG9ZyTw==", + "requires": { + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -14355,6 +14344,11 @@ } } }, + "ulid": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz", + "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==" + }, "ultron": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", @@ -16226,9 +16220,9 @@ "dev": true }, "yargs-parser": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", - "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -16236,56 +16230,58 @@ } }, "yargs-unparser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", - "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", "dev": true, "requires": { "flat": "^4.1.0", - "lodash": "^4.17.11", - "yargs": "^12.0.5" + "lodash": "^4.17.15", + "yargs": "^13.3.0" }, "dependencies": { - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } }, "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", "dev": true, "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", + "cliui": "^5.0.0", "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", + "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", + "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^2.0.0", + "string-width": "^3.0.0", "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" } } } diff --git a/package.json b/package.json index 9e5effb8627..6bb412352a2 100755 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "dset": "2.0.1", "fun-hooks": "^0.9.5", "jsencrypt": "^3.0.0-rc.1", - "just-clone": "^1.0.2" + "just-clone": "^1.0.2", + "ulid": "^2.3.0" } } diff --git a/src/utils.js b/src/utils.js index 2caaedc4164..1eff91dcc1d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -918,8 +918,8 @@ export function getCookie(name) { return m ? decodeURIComponent(m[2]) : null; } -export function setCookie(key, value, expires, sameSite) { - document.cookie = `${key}=${encodeURIComponent(value)}${(expires !== '') ? `; expires=${expires}` : ''}; path=/${sameSite ? `; SameSite=${sameSite}` : ''}`; +export function setCookie(key, value, expires, sameSite, domain) { + document.cookie = `${key}=${encodeURIComponent(value)}${(expires !== '') ? `; expires=${expires}` : ''}; path=/${sameSite ? `; SameSite=${sameSite}` : ''}${domain ? `; domain=${domain}` : ''}`; } /** diff --git a/test/spec/modules/liveConnect_spec.js b/test/spec/modules/liveConnect_spec.js new file mode 100644 index 00000000000..bd7d231f3e6 --- /dev/null +++ b/test/spec/modules/liveConnect_spec.js @@ -0,0 +1,275 @@ +import * as liveConnect from 'modules/liveConnect'; +import {config} from 'src/config'; +import * as utils from 'src/utils'; +import {resetConsentData} from 'modules/consentManagement'; + +const expect = require('chai').expect; + +const USER_IDENTIFIER_NAME = 'testPfpi'; +const USER_IDENTIFIER_VALUE = 'testPfpiValue'; +const ADDITIONAL_IDENTIFIER_NAME = 'testIdentifier'; +const ADDITIONAL_IDENTIFIER_VALUE = 'testIdentifierValue'; +const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; +const BASIC_PIXEL_CALL = 'https://rp\\.liadm\\.com/p\\?duid=[0-9A-Z]{26}&tna=$prebid.version$&pu=http%3A%2F%2Flocalhost%3A[0-9]{4}%2F%3Fid%3D[0-9]+'; + +function liveConnectConfig(config) { + return { + liveConnect: config + } +} + +describe('LiveConnect', () => { + beforeEach(function () { + liveConnect.init(); + sinon.stub(utils, 'triggerPixel'); + sinon.spy(utils, 'setCookie'); + sinon.spy(localStorage, 'setItem'); + }); + + afterEach(function () { + $$PREBID_GLOBAL$$.requestBids.removeAll(); + config.resetConfig(); + utils.setCookie(USER_IDENTIFIER_NAME, '', EXPIRED_COOKIE_DATE); + utils.setCookie(ADDITIONAL_IDENTIFIER_NAME, '', EXPIRED_COOKIE_DATE); + localStorage.removeItem(USER_IDENTIFIER_NAME); + localStorage.removeItem(ADDITIONAL_IDENTIFIER_NAME); + localStorage.removeItem('_li_duid'); + liveConnect.resetPixel(); + utils.triggerPixel.restore(); + utils.setCookie.restore(); + localStorage.setItem.restore(); + resetConsentData(); + }); + + describe('identifier', () => { + it('should be set to cookie', () => { + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.setCookie.callCount).to.equal(1); + expect(utils.setCookie.getCall(0).args[0]).to.exist.and.to.equal('_lc2_duid'); + }); + + it('should be set to cookie and shared local storage identifier', () => { + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.setCookie.callCount).to.equal(1); + expect(utils.setCookie.getCall(0).args[0]).to.exist.and.to.equal('_lc2_duid'); + expect(localStorage.setItem.callCount).to.equal(1); + expect(localStorage.setItem.getCall(0).args[0]).to.exist.and.to.equal('_li_duid'); + }); + + it('should be set to cookie but not set to shared local storage identifier when there is a legady duid in shared local storage item', () => { + localStorage.setItem.restore(); + localStorage.setItem('_li_duid', 'a-1234--519253bc-469f-43f4-8100-592803e07dc3'); + sinon.spy(localStorage, 'setItem'); + + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.setCookie.callCount).to.equal(1); + expect(utils.setCookie.getCall(0).args[0]).to.exist.and.to.equal('_lc2_duid'); + expect(localStorage.setItem.callCount).to.equal(0); + }); + + it('should be reset to cookie when the duid has been already stored', () => { + utils.setCookie.restore(); + utils.setCookie('_lc2_duid', '01DRV0Z3SYRKV68NFAB40TN3EE', 'Thu, 01 Jan 2030 00:00:01 GMT'); + sinon.spy(utils, 'setCookie'); + + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.setCookie.callCount).to.equal(1); + expect(utils.setCookie.getCall(0).args[0]).to.exist.and.to.equal('_lc2_duid'); + expect(utils.setCookie.getCall(0).args[1]).to.exist.and.to.equal('01DRV0Z3SYRKV68NFAB40TN3EE'); + }); + + it('should be set to cookie when storage type is unknown', () => { + config.setConfig(liveConnectConfig({storage: {type: 'html7'}})); + + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.setCookie.callCount).to.equal(1); + expect(utils.setCookie.getCall(0).args[0]).to.exist.and.to.equal('_lc2_duid'); + }); + + it('should be set to local storage identifier', () => { + config.setConfig(liveConnectConfig({storage: {type: 'html5', expires: 23}})); + + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.setCookie.callCount).to.equal(0); + expect(localStorage.setItem.callCount).to.equal(3); + expect(localStorage.setItem.getCall(0).args[0]).to.exist.and.to.equal('_lc2_duid_exp'); + expect(localStorage.setItem.getCall(1).args[0]).to.exist.and.to.equal('_lc2_duid'); + expect(localStorage.setItem.getCall(2).args[0]).to.exist.and.to.equal('_li_duid'); + }); + + it('should be reset to local storage when the duid has been already stored', () => { + localStorage.setItem.restore(); + localStorage.setItem('_lc2_duid', '01DRV0Z3SYRKV68NFAB40TN3EE'); + sinon.spy(localStorage, 'setItem'); + config.setConfig(liveConnectConfig({storage: {type: 'html5'}})); + + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.setCookie.callCount).to.equal(0); + expect(localStorage.setItem.callCount).to.equal(3); + expect(localStorage.setItem.getCall(0).args[0]).to.exist.and.to.equal('_lc2_duid_exp'); + expect(localStorage.setItem.getCall(1).args[0]).to.exist.and.to.equal('_lc2_duid'); + expect(localStorage.setItem.getCall(1).args[1]).to.exist.and.to.equal('01DRV0Z3SYRKV68NFAB40TN3EE'); + expect(localStorage.setItem.getCall(2).args[0]).to.exist.and.to.equal('_li_duid'); + }); + + it('should be set only once', () => { + $$PREBID_GLOBAL$$.liveConnect(); + $$PREBID_GLOBAL$$.requestBids({}); + + expect(utils.setCookie.callCount).to.equal(1); + }); + + it('should not be set when consent is not given', () => { + config.setConfig({ + consentManagement: { + cmpApi: 'static', + timeout: 7500, + allowAuctionWithoutConsent: false, + consentData: { + getConsentData: { + 'gdprApplies': true, + 'consentData': 'BOOgjO9OOgjO9APABAENAi-AAAAWd7_______9____7_9uz_Gv_r_ff_3nW0739P1A_r_Oz_rm_-zzV44_lpQQRCEA' + }, + getVendorConsents: { + 'purposeConsents': { + '1': false + } + } + } + } + }); + + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.setCookie.callCount).to.equal(0); + expect(localStorage.setItem.callCount).to.equal(0); + }); + }); + + describe('pixel call', () => { + it('should be sent to pixel endpoint', () => { + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.triggerPixel.callCount).to.equal(1); + expect(utils.triggerPixel.getCall(0).args[0]).to.exist.and.to.match(new RegExp(BASIC_PIXEL_CALL)); + }); + + it('should be sent to pixel endpoint when bid is performed', () => { + $$PREBID_GLOBAL$$.requestBids({}); + + expect(utils.triggerPixel.callCount).to.equal(1); + expect(utils.triggerPixel.getCall(0).args[0]).to.exist.and.to.match(new RegExp(BASIC_PIXEL_CALL)); + }); + + it('should be sent to pixel endpoint only once', () => { + $$PREBID_GLOBAL$$.liveConnect(); + $$PREBID_GLOBAL$$.requestBids({}); + + expect(utils.setCookie.callCount).to.equal(1); + }); + + it('should be sent to pixel endpoint when the legacy duid is set into local storage', () => { + localStorage.setItem('_li_duid', 'a-1234--519253bc-469f-43f4-8100-592803e07dc3'); + + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.triggerPixel.callCount).to.equal(1); + expect(utils.triggerPixel.getCall(0).args[0]).to.exist.and.to.match(new RegExp( + BASIC_PIXEL_CALL + '&lduid=a-1234--519253bc-469f-43f4-8100-592803e07dc3' + )); + }); + + it('should be sent to pixel endpoint when the user identifier is set into cookie', () => { + utils.setCookie(USER_IDENTIFIER_NAME, USER_IDENTIFIER_VALUE, (new Date(Date.now() + 60000).toUTCString())); + config.setConfig(liveConnectConfig({userIdentifier: USER_IDENTIFIER_NAME})); + + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.triggerPixel.callCount).to.equal(1); + expect(utils.triggerPixel.getCall(0).args[0]).to.exist.and.to.match(new RegExp( + BASIC_PIXEL_CALL + `&pfpi=${USER_IDENTIFIER_VALUE}&fpn=${USER_IDENTIFIER_NAME}` + )); + }); + + it('should be sent to pixel endpoint when the user identifier is set into local storage', () => { + localStorage.setItem(USER_IDENTIFIER_NAME, USER_IDENTIFIER_VALUE); + config.setConfig(liveConnectConfig({userIdentifier: USER_IDENTIFIER_NAME})); + + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.triggerPixel.callCount).to.equal(1); + expect(utils.triggerPixel.getCall(0).args[0]).to.exist.and.to.match(new RegExp( + BASIC_PIXEL_CALL + `&pfpi=${USER_IDENTIFIER_VALUE}&fpn=${USER_IDENTIFIER_NAME}` + )); + }); + + it('should be sent to pixel endpoint when the additional identifier is set into cookie', () => { + utils.setCookie(ADDITIONAL_IDENTIFIER_NAME, ADDITIONAL_IDENTIFIER_VALUE, (new Date(Date.now() + 60000).toUTCString())); + config.setConfig(liveConnectConfig({additionalUserIdentifiers: [ADDITIONAL_IDENTIFIER_NAME]})); + + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.triggerPixel.callCount).to.equal(1); + expect(utils.triggerPixel.getCall(0).args[0]).to.exist.and.to.match(new RegExp( + BASIC_PIXEL_CALL + `&ext_${ADDITIONAL_IDENTIFIER_NAME}=${ADDITIONAL_IDENTIFIER_VALUE}` + )); + }); + + it('should be sent to pixel endpoint when the additional identifier is set into local storage', () => { + localStorage.setItem(ADDITIONAL_IDENTIFIER_NAME, ADDITIONAL_IDENTIFIER_VALUE); + config.setConfig(liveConnectConfig({additionalUserIdentifiers: [ADDITIONAL_IDENTIFIER_NAME]})); + + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.triggerPixel.callCount).to.equal(1); + expect(utils.triggerPixel.getCall(0).args[0]).to.exist.and.to.match(new RegExp( + BASIC_PIXEL_CALL + `&ext_${ADDITIONAL_IDENTIFIER_NAME}=${ADDITIONAL_IDENTIFIER_VALUE}` + )); + }); + + it('should be sent to pixel endpoint when the additional identifier and user identifier are set', () => { + localStorage.setItem(ADDITIONAL_IDENTIFIER_NAME, ADDITIONAL_IDENTIFIER_VALUE); + localStorage.setItem(USER_IDENTIFIER_NAME, USER_IDENTIFIER_VALUE); + config.setConfig(liveConnectConfig({userIdentifier: USER_IDENTIFIER_NAME, additionalUserIdentifiers: [ADDITIONAL_IDENTIFIER_NAME]})); + + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.triggerPixel.callCount).to.equal(1); + expect(utils.triggerPixel.getCall(0).args[0]).to.exist.and.to.match(new RegExp( + BASIC_PIXEL_CALL + `&pfpi=${USER_IDENTIFIER_VALUE}&fpn=${USER_IDENTIFIER_NAME}&ext_${ADDITIONAL_IDENTIFIER_NAME}=${ADDITIONAL_IDENTIFIER_VALUE}` + )); + }); + + it('should not be sent to pixel endpoint when consent is not given', () => { + config.setConfig({ + consentManagement: { + cmpApi: 'static', + timeout: 7500, + allowAuctionWithoutConsent: false, + consentData: { + getConsentData: { + 'gdprApplies': true, + 'consentData': 'BOOgjO9OOgjO9APABAENAi-AAAAWd7_______9____7_9uz_Gv_r_ff_3nW0739P1A_r_Oz_rm_-zzV44_lpQQRCEA' + }, + getVendorConsents: { + 'purposeConsents': { + '1': false + } + } + } + } + }); + + $$PREBID_GLOBAL$$.liveConnect(); + + expect(utils.triggerPixel.callCount).to.equal(0); + }); + }); +});