From 41b21190633a7520a58b0098daff642e98134edd Mon Sep 17 00:00:00 2001 From: Peixun Zhang Date: Fri, 16 Aug 2024 12:09:12 +0200 Subject: [PATCH 01/16] CM-1260 Implement LiveIntent Prebid User Id Module Based On The Hub --- .../liveIntentIdHubSystem.js | 162 +++++++ .../liveIntentIdSystem/liveIntentIdSystem.js | 224 ++++++++++ .../liveIntentIdSystemShared.js | 202 +++++++++ modules/liveIntentIdSystem.js | 422 +----------------- .../modules/liveIntentIdHubSystem_spec.js | 363 +++++++++++++++ test/spec/modules/liveIntentIdSystem_spec.js | 2 +- 6 files changed, 962 insertions(+), 413 deletions(-) create mode 100644 libraries/liveIntentIdSystem/liveIntentIdHubSystem.js create mode 100644 libraries/liveIntentIdSystem/liveIntentIdSystem.js create mode 100644 libraries/liveIntentIdSystem/liveIntentIdSystemShared.js create mode 100644 test/spec/modules/liveIntentIdHubSystem_spec.js diff --git a/libraries/liveIntentIdSystem/liveIntentIdHubSystem.js b/libraries/liveIntentIdSystem/liveIntentIdHubSystem.js new file mode 100644 index 00000000000..b8966c4aa90 --- /dev/null +++ b/libraries/liveIntentIdSystem/liveIntentIdHubSystem.js @@ -0,0 +1,162 @@ +import { logError } from '../../src/utils.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../../src/adapterManager.js'; +import { submodule } from '../../src/hook.js'; +import { DEFAULT_AJAX_TIMEOUT, MODULE_NAME, parseRequestedAttributes, composeIdObject, eids, DEFAULT_DELAY, GVLID } from './liveIntentIdSystemShared.js' + +// reference to the client for the liQHub +let cachedClientRef + +/** + * This function is used in tests + */ +export function resetSubmodule() { + cachedClientRef = undefined +} + +window.liQHub = window.liQHub ?? [] + +function initializeClient(configParams) { + // only initialize once + if (cachedClientRef != null) return cachedClientRef + + const clientRef = {} + + const clientDetails = { name: 'prebid', version: '$prebid.version$' } + + const collectConfig = configParams.liCollectConfig ?? {}; + + let integration + if (collectConfig.appId != null) { + integration = { type: 'application', appId: collectConfig.appId, publisherId: configParams.publisherId } + } else if (configParams.distributorId != null) { + integration = { type: 'distributor', distributorId: configParams.distributorId } + } else { + integration = { type: 'custom', publisherId: configParams.publisherId, distributorId: configParams.distributorId } + } + + const partnerCookies = new Set(configParams.identifiersToResolve ?? []); + + const collectSettings = { timeout: collectConfig.ajaxTimeout ?? DEFAULT_AJAX_TIMEOUT } + + let identityPartner + if (configParams.appId == null && configParams.distributorId != null) { + identityPartner = configParams.distributorId + } else if (configParams.partner != null) { + identityPartner = configParams.partner + } else { + identityPartner = 'prebid' + } + + const resolveSettings = { + identityPartner, + timeout: configParams.ajaxTimeout ?? DEFAULT_AJAX_TIMEOUT + } + + function loadConsent() { + const consent = {} + const usPrivacyString = uspDataHandler.getConsentData(); + if (usPrivacyString != null) { + consent.usPrivacy = { consentString: usPrivacyString } + } + const gdprConsent = gdprDataHandler.getConsentData() + if (gdprConsent != null) { + consent.gdpr = gdprConsent + } + const gppConsent = gppDataHandler.getConsentData(); + if (gppConsent != null) { + consent.gpp = { consentString: gppConsent.gppString, applicableSections: gppConsent.applicableSections } + } + + return consent + } + const consent = loadConsent() + + window.liQHub.push({ + type: 'register_client', + clientRef, + clientDetails, + integration, + consent, + partnerCookies, + collectSettings, + resolveSettings + }) + + // fire default collect request + if (configParams.emailHash != null) { + window.liQHub.push({ type: 'collect', clientRef, sourceEvent: { hash: configParams.emailHash } }) + } else { + window.liQHub.push({ type: 'schedule_default_collect', clientRef, delay: configParams.fireEventDelay ?? DEFAULT_DELAY }) + } + + cachedClientRef = clientRef + return clientRef +} + +/** + * Create requestedAttributes array to pass to liveconnect + * @function + * @param {Object} overrides - object with boolean values that will override defaults { 'foo': true, 'bar': false } + * @returns {Array} + */ + +function resolve(configParams, clientRef, callback) { + function onFailure(error) { + logError(`${MODULE_NAME}: ID fetch encountered an error: `, error); + callback(); + } + + const onSuccess = [{ type: 'callback', callback }] + + window.liQHub.push({ + type: 'resolve', + clientRef, + requestedAttributes: parseRequestedAttributes(configParams.requestedAttributesOverrides), + onFailure, + onSuccess + }) +} + +/** + * @typedef {import('../../modules/userId/index.js').Submodule} Submodule + */ + +/** @type {Submodule} */ +export const liveIntentIdHubSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + gvlid: GVLID, + + /** + * decode the stored id value for passing to bid requests + * @function + */ + decode(value, config) { + const configParams = config != null ? config.params : {}; + + // ensure client is initialized and we fired at least one collect request + initializeClient(configParams) + + return composeIdObject(value); + }, + + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + */ + getId(config) { + const configParams = config != null ? config.params : {}; + + const clientRef = initializeClient(configParams) + + return { callback: function(cb) { resolve(configParams, clientRef, cb); } }; + }, + eids: { + eids + } +}; + +submodule('userId', liveIntentIdHubSubmodule); diff --git a/libraries/liveIntentIdSystem/liveIntentIdSystem.js b/libraries/liveIntentIdSystem/liveIntentIdSystem.js new file mode 100644 index 00000000000..7e160cde1b4 --- /dev/null +++ b/libraries/liveIntentIdSystem/liveIntentIdSystem.js @@ -0,0 +1,224 @@ +/** + * This module adds LiveIntentId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/liveIntentIdSystem + * @requires module:modules/userId + */ +import { triggerPixel, logError } from '../../src/utils.js'; +import { ajaxBuilder } from '../../src/ajax.js'; +import { LiveConnect } from 'live-connect-js'; // eslint-disable-line prebid/validate-imports +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../../src/adapterManager.js'; +import {getStorageManager} from '../../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../../src/activities/modules.js'; +import { submodule } from '../../src/hook.js'; +import { DEFAULT_AJAX_TIMEOUT, MODULE_NAME, parseRequestedAttributes, composeIdObject, eids, DEFAULT_DELAY, GVLID } from './liveIntentIdSystemShared.js' + +/** + * @typedef {import('../../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../../modules/userId/index.js').IdResponse} IdResponse + */ + +const EVENTS_TOPIC = 'pre_lips' +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); + +const calls = { + ajaxGet: (url, onSuccess, onError, timeout) => { + ajaxBuilder(timeout)( + url, + { + success: onSuccess, + error: onError + }, + undefined, + { + method: 'GET', + withCredentials: true + } + ) + }, + pixelGet: (url, onload) => triggerPixel(url, onload) +} + +let eventFired = false; +let liveConnect = null; + +/** + * This function is used in tests + */ +export function reset() { + if (window && window.liQ_instances) { + window.liQ_instances.forEach(i => i.eventBus.off(EVENTS_TOPIC, setEventFiredFlag)) + window.liQ_instances = []; + } + liveIntentIdSubmodule.setModuleMode(null) + eventFired = false; + liveConnect = null; +} + +/** + * This function is also used in tests + */ +export function setEventFiredFlag() { + eventFired = true; +} + +function parseLiveIntentCollectorConfig(collectConfig) { + const config = {}; + collectConfig = collectConfig || {} + collectConfig.appId && (config.appId = collectConfig.appId); + collectConfig.fpiStorageStrategy && (config.storageStrategy = collectConfig.fpiStorageStrategy); + collectConfig.fpiExpirationDays && (config.expirationDays = collectConfig.fpiExpirationDays); + collectConfig.collectorUrl && (config.collectorUrl = collectConfig.collectorUrl); + config.ajaxTimeout = collectConfig.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; + return config; +} + +/** + * Create requestedAttributes array to pass to liveconnect + * @function + * @param {Object} overrides - object with boolean values that will override defaults { 'foo': true, 'bar': false } + * @returns {Array} + */ + +function initializeLiveConnect(configParams) { + if (liveConnect) { + return liveConnect; + } + + configParams = configParams || {}; + const fpidConfig = configParams.fpid || {}; + + const publisherId = configParams.publisherId || 'any'; + const identityResolutionConfig = { + publisherId: publisherId, + requestedAttributes: parseRequestedAttributes(configParams.requestedAttributesOverrides) + }; + if (configParams.url) { + identityResolutionConfig.url = configParams.url + } + + identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; + + const liveConnectConfig = parseLiveIntentCollectorConfig(configParams.liCollectConfig); + + if (!liveConnectConfig.appId && configParams.distributorId) { + liveConnectConfig.distributorId = configParams.distributorId; + identityResolutionConfig.source = configParams.distributorId; + } else { + identityResolutionConfig.source = configParams.partner || 'prebid' + } + + liveConnectConfig.wrapperName = 'prebid'; + liveConnectConfig.trackerVersion = '$prebid.version$'; + liveConnectConfig.identityResolutionConfig = identityResolutionConfig; + liveConnectConfig.identifiersToResolve = configParams.identifiersToResolve || []; + liveConnectConfig.fireEventDelay = configParams.fireEventDelay; + liveConnectConfig.idCookie = {}; + liveConnectConfig.idCookie.name = fpidConfig.name; + liveConnectConfig.idCookie.strategy = fpidConfig.strategy == 'html5' ? 'localStorage' : fpidConfig.strategy; + const usPrivacyString = uspDataHandler.getConsentData(); + if (usPrivacyString) { + liveConnectConfig.usPrivacyString = usPrivacyString; + } + const gdprConsent = gdprDataHandler.getConsentData() + if (gdprConsent) { + liveConnectConfig.gdprApplies = gdprConsent.gdprApplies; + liveConnectConfig.gdprConsent = gdprConsent.consentString; + } + const gppConsent = gppDataHandler.getConsentData(); + if (gppConsent) { + liveConnectConfig.gppString = gppConsent.gppString; + liveConnectConfig.gppApplicableSections = gppConsent.applicableSections; + } + // The second param is the storage object, LS & Cookie manipulation uses PBJS + // The third param is the ajax and pixel object, the ajax and pixel use PBJS + liveConnect = liveIntentIdSubmodule.getInitializer()(liveConnectConfig, storage, calls); + if (configParams.emailHash) { + liveConnect.push({ hash: configParams.emailHash }) + } + return liveConnect; +} + +function tryFireEvent() { + if (!eventFired && liveConnect) { + const eventDelay = liveConnect.config.fireEventDelay ?? DEFAULT_DELAY + setTimeout(() => { + const instances = window.liQ_instances + instances.forEach(i => i.eventBus.once(EVENTS_TOPIC, setEventFiredFlag)) + if (!eventFired && liveConnect) { + liveConnect.fire(); + } + }, eventDelay) + } +} + +/** @type {Submodule} */ +export const liveIntentIdSubmodule = { + moduleMode: '$$LIVE_INTENT_MODULE_MODE$$', + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + gvlid: GVLID, + + setModuleMode(mode) { + this.moduleMode = mode + }, + getInitializer() { + return (liveConnectConfig, storage, calls) => LiveConnect(liveConnectConfig, storage, calls, this.moduleMode) + }, + + /** + * decode the stored id value for passing to bid requests. Note that lipb object is a wrapper for everything, and + * internally it could contain more data other than `lipbid`(e.g. `segments`) depending on the `partner` and + * `publisherId` params. + * @function + * @param {{unifiedId:string}} value + * @param {SubmoduleConfig|undefined} config + * @returns {{lipb:Object}} + */ + decode(value, config) { + const configParams = (config && config.params) || {}; + + if (!liveConnect) { + initializeLiveConnect(configParams); + } + tryFireEvent(); + + return composeIdObject(value); + }, + + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @returns {IdResponse|undefined} + */ + getId(config) { + const configParams = (config && config.params) || {}; + const liveConnect = initializeLiveConnect(configParams); + if (!liveConnect) { + return; + } + tryFireEvent(); + const result = function(callback) { + liveConnect.resolve( + response => { + callback(response); + }, + error => { + logError(`${MODULE_NAME}: ID fetch encountered an error: `, error); + callback(); + } + ) + } + return { callback: result }; + }, + eids: { + eids + } +}; + +submodule('userId', liveIntentIdSubmodule); diff --git a/libraries/liveIntentIdSystem/liveIntentIdSystemShared.js b/libraries/liveIntentIdSystem/liveIntentIdSystemShared.js new file mode 100644 index 00000000000..22331bc8e76 --- /dev/null +++ b/libraries/liveIntentIdSystem/liveIntentIdSystemShared.js @@ -0,0 +1,202 @@ +import {UID1_EIDS} from '../uid1Eids/uid1Eids.js'; +import {UID2_EIDS} from '../uid2Eids/uid2Eids.js'; +import { getRefererInfo } from '../../src/refererDetection.js'; + +const GVLID = 148; +export const DEFAULT_AJAX_TIMEOUT = 5000 +export const MODULE_NAME = 'liveIntentId'; +export const LI_PROVIDER_DOMAIN = 'liveintent.com'; +export const DEFAULT_REQUESTED_ATTRIBUTES = {'nonId': true} +export const DEFAULT_DELAY = 500 +const defaultRequestedAttributes = {'nonId': true} + +export function parseRequestedAttributes(overrides) { + function renameAttribute(attribute) { + if (attribute === 'fpid') { + return 'idCookie'; + } else { + return attribute; + }; + } + function createParameterArray(config) { + return Object.entries(config).flatMap(([k, v]) => (typeof v === 'boolean' && v) ? [renameAttribute(k)] : []); + } + if (typeof overrides === 'object') { + return createParameterArray({...defaultRequestedAttributes, ...overrides}); + } else { + return createParameterArray(defaultRequestedAttributes); + } +} + +export function composeIdObject(value) { + const result = {}; + + // old versions stored lipbid in unifiedId. Ensure that we can still read the data. + const lipbid = value.nonId || value.unifiedId + if (lipbid) { + const lipb = { ...value, lipbid }; + delete lipb.unifiedId; + result.lipb = lipb; + } + + // Lift usage of uid2 by exposing uid2 if we were asked to resolve it. + // As adapters are applied in lexicographical order, we will always + // be overwritten by the 'proper' uid2 module if it is present. + if (value.uid2) { + result.uid2 = { 'id': value.uid2, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.bidswitch) { + result.bidswitch = { 'id': value.bidswitch, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.medianet) { + result.medianet = { 'id': value.medianet, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.magnite) { + result.magnite = { 'id': value.magnite, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.index) { + result.index = { 'id': value.index, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.openx) { + result.openx = { 'id': value.openx, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.pubmatic) { + result.pubmatic = { 'id': value.pubmatic, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.sovrn) { + result.sovrn = { 'id': value.sovrn, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.idCookie) { + if (!coppaDataHandler.getCoppa()) { + result.lipb = { ...result.lipb, fpid: value.idCookie }; + result.fpid = { 'id': value.idCookie }; + } + delete result.lipb.idCookie; + } + + if (value.thetradedesk) { + result.lipb = {...result.lipb, tdid: value.thetradedesk} + result.tdid = { 'id': value.thetradedesk, ext: { rtiPartner: 'TDID', provider: getRefererInfo().domain || LI_PROVIDER_DOMAIN } } + delete result.lipb.thetradedesk + } + + return result +} + +export const eids = { + ...UID1_EIDS, + ...UID2_EIDS, + 'lipb': { + getValue: function(data) { + return data.lipbid; + }, + source: 'liveintent.com', + atype: 3, + getEidExt: function(data) { + if (Array.isArray(data.segments) && data.segments.length) { + return { + segments: data.segments + }; + } + } + }, + 'bidswitch': { + source: 'bidswitch.net', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'medianet': { + source: 'media.net', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'magnite': { + source: 'rubiconproject.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'index': { + source: 'liveintent.indexexchange.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'openx': { + source: 'openx.net', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'pubmatic': { + source: 'pubmatic.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'sovrn': { + source: 'liveintent.sovrn.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'fpid': { + source: 'fpid.liveintent.com', + atype: 1, + getValue: function(data) { + return data.id; + } + } +} diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index a9708910ca7..00d05787ff7 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -1,416 +1,14 @@ -/** - * This module adds LiveIntentId to the User ID module - * The {@link module:modules/userId} module is required - * @module modules/liveIntentIdSystem - * @requires module:modules/userId - */ -import { triggerPixel, logError } from '../src/utils.js'; -import { ajaxBuilder } from '../src/ajax.js'; -import { submodule } from '../src/hook.js'; -import { LiveConnect } from 'live-connect-js/prebid'; // eslint-disable-line prebid/validate-imports -import { gdprDataHandler, uspDataHandler, gppDataHandler, coppaDataHandler } from '../src/adapterManager.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { MODULE_TYPE_UID } from '../src/activities/modules.js'; -import { UID2_EIDS } from '../libraries/uid2Eids/uid2Eids.js'; -import {UID1_EIDS} from '../libraries/uid1Eids/uid1Eids.js'; -import { getRefererInfo } from '../src/refererDetection.js'; - -/** - * @typedef {import('../modules/userId/index.js').Submodule} Submodule - * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig - * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse - */ -const GVLID = 148; -const DEFAULT_AJAX_TIMEOUT = 5000; -const EVENTS_TOPIC = 'pre_lips'; -const MODULE_NAME = 'liveIntentId'; -const LI_PROVIDER_DOMAIN = 'liveintent.com'; -export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); -const defaultRequestedAttributes = {'nonId': true}; -const calls = { - ajaxGet: (url, onSuccess, onError, timeout) => { - ajaxBuilder(timeout)( - url, - { - success: onSuccess, - error: onError - }, - undefined, - { - method: 'GET', - withCredentials: true - } - ) - }, - pixelGet: (url, onload) => triggerPixel(url, onload) -} - -let eventFired = false; -let liveConnect = null; - -/** - * This function is used in tests - */ -export function reset() { - if (window && window.liQ_instances) { - window.liQ_instances.forEach(i => i.eventBus.off(EVENTS_TOPIC, setEventFiredFlag)); - window.liQ_instances = []; - } - liveIntentIdSubmodule.setModuleMode(null); - eventFired = false; - liveConnect = null; -} - -/** - * This function is also used in tests - */ -export function setEventFiredFlag() { - eventFired = true; -} - -function parseLiveIntentCollectorConfig(collectConfig) { - const config = {}; - collectConfig = collectConfig || {}; - collectConfig.appId && (config.appId = collectConfig.appId); - collectConfig.fpiStorageStrategy && (config.storageStrategy = collectConfig.fpiStorageStrategy); - collectConfig.fpiExpirationDays && (config.expirationDays = collectConfig.fpiExpirationDays); - collectConfig.collectorUrl && (config.collectorUrl = collectConfig.collectorUrl); - config.ajaxTimeout = collectConfig.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; - return config; -} - -/** - * Create requestedAttributes array to pass to liveconnect - * @function - * @param {Object} overrides - object with boolean values that will override defaults { 'foo': true, 'bar': false } - * @returns {Array} - */ -function parseRequestedAttributes(overrides) { - function renameAttribute(attribute) { - if (attribute === 'fpid') { - return 'idCookie'; - } else { - return attribute; - }; - } - function createParameterArray(config) { - return Object.entries(config).flatMap(([k, v]) => (typeof v === 'boolean' && v) ? [renameAttribute(k)] : []); - } - if (typeof overrides === 'object') { - return createParameterArray({...defaultRequestedAttributes, ...overrides}); +function loadModule() { + // Load appropriate module based on the build flag. Constant folding ensures + // that the other one will not be included in the bundle. + // eslint-disable-next-line no-constant-condition + if ('$$LIVE_INTENT_MODULE_MODE$$' === 'hub') { + // eslint-disable-next-line no-restricted-globals + return require('../libraries/liveIntentIdSystem/liveIntentIdHubSystem.js') } else { - return createParameterArray(defaultRequestedAttributes); - } -} - -function initializeLiveConnect(configParams) { - if (liveConnect) { - return liveConnect; - } - - configParams = configParams || {}; - const fpidConfig = configParams.fpid || {}; - - const publisherId = configParams.publisherId || 'any'; - const identityResolutionConfig = { - publisherId: publisherId, - requestedAttributes: parseRequestedAttributes(configParams.requestedAttributesOverrides) - }; - if (configParams.url) { - identityResolutionConfig.url = configParams.url; - }; - - identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; - - const liveConnectConfig = parseLiveIntentCollectorConfig(configParams.liCollectConfig); - - if (!liveConnectConfig.appId && configParams.distributorId) { - liveConnectConfig.distributorId = configParams.distributorId; - identityResolutionConfig.source = configParams.distributorId; - } else { - identityResolutionConfig.source = configParams.partner || 'prebid'; - } - - liveConnectConfig.wrapperName = 'prebid'; - liveConnectConfig.trackerVersion = '$prebid.version$'; - liveConnectConfig.identityResolutionConfig = identityResolutionConfig; - liveConnectConfig.identifiersToResolve = configParams.identifiersToResolve || []; - liveConnectConfig.fireEventDelay = configParams.fireEventDelay; - - liveConnectConfig.idCookie = {}; - liveConnectConfig.idCookie.name = fpidConfig.name; - liveConnectConfig.idCookie.strategy = fpidConfig.strategy == 'html5' ? 'localStorage' : fpidConfig.strategy; - - const usPrivacyString = uspDataHandler.getConsentData(); - if (usPrivacyString) { - liveConnectConfig.usPrivacyString = usPrivacyString; - } - const gdprConsent = gdprDataHandler.getConsentData(); - if (gdprConsent) { - liveConnectConfig.gdprApplies = gdprConsent.gdprApplies; - liveConnectConfig.gdprConsent = gdprConsent.consentString; - } - const gppConsent = gppDataHandler.getConsentData(); - if (gppConsent) { - liveConnectConfig.gppString = gppConsent.gppString; - liveConnectConfig.gppApplicableSections = gppConsent.applicableSections; - } - // The second param is the storage object, LS & Cookie manipulation uses PBJS - // The third param is the ajax and pixel object, the ajax and pixel use PBJS - liveConnect = liveIntentIdSubmodule.getInitializer()(liveConnectConfig, storage, calls); - if (configParams.emailHash) { - liveConnect.push({ hash: configParams.emailHash }); - } - return liveConnect; -} - -function tryFireEvent() { - if (!eventFired && liveConnect) { - const eventDelay = liveConnect.config.fireEventDelay || 500; - setTimeout(() => { - const instances = window.liQ_instances; - instances.forEach(i => i.eventBus.once(EVENTS_TOPIC, setEventFiredFlag)); - if (!eventFired && liveConnect) { - liveConnect.fire(); - } - }, eventDelay); + // eslint-disable-next-line no-restricted-globals + return require('../libraries/liveIntentIdSystem/liveIntentIdSystem.js') } } -/** @type {Submodule} */ -export const liveIntentIdSubmodule = { - moduleMode: '$$LIVE_INTENT_MODULE_MODE$$', - /** - * used to link submodule with config - * @type {string} - */ - name: MODULE_NAME, - gvlid: GVLID, - setModuleMode(mode) { - this.moduleMode = mode; - }, - getInitializer() { - return (liveConnectConfig, storage, calls) => LiveConnect(liveConnectConfig, storage, calls, this.moduleMode); - }, - - /** - * decode the stored id value for passing to bid requests. Note that lipb object is a wrapper for everything, and - * internally it could contain more data other than `lipbid`(e.g. `segments`) depending on the `partner` and - * `publisherId` params. - * @function - * @param {{unifiedId:string}} value - * @param {SubmoduleConfig|undefined} config - * @returns {{lipb:Object}} - */ - decode(value, config) { - const configParams = (config && config.params) || {}; - function composeIdObject(value) { - const result = {}; - - // old versions stored lipbid in unifiedId. Ensure that we can still read the data. - const lipbid = value.nonId || value.unifiedId; - if (lipbid) { - const lipb = { ...value, lipbid }; - delete lipb.unifiedId; - result.lipb = lipb; - } - - // Lift usage of uid2 by exposing uid2 if we were asked to resolve it. - // As adapters are applied in lexicographical order, we will always - // be overwritten by the 'proper' uid2 module if it is present. - if (value.uid2) { - result.uid2 = { 'id': value.uid2, ext: { provider: LI_PROVIDER_DOMAIN } }; - } - - if (value.bidswitch) { - result.bidswitch = { 'id': value.bidswitch, ext: { provider: LI_PROVIDER_DOMAIN } }; - } - - if (value.medianet) { - result.medianet = { 'id': value.medianet, ext: { provider: LI_PROVIDER_DOMAIN } }; - } - - if (value.magnite) { - result.magnite = { 'id': value.magnite, ext: { provider: LI_PROVIDER_DOMAIN } }; - } - - if (value.index) { - result.index = { 'id': value.index, ext: { provider: LI_PROVIDER_DOMAIN } }; - } - - if (value.openx) { - result.openx = { 'id': value.openx, ext: { provider: LI_PROVIDER_DOMAIN } }; - } - - if (value.pubmatic) { - result.pubmatic = { 'id': value.pubmatic, ext: { provider: LI_PROVIDER_DOMAIN } }; - } - - if (value.sovrn) { - result.sovrn = { 'id': value.sovrn, ext: { provider: LI_PROVIDER_DOMAIN } }; - } - - if (value.idCookie) { - if (!coppaDataHandler.getCoppa()) { - result.lipb = { ...result.lipb, fpid: value.idCookie }; - result.fpid = { 'id': value.idCookie }; - } - delete result.lipb.idCookie; - } - - if (value.thetradedesk) { - result.lipb = {...result.lipb, tdid: value.thetradedesk} - result.tdid = { 'id': value.thetradedesk, ext: { rtiPartner: 'TDID', provider: getRefererInfo().domain || LI_PROVIDER_DOMAIN } } - delete result.lipb.thetradedesk - } - - return result - } - - if (!liveConnect) { - initializeLiveConnect(configParams); - } - tryFireEvent(); - - return composeIdObject(value); - }, - - /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} [config] - * @returns {IdResponse|undefined} - */ - getId(config) { - const configParams = (config && config.params) || {}; - const liveConnect = initializeLiveConnect(configParams); - if (!liveConnect) { - return; - } - tryFireEvent(); - const result = function(callback) { - liveConnect.resolve( - response => { - callback(response); - }, - error => { - logError(`${MODULE_NAME}: ID fetch encountered an error: `, error); - callback(); - } - ) - } - - return { callback: result }; - }, - eids: { - ...UID1_EIDS, - ...UID2_EIDS, - 'lipb': { - getValue: function(data) { - return data.lipbid; - }, - source: 'liveintent.com', - atype: 3, - getEidExt: function(data) { - if (Array.isArray(data.segments) && data.segments.length) { - return { - segments: data.segments - }; - } - } - }, - 'bidswitch': { - source: 'bidswitch.net', - atype: 3, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - 'medianet': { - source: 'media.net', - atype: 3, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - 'magnite': { - source: 'rubiconproject.com', - atype: 3, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - 'index': { - source: 'liveintent.indexexchange.com', - atype: 3, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - 'openx': { - source: 'openx.net', - atype: 3, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - 'pubmatic': { - source: 'pubmatic.com', - atype: 3, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - 'sovrn': { - source: 'liveintent.sovrn.com', - atype: 3, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - 'fpid': { - source: 'fpid.liveintent.com', - atype: 1, - getValue: function(data) { - return data.id; - } - } - } -}; - -submodule('userId', liveIntentIdSubmodule); +export const liveIntentIdSubmodule = loadModule() diff --git a/test/spec/modules/liveIntentIdHubSystem_spec.js b/test/spec/modules/liveIntentIdHubSystem_spec.js new file mode 100644 index 00000000000..a048a43bbf5 --- /dev/null +++ b/test/spec/modules/liveIntentIdHubSystem_spec.js @@ -0,0 +1,363 @@ +import { liveIntentIdHubSubmodule, resetSubmodule } from 'libraries/liveIntentIdSystem/liveIntentIdHubSystem.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../../../src/adapterManager.js'; +import * as refererDetection from '../../../src/refererDetection.js'; +const DEFAULT_AJAX_TIMEOUT = 5000 +const PUBLISHER_ID = '89899'; +const defaultConfigParams = { params: {publisherId: PUBLISHER_ID, fireEventDelay: 1} }; + +describe('LiveIntentIdHub', function() { + let uspConsentDataStub; + let gdprConsentDataStub; + let gppConsentDataStub; + let refererInfoStub; + + beforeEach(function() { + uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); + gdprConsentDataStub = sinon.stub(gdprDataHandler, 'getConsentData'); + gppConsentDataStub = sinon.stub(gppDataHandler, 'getConsentData'); + refererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); + }); + + afterEach(function() { + uspConsentDataStub.restore(); + gdprConsentDataStub.restore(); + gppConsentDataStub.restore(); + refererInfoStub.restore(); + window.liQHub = []; // reset + resetSubmodule(); + }); + + it('should fire an event and resolve when getId and include the pricacy settings the resolution request', function () { + uspConsentDataStub.returns('1YNY'); + gdprConsentDataStub.returns({ + gdprApplies: true, + consentString: 'consentDataString' + }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1, 2] + }) + liveIntentIdHubSubmodule.getId(defaultConfigParams).callback(() => {}); + + const expectedConsent = { gdpr: { consentString: 'consentDataString', gdprApplies: true }, gpp: { applicableSections: [1, 2], consentString: 'gppConsentDataString' }, usPrivacy: { consentString: '1YNY' } } + + expect(window.liQHub).to.have.length(3) + + expect(window.liQHub[0]).to.eql({ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: expectedConsent, + integration: { distributorId: defaultConfigParams.distributorId, publisherId: '89899', type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }) + + expect(window.liQHub[1]).to.eql({ + clientRef: {}, + delay: defaultConfigParams.params.fireEventDelay, + type: 'schedule_default_collect' + }) + + const resolveCommand = window.liQHub[2] + // functions cannot be reasonably compared, remove them + delete resolveCommand.onSuccess[0].callback + delete resolveCommand.onFailure + + expect(resolveCommand).to.eql({ + clientRef: {}, + onSuccess: [{ type: 'callback' }], + requestedAttributes: [ 'nonId' ], + type: 'resolve' + }) + }); + + it('should fire an event when getId and a hash is provided', function() { + liveIntentIdHubSubmodule.getId({ params: { + ...defaultConfigParams.params, + emailHash: '58131bc547fb87af94cebdaf3102321f' + }}).callback(() => {}); + + expect(window.liQHub).to.have.length(3) + + expect(window.liQHub[0]).to.eql({ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { distributorId: defaultConfigParams.distributorId, publisherId: '89899', type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }) + + expect(window.liQHub[1]).to.eql({ + clientRef: {}, + sourceEvent: { hash: '58131bc547fb87af94cebdaf3102321f' }, + type: 'collect' + }) + + const resolveCommand = window.liQHub[2] + // functions cannot be reasonably compared, remove them + delete resolveCommand.onSuccess[0].callback + delete resolveCommand.onFailure + + expect(resolveCommand).to.eql({ + clientRef: {}, + onSuccess: [{ type: 'callback' }], + requestedAttributes: [ 'nonId' ], + type: 'resolve' + }) + }); + + it('should have the same data after call decode when appId, disrtributorId and sourceEvent is absent', function() { + liveIntentIdHubSubmodule.decode({}, defaultConfigParams); + expect(window.liQHub).to.eql([{ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { distributorId: defaultConfigParams.distributorId, publisherId: '89899', type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }, + { + clientRef: {}, + delay: defaultConfigParams.params.fireEventDelay, + type: 'schedule_default_collect' + }]) + }); + + it('should have the same data after call decode when appId and sourceEvent is present', function() { + const configParams = { + params: { + ...defaultConfigParams.params, + liCollectConfig: { + appId: 'a-0001' + }, + emailHash: '123' + } + } + liveIntentIdHubSubmodule.decode({}, configParams); + expect(window.liQHub).to.eql([{ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { appId: 'a-0001', publisherId: '89899', type: 'application' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }, + { + clientRef: {}, + sourceEvent: { hash: '123' }, + type: 'collect' + }]) + }); + + it('should have the same data after call decode when distributorId and sourceEvent is present', function() { + const configParams = { + params: { + ...defaultConfigParams.params, + distributorId: 'did-1111', + emailHash: '123' + } + } + liveIntentIdHubSubmodule.decode({}, configParams); + expect(window.liQHub).to.eql([{ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { distributorId: 'did-1111', type: 'distributor' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'did-1111', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }, + { + clientRef: {}, + sourceEvent: { hash: '123' }, + type: 'collect' + }]) + }); + + it('should have the same data when decode with privacy settings', function() { + uspConsentDataStub.returns('1YNY'); + gdprConsentDataStub.returns({ + gdprApplies: false, + consentString: 'consentDataString' + }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1] + }) + liveIntentIdHubSubmodule.decode({}, defaultConfigParams); + expect(window.liQHub).to.eql([{ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: { gdpr: { consentString: 'consentDataString', gdprApplies: false }, gpp: { applicableSections: [1], consentString: 'gppConsentDataString' }, usPrivacy: { consentString: '1YNY' } }, + integration: { distributorId: defaultConfigParams.params.distributorId, publisherId: defaultConfigParams.params.publisherId, type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }, + { + clientRef: {}, + delay: defaultConfigParams.params.fireEventDelay, + type: 'schedule_default_collect' + }]) + }); + + it('should not fire event again when it is already fired', function() { + liveIntentIdHubSubmodule.decode({}, defaultConfigParams); + liveIntentIdHubSubmodule.decode({}, defaultConfigParams); + + expect(window.liQHub).to.have.length(2) // instead of 4 + }); + + it('should not return a decoded identifier when the unifiedId is not present in the value', function() { + const result = liveIntentIdHubSubmodule.decode({ fireEventDelay: 1, additionalData: 'data' }); + expect(result).to.be.eql({}); + }); + + it('should decode a unifiedId to lipbId and remove it', function() { + const result = liveIntentIdHubSubmodule.decode({ unifiedId: 'data' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'data'}}); + }); + + it('should decode a nonId to lipbId', function() { + const result = liveIntentIdHubSubmodule.decode({ nonId: 'data' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'data', 'nonId': 'data'}}); + }); + + it('should resolve extra attributes', function() { + liveIntentIdHubSubmodule.getId({ params: { + ...defaultConfigParams.params, + ...{ requestedAttributesOverrides: { 'foo': true, 'bar': false } } + } }).callback(() => {}); + + expect(window.liQHub).to.have.length(3) + expect(window.liQHub[0]).to.eql({ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { distributorId: defaultConfigParams.distributorId, publisherId: '89899', type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }) + + expect(window.liQHub[1]).to.eql({ + clientRef: {}, + delay: defaultConfigParams.params.fireEventDelay, + type: 'schedule_default_collect' + }) + + const resolveCommand = window.liQHub[2] + + // functions cannot be reasonably compared, remove them + delete resolveCommand.onSuccess[0].callback + delete resolveCommand.onFailure + + expect(resolveCommand).to.eql({ + clientRef: {}, + onSuccess: [{ type: 'callback' }], + requestedAttributes: [ 'nonId', 'foo' ], + type: 'resolve' + }) + }); + + it('should decode a uid2 to a separate object when present', function() { + const result = liveIntentIdHubSubmodule.decode({ nonId: 'foo', uid2: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode values with uid2 but no nonId', function() { + const result = liveIntentIdHubSubmodule.decode({ uid2: 'bar' }, defaultConfigParams); + expect(result).to.eql({'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a bidswitch id to a separate object when present', function() { + const result = liveIntentIdHubSubmodule.decode({ nonId: 'foo', bidswitch: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'bidswitch': 'bar'}, 'bidswitch': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a medianet id to a separate object when present', function() { + const result = liveIntentIdHubSubmodule.decode({ nonId: 'foo', medianet: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'medianet': 'bar'}, 'medianet': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a sovrn id to a separate object when present', function() { + const result = liveIntentIdHubSubmodule.decode({ nonId: 'foo', sovrn: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'sovrn': 'bar'}, 'sovrn': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a magnite id to a separate object when present', function() { + const result = liveIntentIdHubSubmodule.decode({ nonId: 'foo', magnite: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'magnite': 'bar'}, 'magnite': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an index id to a separate object when present', function() { + const result = liveIntentIdHubSubmodule.decode({ nonId: 'foo', index: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'index': 'bar'}, 'index': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an openx id to a separate object when present', function () { + const result = liveIntentIdHubSubmodule.decode({ nonId: 'foo', openx: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'openx': 'bar'}, 'openx': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an pubmatic id to a separate object when present', function() { + const result = liveIntentIdHubSubmodule.decode({ nonId: 'foo', pubmatic: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'pubmatic': 'bar'}, 'pubmatic': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a thetradedesk id to a separate object when present', function() { + const provider = 'liveintent.com' + refererInfoStub.returns({domain: provider}) + const result = liveIntentIdHubSubmodule.decode({ nonId: 'foo', thetradedesk: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'tdid': 'bar'}, 'tdid': {'id': 'bar', 'ext': {'rtiPartner': 'TDID', 'provider': provider}}}); + }); + + it('should allow disabling nonId resolution', function() { + liveIntentIdHubSubmodule.getId({ params: { + ...defaultConfigParams.params, + ...{ requestedAttributesOverrides: { 'nonId': false, 'uid2': true } } + } }).callback(() => {}); + + expect(window.liQHub).to.have.length(3) + expect(window.liQHub[0]).to.eql({ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { distributorId: defaultConfigParams.distributorId, publisherId: '89899', type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }) + + expect(window.liQHub[1]).to.eql({ + clientRef: {}, + delay: defaultConfigParams.params.fireEventDelay, + type: 'schedule_default_collect' + }) + + const resolveCommand = window.liQHub[2] + // functions cannot be reasonably compared, remove them + delete resolveCommand.onSuccess[0].callback + delete resolveCommand.onFailure + + expect(resolveCommand).to.eql({ + clientRef: {}, + onSuccess: [{ type: 'callback' }], + requestedAttributes: [ 'uid2' ], + type: 'resolve' + }) + }); +}); diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index dae19d0f578..6bd10713dfb 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -1,4 +1,4 @@ -import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from 'modules/liveIntentIdSystem.js'; +import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from 'libraries/liveIntentIdSystem/liveIntentIdSystem.js'; import * as utils from 'src/utils.js'; import { gdprDataHandler, uspDataHandler, gppDataHandler, coppaDataHandler } from '../../../src/adapterManager.js'; import { server } from 'test/mocks/xhr.js'; From 6fd43e1bd3741bcdc848cedfec5544c001ac5c67 Mon Sep 17 00:00:00 2001 From: Peixun Zhang Date: Fri, 16 Aug 2024 16:45:34 +0200 Subject: [PATCH 02/16] add tests and rebase to the correct master --- .../liveIntentIdHubSystem.js | 4 +- .../liveIntentIdSystem/liveIntentIdSystem.js | 54 +++++++++--------- .../liveIntentIdSystemShared.js | 13 +++-- .../modules/liveIntentIdHubSystem_spec.js | 57 ++++++++++++++++++- 4 files changed, 92 insertions(+), 36 deletions(-) diff --git a/libraries/liveIntentIdSystem/liveIntentIdHubSystem.js b/libraries/liveIntentIdSystem/liveIntentIdHubSystem.js index b8966c4aa90..82eb67212b8 100644 --- a/libraries/liveIntentIdSystem/liveIntentIdHubSystem.js +++ b/libraries/liveIntentIdSystem/liveIntentIdHubSystem.js @@ -135,7 +135,7 @@ export const liveIntentIdHubSubmodule = { * @function */ decode(value, config) { - const configParams = config != null ? config.params : {}; + const configParams = config?.params ?? {}; // ensure client is initialized and we fired at least one collect request initializeClient(configParams) @@ -148,7 +148,7 @@ export const liveIntentIdHubSubmodule = { * @function */ getId(config) { - const configParams = config != null ? config.params : {}; + const configParams = config?.params ?? {}; const clientRef = initializeClient(configParams) diff --git a/libraries/liveIntentIdSystem/liveIntentIdSystem.js b/libraries/liveIntentIdSystem/liveIntentIdSystem.js index 7e160cde1b4..80a3e796432 100644 --- a/libraries/liveIntentIdSystem/liveIntentIdSystem.js +++ b/libraries/liveIntentIdSystem/liveIntentIdSystem.js @@ -6,22 +6,22 @@ */ import { triggerPixel, logError } from '../../src/utils.js'; import { ajaxBuilder } from '../../src/ajax.js'; -import { LiveConnect } from 'live-connect-js'; // eslint-disable-line prebid/validate-imports import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../../src/adapterManager.js'; -import {getStorageManager} from '../../src/storageManager.js'; -import {MODULE_TYPE_UID} from '../../src/activities/modules.js'; import { submodule } from '../../src/hook.js'; -import { DEFAULT_AJAX_TIMEOUT, MODULE_NAME, parseRequestedAttributes, composeIdObject, eids, DEFAULT_DELAY, GVLID } from './liveIntentIdSystemShared.js' +import { LiveConnect } from 'live-connect-js'; // eslint-disable-line prebid/validate-imports +import { getStorageManager } from '../../src/storageManager.js'; +import { MODULE_TYPE_UID } from '../../src/activities/modules.js'; +import { DEFAULT_AJAX_TIMEOUT, MODULE_NAME, composeIdObject, eids, DEFAULT_DELAY, GVLID, parseRequestedAttributes } from './liveIntentIdSystemShared.js' /** - * @typedef {import('../../modules/userId/index.js').Submodule} Submodule - * @typedef {import('../../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig - * @typedef {import('../../modules/userId/index.js').IdResponse} IdResponse + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse */ -const EVENTS_TOPIC = 'pre_lips' -export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); +const EVENTS_TOPIC = 'pre_lips'; +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); const calls = { ajaxGet: (url, onSuccess, onError, timeout) => { ajaxBuilder(timeout)( @@ -48,10 +48,10 @@ let liveConnect = null; */ export function reset() { if (window && window.liQ_instances) { - window.liQ_instances.forEach(i => i.eventBus.off(EVENTS_TOPIC, setEventFiredFlag)) + window.liQ_instances.forEach(i => i.eventBus.off(EVENTS_TOPIC, setEventFiredFlag)); window.liQ_instances = []; } - liveIntentIdSubmodule.setModuleMode(null) + liveIntentIdSubmodule.setModuleMode(null); eventFired = false; liveConnect = null; } @@ -65,7 +65,7 @@ export function setEventFiredFlag() { function parseLiveIntentCollectorConfig(collectConfig) { const config = {}; - collectConfig = collectConfig || {} + collectConfig = collectConfig || {}; collectConfig.appId && (config.appId = collectConfig.appId); collectConfig.fpiStorageStrategy && (config.storageStrategy = collectConfig.fpiStorageStrategy); collectConfig.fpiExpirationDays && (config.expirationDays = collectConfig.fpiExpirationDays); @@ -95,8 +95,8 @@ function initializeLiveConnect(configParams) { requestedAttributes: parseRequestedAttributes(configParams.requestedAttributesOverrides) }; if (configParams.url) { - identityResolutionConfig.url = configParams.url - } + identityResolutionConfig.url = configParams.url; + }; identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; @@ -106,7 +106,7 @@ function initializeLiveConnect(configParams) { liveConnectConfig.distributorId = configParams.distributorId; identityResolutionConfig.source = configParams.distributorId; } else { - identityResolutionConfig.source = configParams.partner || 'prebid' + identityResolutionConfig.source = configParams.partner || 'prebid'; } liveConnectConfig.wrapperName = 'prebid'; @@ -114,14 +114,16 @@ function initializeLiveConnect(configParams) { liveConnectConfig.identityResolutionConfig = identityResolutionConfig; liveConnectConfig.identifiersToResolve = configParams.identifiersToResolve || []; liveConnectConfig.fireEventDelay = configParams.fireEventDelay; + liveConnectConfig.idCookie = {}; liveConnectConfig.idCookie.name = fpidConfig.name; liveConnectConfig.idCookie.strategy = fpidConfig.strategy == 'html5' ? 'localStorage' : fpidConfig.strategy; + const usPrivacyString = uspDataHandler.getConsentData(); if (usPrivacyString) { liveConnectConfig.usPrivacyString = usPrivacyString; } - const gdprConsent = gdprDataHandler.getConsentData() + const gdprConsent = gdprDataHandler.getConsentData(); if (gdprConsent) { liveConnectConfig.gdprApplies = gdprConsent.gdprApplies; liveConnectConfig.gdprConsent = gdprConsent.consentString; @@ -135,21 +137,21 @@ function initializeLiveConnect(configParams) { // The third param is the ajax and pixel object, the ajax and pixel use PBJS liveConnect = liveIntentIdSubmodule.getInitializer()(liveConnectConfig, storage, calls); if (configParams.emailHash) { - liveConnect.push({ hash: configParams.emailHash }) + liveConnect.push({ hash: configParams.emailHash }); } return liveConnect; } function tryFireEvent() { if (!eventFired && liveConnect) { - const eventDelay = liveConnect.config.fireEventDelay ?? DEFAULT_DELAY + const eventDelay = liveConnect.config.fireEventDelay || DEFAULT_DELAY; setTimeout(() => { - const instances = window.liQ_instances - instances.forEach(i => i.eventBus.once(EVENTS_TOPIC, setEventFiredFlag)) + const instances = window.liQ_instances; + instances.forEach(i => i.eventBus.once(EVENTS_TOPIC, setEventFiredFlag)); if (!eventFired && liveConnect) { liveConnect.fire(); } - }, eventDelay) + }, eventDelay); } } @@ -162,12 +164,11 @@ export const liveIntentIdSubmodule = { */ name: MODULE_NAME, gvlid: GVLID, - setModuleMode(mode) { - this.moduleMode = mode + this.moduleMode = mode; }, getInitializer() { - return (liveConnectConfig, storage, calls) => LiveConnect(liveConnectConfig, storage, calls, this.moduleMode) + return (liveConnectConfig, storage, calls) => LiveConnect(liveConnectConfig, storage, calls, this.moduleMode); }, /** @@ -214,11 +215,10 @@ export const liveIntentIdSubmodule = { } ) } + return { callback: result }; }, - eids: { - eids - } + eids }; submodule('userId', liveIntentIdSubmodule); diff --git a/libraries/liveIntentIdSystem/liveIntentIdSystemShared.js b/libraries/liveIntentIdSystem/liveIntentIdSystemShared.js index 22331bc8e76..02ab9165187 100644 --- a/libraries/liveIntentIdSystem/liveIntentIdSystemShared.js +++ b/libraries/liveIntentIdSystem/liveIntentIdSystemShared.js @@ -1,8 +1,9 @@ import {UID1_EIDS} from '../uid1Eids/uid1Eids.js'; import {UID2_EIDS} from '../uid2Eids/uid2Eids.js'; import { getRefererInfo } from '../../src/refererDetection.js'; +import { coppaDataHandler } from '../../src/adapterManager.js'; -const GVLID = 148; +export const GVLID = 148; export const DEFAULT_AJAX_TIMEOUT = 5000 export const MODULE_NAME = 'liveIntentId'; export const LI_PROVIDER_DOMAIN = 'liveintent.com'; @@ -193,10 +194,10 @@ export const eids = { } }, 'fpid': { - source: 'fpid.liveintent.com', - atype: 1, - getValue: function(data) { - return data.id; - } + source: 'fpid.liveintent.com', + atype: 1, + getValue: function(data) { + return data.id; } + } } diff --git a/test/spec/modules/liveIntentIdHubSystem_spec.js b/test/spec/modules/liveIntentIdHubSystem_spec.js index a048a43bbf5..74326dde3d7 100644 --- a/test/spec/modules/liveIntentIdHubSystem_spec.js +++ b/test/spec/modules/liveIntentIdHubSystem_spec.js @@ -1,5 +1,5 @@ import { liveIntentIdHubSubmodule, resetSubmodule } from 'libraries/liveIntentIdSystem/liveIntentIdHubSystem.js'; -import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../../../src/adapterManager.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler, coppaDataHandler } from '../../../src/adapterManager.js'; import * as refererDetection from '../../../src/refererDetection.js'; const DEFAULT_AJAX_TIMEOUT = 5000 const PUBLISHER_ID = '89899'; @@ -9,12 +9,14 @@ describe('LiveIntentIdHub', function() { let uspConsentDataStub; let gdprConsentDataStub; let gppConsentDataStub; + let coppaConsentDataStub; let refererInfoStub; beforeEach(function() { uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); gdprConsentDataStub = sinon.stub(gdprDataHandler, 'getConsentData'); gppConsentDataStub = sinon.stub(gppDataHandler, 'getConsentData'); + coppaConsentDataStub = sinon.stub(coppaDataHandler, 'getCoppa'); refererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); }); @@ -22,6 +24,7 @@ describe('LiveIntentIdHub', function() { uspConsentDataStub.restore(); gdprConsentDataStub.restore(); gppConsentDataStub.restore(); + coppaConsentDataStub.restore(); refererInfoStub.restore(); window.liQHub = []; // reset resetSubmodule(); @@ -360,4 +363,56 @@ describe('LiveIntentIdHub', function() { type: 'resolve' }) }); + + it('should decode a idCookie as fpid if it exists and coppa is false', function() { + coppaConsentDataStub.returns(false) + const result = liveIntentIdHubSubmodule.decode({ nonId: 'foo', idCookie: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'fpid': 'bar'}, 'fpid': {'id': 'bar'}}); + }); + + it('should not decode a idCookie as fpid if it exists and coppa is true', function() { + coppaConsentDataStub.returns(true) + const result = liveIntentIdHubSubmodule.decode({ nonId: 'foo', idCookie: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo'}}) + }); + + it('should resolve fpid from cookie', function() { + const cookieName = 'testcookie' + liveIntentIdHubSubmodule.getId({ params: { + ...defaultConfigParams.params, + fpid: { 'strategy': 'cookie', 'name': cookieName }, + requestedAttributesOverrides: { 'fpid': true } } + }).callback(() => {}); + + expect(window.liQHub).to.have.length(3) + expect(window.liQHub[0]).to.eql({ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { distributorId: defaultConfigParams.distributorId, publisherId: PUBLISHER_ID, type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }) + + expect(window.liQHub[1]).to.eql({ + clientRef: {}, + delay: defaultConfigParams.params.fireEventDelay, + type: 'schedule_default_collect' + }) + + const resolveCommand = window.liQHub[2] + + // functions cannot be reasonably compared, remove them + delete resolveCommand.onSuccess[0].callback + delete resolveCommand.onFailure + + expect(resolveCommand).to.eql({ + clientRef: {}, + onSuccess: [{ type: 'callback' }], + requestedAttributes: [ 'nonId', 'idCookie' ], + type: 'resolve' + }) + }); }); From 89d9369b727dea3f7fcdf01794f3a0b1e8394032 Mon Sep 17 00:00:00 2001 From: Peixun Zhang Date: Mon, 19 Aug 2024 15:48:25 +0200 Subject: [PATCH 03/16] add fpid handling --- .../liveIntentIdSystem/liveIntentIdHubSystem.js | 15 +++++++++++++++ test/spec/modules/liveIntentIdHubSystem_spec.js | 1 + 2 files changed, 16 insertions(+) diff --git a/libraries/liveIntentIdSystem/liveIntentIdHubSystem.js b/libraries/liveIntentIdSystem/liveIntentIdHubSystem.js index 82eb67212b8..996e8090e8e 100644 --- a/libraries/liveIntentIdSystem/liveIntentIdHubSystem.js +++ b/libraries/liveIntentIdSystem/liveIntentIdHubSystem.js @@ -52,6 +52,20 @@ function initializeClient(configParams) { timeout: configParams.ajaxTimeout ?? DEFAULT_AJAX_TIMEOUT } + let idCookieSettings + if (configParams.fpid != null) { + const fpidConfig = configParams.fpid + let fpidStrategy + if (fpidConfig.strategy === 'html5') { + fpidStrategy = 'localStorage' + } else { + fpidStrategy = fpidConfig.strategy + } + idCookieSettings = { idCookieSettings: { type: 'provided', name: fpidConfig.name, fpidStrategy } }; + } else { + idCookieSettings = {} + } + function loadConsent() { const consent = {} const usPrivacyString = uspDataHandler.getConsentData(); @@ -79,6 +93,7 @@ function initializeClient(configParams) { consent, partnerCookies, collectSettings, + ...idCookieSettings, resolveSettings }) diff --git a/test/spec/modules/liveIntentIdHubSystem_spec.js b/test/spec/modules/liveIntentIdHubSystem_spec.js index 74326dde3d7..4fb09f720f8 100644 --- a/test/spec/modules/liveIntentIdHubSystem_spec.js +++ b/test/spec/modules/liveIntentIdHubSystem_spec.js @@ -393,6 +393,7 @@ describe('LiveIntentIdHub', function() { integration: { distributorId: defaultConfigParams.distributorId, publisherId: PUBLISHER_ID, type: 'custom' }, partnerCookies: new Set(), resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + idCookieSettings: { type: 'provided', name: 'testcookie', fpidStrategy: 'cookie' }, type: 'register_client' }) From 5eb71d279e59f99172b3cdd8dd243f578155763b Mon Sep 17 00:00:00 2001 From: Peixun Zhang Date: Mon, 19 Aug 2024 16:14:57 +0200 Subject: [PATCH 04/16] fix naming --- integrationExamples/gpt/x-domain/creative.html | 2 +- libraries/creative-renderer-display/renderer.js | 2 +- libraries/creative-renderer-native/renderer.js | 2 +- libraries/liveIntentIdSystem/liveIntentIdHubSystem.js | 8 ++++---- test/spec/modules/liveIntentIdHubSystem_spec.js | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/integrationExamples/gpt/x-domain/creative.html b/integrationExamples/gpt/x-domain/creative.html index 63842b00882..b7ba6b36a79 100644 --- a/integrationExamples/gpt/x-domain/creative.html +++ b/integrationExamples/gpt/x-domain/creative.html @@ -2,7 +2,7 @@ // creative will be rendered, e.g. GAM delivering a SafeFrame // this code is autogenerated, also available in 'build/creative/creative.js' - + +