Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CM-1260 Implement LiveIntent Prebid User Id Module Based On The Hub #42

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
162 changes: 162 additions & 0 deletions libraries/liveIntentIdSystem/liveIntentIdHubSystem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { logError } from '../../src/utils.js';
peixunzhang marked this conversation as resolved.
Show resolved Hide resolved
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 }
}
3link marked this conversation as resolved.
Show resolved Hide resolved

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 = {
peixunzhang marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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?.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?.params ?? {};

const clientRef = initializeClient(configParams)

return { callback: function(cb) { resolve(configParams, clientRef, cb); } };
},
eids: {
eids
}
};

submodule('userId', liveIntentIdHubSubmodule);
224 changes: 224 additions & 0 deletions libraries/liveIntentIdSystem/liveIntentIdSystem.js
Original file line number Diff line number Diff line change
@@ -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 { gdprDataHandler, uspDataHandler, gppDataHandler } from '../../src/adapterManager.js';
import { submodule } from '../../src/hook.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
*/

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
};

submodule('userId', liveIntentIdSubmodule);
Loading