UNPKG

mk9-prebid

Version:

Header Bidding Management Library

861 lines (769 loc) 31.7 kB
/** * This module adds User ID support to prebid.js * @module modules/userId */ /** * @interface Submodule */ /** * @function * @summary performs action to obtain id and return a value in the callback's response argument. * If IdResponse#id is defined, then it will be written to the current active storage. * If IdResponse#callback is defined, then it'll called at the end of auction. * It's permissible to return neither, one, or both fields. * @name Submodule#getId * @param {SubmoduleConfig} config * @param {ConsentData|undefined} consentData * @param {(Object|undefined)} cacheIdObj * @return {(IdResponse|undefined)} A response object that contains id and/or callback. */ /** * @function * @summary Similar to Submodule#getId, this optional method returns response to for id that exists already. * If IdResponse#id is defined, then it will be written to the current active storage even if it exists already. * If IdResponse#callback is defined, then it'll called at the end of auction. * It's permissible to return neither, one, or both fields. * @name Submodule#extendId * @param {SubmoduleConfig} config * @param {ConsentData|undefined} consentData * @param {Object} storedId - existing id, if any * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. */ /** * @function * @summary decode a stored value for passing to bid requests * @name Submodule#decode * @param {Object|string} value * @param {SubmoduleConfig|undefined} config * @return {(Object|undefined)} */ /** * @property * @summary used to link submodule with config * @name Submodule#name * @type {string} */ /** * @property * @summary use a predefined domain override for cookies or provide your own * @name Submodule#domainOverride * @type {(undefined|function)} */ /** * @function * @summary Returns the root domain * @name Submodule#findRootDomain * @returns {string} */ /** * @typedef {Object} SubmoduleConfig * @property {string} name - the User ID submodule name (used to link submodule with config) * @property {(SubmoduleStorage|undefined)} storage - browser storage config * @property {(SubmoduleParams|undefined)} params - params config for use by the submodule.getId function * @property {(Object|undefined)} value - if not empty, this value is added to bid requests for access in adapters */ /** * @typedef {Object} SubmoduleStorage * @property {string} type - browser storage type (html5 or cookie) * @property {string} name - key name to use when saving/reading to local storage or cookies * @property {number} expires - time to live for browser storage in days * @property {(number|undefined)} refreshInSeconds - if not empty, this value defines the maximum time span in seconds before refreshing user ID stored in browser */ /** * @typedef {Object} LiveIntentCollectConfig * @property {(string|undefined)} fpiStorageStrategy - defines whether the first party identifiers that LiveConnect creates and updates are stored in a cookie jar, local storage, or not created at all * @property {(number|undefined)} fpiExpirationDays - the expiration time of an identifier created and updated by LiveConnect * @property {(string|undefined)} collectorUrl - defines where the LiveIntentId signal pixels are pointing to * @property {(string|undefined)} appId - the unique identifier of the application in question */ /** * @typedef {Object} SubmoduleParams * @property {(string|undefined)} partner - partner url param value * @property {(string|undefined)} url - webservice request url used to load Id data * @property {(string|undefined)} pixelUrl - publisher pixel to extend/modify cookies * @property {(boolean|undefined)} create - create id if missing. default is true. * @property {(boolean|undefined)} extend - extend expiration time on each access. default is false. * @property {(string|undefined)} pid - placement id url param value * @property {(string|undefined)} publisherId - the unique identifier of the publisher in question * @property {(string|undefined)} ajaxTimeout - the number of milliseconds a resolution request can take before automatically being terminated * @property {(array|undefined)} identifiersToResolve - the identifiers from either ls|cookie to be attached to the getId query * @property {(LiveIntentCollectConfig|undefined)} liCollectConfig - the config for LiveIntent's collect requests * @property {(string|undefined)} pd - publisher provided data for reconciling ID5 IDs * @property {(string|undefined)} emailHash - if provided, the hashed email address of a user * @property {(string|undefined)} notUse3P - use to retrieve envelope from 3p endpoint */ /** * @typedef {Object} SubmoduleContainer * @property {Submodule} submodule * @property {SubmoduleConfig} config * @property {(Object|undefined)} idObj - cache decoded id value (this is copied to every adUnit bid) * @property {(function|undefined)} callback - holds reference to submodule.getId() result if it returned a function. Will be set to undefined after callback executes */ /** * @typedef {Object} ConsentData * @property {(string|undefined)} consentString * @property {(Object|undefined)} vendorData * @property {(boolean|undefined)} gdprApplies */ /** * @typedef {Object} IdResponse * @property {(Object|undefined)} id - id data * @property {(function|undefined)} callback - function that will return an id */ /** * @typedef {Object} RefreshUserIdsOptions * @property {(string[]|undefined)} submoduleNames - submodules to refresh */ import find from 'core-js-pure/features/array/find.js'; import { config } from '../../src/config.js'; import events from '../../src/events.js'; import * as utils from '../../src/utils.js'; import { getGlobal } from '../../src/prebidGlobal.js'; import { gdprDataHandler } from '../../src/adapterManager.js'; import CONSTANTS from '../../src/constants.json'; import { module, hook } from '../../src/hook.js'; import { createEidsArray, buildEidPermissions } from './eids.js'; import { getCoreStorageManager } from '../../src/storageManager.js'; import {getPrebidInternal} from '../../src/utils.js'; import includes from 'core-js-pure/features/array/includes.js'; const MODULE_NAME = 'User ID'; const COOKIE = 'cookie'; const LOCAL_STORAGE = 'html5'; const DEFAULT_SYNC_DELAY = 500; const NO_AUCTION_DELAY = 0; const CONSENT_DATA_COOKIE_STORAGE_CONFIG = { name: '_pbjs_userid_consent_data', expires: 30 // 30 days expiration, which should match how often consent is refreshed by CMPs }; export const PBJS_USER_ID_OPTOUT_NAME = '_pbjs_id_optout'; export const coreStorage = getCoreStorageManager('userid'); /** @type {string[]} */ let validStorageTypes = []; /** @type {boolean} */ let addedUserIdHook = false; /** @type {SubmoduleContainer[]} */ let submodules = []; /** @type {SubmoduleContainer[]} */ let initializedSubmodules; /** @type {SubmoduleConfig[]} */ let configRegistry = []; /** @type {Submodule[]} */ let submoduleRegistry = []; /** @type {(number|undefined)} */ let timeoutID; /** @type {(number|undefined)} */ export let syncDelay; /** @type {(number|undefined)} */ export let auctionDelay; /** @param {Submodule[]} submodules */ export function setSubmoduleRegistry(submodules) { submoduleRegistry = submodules; } /** * @param {SubmoduleContainer} submodule * @param {(Object|string)} value */ export function setStoredValue(submodule, value) { /** * @type {SubmoduleStorage} */ const storage = submodule.config.storage; const domainOverride = (typeof submodule.submodule.domainOverride === 'function') ? submodule.submodule.domainOverride() : null; try { const valueStr = utils.isPlainObject(value) ? JSON.stringify(value) : value; const expiresStr = (new Date(Date.now() + (storage.expires * (60 * 60 * 24 * 1000)))).toUTCString(); if (storage.type === COOKIE) { coreStorage.setCookie(storage.name, valueStr, expiresStr, 'Lax', domainOverride); if (typeof storage.refreshInSeconds === 'number') { coreStorage.setCookie(`${storage.name}_last`, new Date().toUTCString(), expiresStr, 'Lax', domainOverride); } } else if (storage.type === LOCAL_STORAGE) { coreStorage.setDataInLocalStorage(`${storage.name}_exp`, expiresStr); coreStorage.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr)); if (typeof storage.refreshInSeconds === 'number') { coreStorage.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString()); } } } catch (error) { utils.logError(error); } } function setPrebidServerEidPermissions(initializedSubmodules) { let setEidPermissions = getPrebidInternal().setEidPermissions; if (typeof setEidPermissions === 'function' && utils.isArray(initializedSubmodules)) { setEidPermissions(buildEidPermissions(initializedSubmodules)); } } /** /** * @param {SubmoduleStorage} storage * @param {String|undefined} key optional key of the value * @returns {string} */ function getStoredValue(storage, key = undefined) { const storedKey = key ? `${storage.name}_${key}` : storage.name; let storedValue; try { if (storage.type === COOKIE) { storedValue = coreStorage.getCookie(storedKey); } else if (storage.type === LOCAL_STORAGE) { const storedValueExp = coreStorage.getDataFromLocalStorage(`${storage.name}_exp`); // empty string means no expiration set if (storedValueExp === '') { storedValue = coreStorage.getDataFromLocalStorage(storedKey); } else if (storedValueExp) { if ((new Date(storedValueExp)).getTime() - Date.now() > 0) { storedValue = decodeURIComponent(coreStorage.getDataFromLocalStorage(storedKey)); } } } // support storing a string or a stringified object if (typeof storedValue === 'string' && storedValue.trim().charAt(0) === '{') { storedValue = JSON.parse(storedValue); } } catch (e) { utils.logError(e); } return storedValue; } /** * makes an object that can be stored with only the keys we need to check. * excluding the vendorConsents object since the consentString is enough to know * if consent has changed without needing to have all the details in an object * @param consentData * @returns {{apiVersion: number, gdprApplies: boolean, consentString: string}} */ function makeStoredConsentDataHash(consentData) { const storedConsentData = { consentString: '', gdprApplies: false, apiVersion: 0 }; if (consentData) { storedConsentData.consentString = consentData.consentString; storedConsentData.gdprApplies = consentData.gdprApplies; storedConsentData.apiVersion = consentData.apiVersion; } return utils.cyrb53Hash(JSON.stringify(storedConsentData)); } /** * puts the current consent data into cookie storage * @param consentData */ export function setStoredConsentData(consentData) { try { const expiresStr = (new Date(Date.now() + (CONSENT_DATA_COOKIE_STORAGE_CONFIG.expires * (60 * 60 * 24 * 1000)))).toUTCString(); coreStorage.setCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name, makeStoredConsentDataHash(consentData), expiresStr, 'Lax'); } catch (error) { utils.logError(error); } } /** * get the stored consent data from local storage, if any * @returns {string} */ function getStoredConsentData() { try { return coreStorage.getCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name); } catch (e) { utils.logError(e); } } /** * test if the consent object stored locally matches the current consent data. if they * don't match or there is nothing stored locally, it means a refresh of the user id * submodule is needed * @param storedConsentData * @param consentData * @returns {boolean} */ function storedConsentDataMatchesConsentData(storedConsentData, consentData) { return ( typeof storedConsentData !== 'undefined' && storedConsentData !== null && storedConsentData === makeStoredConsentDataHash(consentData) ); } /** * 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.apiVersion === 1 && utils.deepAccess(consentData, 'vendorData.purposeConsents.1') === false) { return false; } if (consentData.apiVersion === 2 && utils.deepAccess(consentData, 'vendorData.purpose.consents.1') === false) { return false; } } return true; } /** * Find the root domain * @param {string|undefined} fullDomain * @return {string} */ export function findRootDomain(fullDomain = window.location.hostname) { if (!coreStorage.cookiesAreEnabled()) { return fullDomain; } const domainParts = fullDomain.split('.'); if (domainParts.length == 2) { return fullDomain; } let rootDomain; let continueSearching; let startIndex = -2; const TEST_COOKIE_NAME = `_rdc${Date.now()}`; const TEST_COOKIE_VALUE = 'writeable'; do { rootDomain = domainParts.slice(startIndex).join('.'); let expirationDate = new Date(utils.timestamp() + 10 * 1000).toUTCString(); // Write a test cookie coreStorage.setCookie( TEST_COOKIE_NAME, TEST_COOKIE_VALUE, expirationDate, 'Lax', rootDomain, undefined ); // See if the write was successful const value = coreStorage.getCookie(TEST_COOKIE_NAME, undefined); if (value === TEST_COOKIE_VALUE) { continueSearching = false; // Delete our test cookie coreStorage.setCookie( TEST_COOKIE_NAME, '', 'Thu, 01 Jan 1970 00:00:01 GMT', undefined, rootDomain, undefined ); } else { startIndex += -1; continueSearching = Math.abs(startIndex) <= domainParts.length; } } while (continueSearching); return rootDomain; } /** * @param {SubmoduleContainer[]} submodules * @param {function} cb - callback for after processing is done. */ function processSubmoduleCallbacks(submodules, cb) { let done = () => {}; if (cb) { done = utils.delayExecution(() => { clearTimeout(timeoutID); cb(); }, submodules.length); } submodules.forEach(function (submodule) { submodule.callback(function callbackCompleted(idObj) { // if valid, id data should be saved to cookie/html storage if (idObj) { if (submodule.config.storage) { setStoredValue(submodule, idObj); } // cache decoded value (this is copied to every adUnit bid) submodule.idObj = submodule.submodule.decode(idObj, submodule.config); } else { utils.logInfo(`${MODULE_NAME}: ${submodule.submodule.name} - request id responded with an empty value`); } done(); }); // clear callback, this prop is used to test if all submodule callbacks are complete below submodule.callback = undefined; }); } /** * This function will create a combined object for all subModule Ids * @param {SubmoduleContainer[]} submodules */ function getCombinedSubmoduleIds(submodules) { if (!Array.isArray(submodules) || !submodules.length) { return {}; } const combinedSubmoduleIds = submodules.filter(i => utils.isPlainObject(i.idObj) && Object.keys(i.idObj).length).reduce((carry, i) => { Object.keys(i.idObj).forEach(key => { carry[key] = i.idObj[key]; }); return carry; }, {}); return combinedSubmoduleIds; } /** * This function will create a combined object for bidder with allowed subModule Ids * @param {SubmoduleContainer[]} submodules * @param {string} bidder */ function getCombinedSubmoduleIdsForBidder(submodules, bidder) { if (!Array.isArray(submodules) || !submodules.length || !bidder) { return {}; } return submodules .filter(i => !i.config.bidders || !utils.isArray(i.config.bidders) || includes(i.config.bidders, bidder)) .filter(i => utils.isPlainObject(i.idObj) && Object.keys(i.idObj).length) .reduce((carry, i) => { Object.keys(i.idObj).forEach(key => { carry[key] = i.idObj[key]; }); return carry; }, {}); } /** * @param {AdUnit[]} adUnits * @param {SubmoduleContainer[]} submodules */ function addIdDataToAdUnitBids(adUnits, submodules) { if ([adUnits].some(i => !Array.isArray(i) || !i.length)) { return; } adUnits.forEach(adUnit => { if (adUnit.bids && utils.isArray(adUnit.bids)) { adUnit.bids.forEach(bid => { const combinedSubmoduleIds = getCombinedSubmoduleIdsForBidder(submodules, bid.bidder); if (Object.keys(combinedSubmoduleIds).length) { // create a User ID object on the bid, bid.userId = combinedSubmoduleIds; bid.userIdAsEids = createEidsArray(combinedSubmoduleIds); } }); } }); } /** * This is a common function that will initialize subModules if not already done and it will also execute subModule callbacks */ function initializeSubmodulesAndExecuteCallbacks(continueAuction) { let delayed = false; // initialize submodules only when undefined if (typeof initializedSubmodules === 'undefined') { initializedSubmodules = initSubmodules(submodules, gdprDataHandler.getConsentData()); if (initializedSubmodules.length) { setPrebidServerEidPermissions(initializedSubmodules); // list of submodules that have callbacks that need to be executed const submodulesWithCallbacks = initializedSubmodules.filter(item => utils.isFn(item.callback)); if (submodulesWithCallbacks.length) { if (continueAuction && auctionDelay > 0) { // delay auction until ids are available delayed = true; let continued = false; const continueCallback = function () { if (!continued) { continued = true; continueAuction(); } } utils.logInfo(`${MODULE_NAME} - auction delayed by ${auctionDelay} at most to fetch ids`); timeoutID = setTimeout(continueCallback, auctionDelay); processSubmoduleCallbacks(submodulesWithCallbacks, continueCallback); } else { // wait for auction complete before processing submodule callbacks events.on(CONSTANTS.EVENTS.AUCTION_END, function auctionEndHandler() { events.off(CONSTANTS.EVENTS.AUCTION_END, auctionEndHandler); // when syncDelay is zero, process callbacks now, otherwise delay process with a setTimeout if (syncDelay > 0) { setTimeout(function () { processSubmoduleCallbacks(submodulesWithCallbacks); }, syncDelay); } else { processSubmoduleCallbacks(submodulesWithCallbacks); } }); } } } } if (continueAuction && !delayed) { continueAuction(); } } /** * Hook is executed before adapters, but after consentManagement. Consent data is requied because * this module requires GDPR consent with Purpose #1 to save data locally. * The two main actions handled by the hook are: * 1. check gdpr consentData and handle submodule initialization. * 2. append user id data (loaded from cookied/html or from the getId method) to bids to be accessed in adapters. * @param {Object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. * @param {function} fn required; The next function in the chain, used by hook.js */ export function requestBidsHook(fn, reqBidsConfigObj) { // initialize submodules only when undefined initializeSubmodulesAndExecuteCallbacks(function () { // pass available user id data to bid adapters addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, initializedSubmodules); // calling fn allows prebid to continue processing fn.call(this, reqBidsConfigObj); }); } /** * This function will be exposed in global-name-space so that userIds stored by Prebid UserId module can be used by external codes as well. * Simple use case will be passing these UserIds to A9 wrapper solution */ function getUserIds() { // initialize submodules only when undefined initializeSubmodulesAndExecuteCallbacks(); return getCombinedSubmoduleIds(initializedSubmodules); } /** * This function will be exposed in global-name-space so that userIds stored by Prebid UserId module can be used by external codes as well. * Simple use case will be passing these UserIds to A9 wrapper solution */ function getUserIdsAsEids() { // initialize submodules only when undefined initializeSubmodulesAndExecuteCallbacks(); return createEidsArray(getCombinedSubmoduleIds(initializedSubmodules)); } /** * This function will be exposed in the global-name-space so that userIds can be refreshed after initialization. * @param {RefreshUserIdsOptions} options */ function refreshUserIds(options, callback) { let submoduleNames = options ? options.submoduleNames : null; if (!submoduleNames) { submoduleNames = []; } initializeSubmodulesAndExecuteCallbacks(function() { let consentData = gdprDataHandler.getConsentData() // gdpr consent with purpose one is required, otherwise exit immediately let {userIdModules, hasValidated} = validateGdprEnforcement(submodules, consentData); if (!hasValidated && !hasGDPRConsent(consentData)) { utils.logWarn(`${MODULE_NAME} - gdpr permission not valid for local storage or cookies, exit module`); return; } // we always want the latest consentData stored, even if we don't execute any submodules const storedConsentData = getStoredConsentData(); setStoredConsentData(consentData); let callbackSubmodules = []; for (let submodule of userIdModules) { if (submoduleNames.length > 0 && submoduleNames.indexOf(submodule.submodule.name) === -1) { continue; } utils.logInfo(`${MODULE_NAME} - refreshing ${submodule.submodule.name}`); populateSubmoduleId(submodule, consentData, storedConsentData, true); updateInitializedSubmodules(submodule); if (initializedSubmodules.length) { setPrebidServerEidPermissions(initializedSubmodules); } if (utils.isFn(submodule.callback)) { callbackSubmodules.push(submodule); } } if (callbackSubmodules.length > 0) { processSubmoduleCallbacks(callbackSubmodules); } if (callback) { callback(); } }); } /** * This hook returns updated list of submodules which are allowed to do get user id based on TCF 2 enforcement rules configured */ export const validateGdprEnforcement = hook('sync', function (submodules, consentData) { return { userIdModules: submodules, hasValidated: consentData && consentData.hasValidated }; }, 'validateGdprEnforcement'); function populateSubmoduleId(submodule, consentData, storedConsentData, forceRefresh) { // There are two submodule configuration types to handle: storage or value // 1. storage: retrieve user id data from cookie/html storage or with the submodule's getId method // 2. value: pass directly to bids if (submodule.config.storage) { let storedId = getStoredValue(submodule.config.storage); let response; let refreshNeeded = false; if (typeof submodule.config.storage.refreshInSeconds === 'number') { const storedDate = new Date(getStoredValue(submodule.config.storage, 'last')); refreshNeeded = storedDate && (Date.now() - storedDate.getTime() > submodule.config.storage.refreshInSeconds * 1000); } if (!storedId || refreshNeeded || forceRefresh || !storedConsentDataMatchesConsentData(storedConsentData, consentData)) { // No id previously saved, or a refresh is needed, or consent has changed. Request a new id from the submodule. response = submodule.submodule.getId(submodule.config, consentData, storedId); } else if (typeof submodule.submodule.extendId === 'function') { // If the id exists already, give submodule a chance to decide additional actions that need to be taken response = submodule.submodule.extendId(submodule.config, consentData, storedId); } if (utils.isPlainObject(response)) { if (response.id) { // A getId/extendId result assumed to be valid user id data, which should be saved to users local storage or cookies setStoredValue(submodule, response.id); storedId = response.id; } if (typeof response.callback === 'function') { // Save async callback to be invoked after auction submodule.callback = response.callback; } } if (storedId) { // cache decoded value (this is copied to every adUnit bid) submodule.idObj = submodule.submodule.decode(storedId, submodule.config); } } else if (submodule.config.value) { // cache decoded value (this is copied to every adUnit bid) submodule.idObj = submodule.config.value; } else { const response = submodule.submodule.getId(submodule.config, consentData, undefined); if (utils.isPlainObject(response)) { if (typeof response.callback === 'function') { submodule.callback = response.callback; } if (response.id) { submodule.idObj = submodule.submodule.decode(response.id, submodule.config); } } } } /** * @param {SubmoduleContainer[]} submodules * @param {ConsentData} consentData * @returns {SubmoduleContainer[]} initialized submodules */ function initSubmodules(submodules, consentData) { // gdpr consent with purpose one is required, otherwise exit immediately let { userIdModules, hasValidated } = validateGdprEnforcement(submodules, consentData); if (!hasValidated && !hasGDPRConsent(consentData)) { utils.logWarn(`${MODULE_NAME} - gdpr permission not valid for local storage or cookies, exit module`); return []; } // we always want the latest consentData stored, even if we don't execute any submodules const storedConsentData = getStoredConsentData(); setStoredConsentData(consentData); return userIdModules.reduce((carry, submodule) => { populateSubmoduleId(submodule, consentData, storedConsentData, false); carry.push(submodule); return carry; }, []); } function updateInitializedSubmodules(submodule) { let updated = false; for (let i = 0; i < initializedSubmodules.length; i++) { if (submodule.config.name.toLowerCase() === initializedSubmodules[i].config.name.toLowerCase()) { updated = true; initializedSubmodules[i] = submodule; break; } } if (!updated) { initializedSubmodules.push(submodule); } } /** * list of submodule configurations with valid 'storage' or 'value' obj definitions * * storage config: contains values for storing/retrieving User ID data in browser storage * * value config: object properties that are copied to bids (without saving to storage) * @param {SubmoduleConfig[]} configRegistry * @param {Submodule[]} submoduleRegistry * @param {string[]} activeStorageTypes * @returns {SubmoduleConfig[]} */ function getValidSubmoduleConfigs(configRegistry, submoduleRegistry, activeStorageTypes) { if (!Array.isArray(configRegistry)) { return []; } return configRegistry.reduce((carry, config) => { // every submodule config obj must contain a valid 'name' if (!config || utils.isEmptyStr(config.name)) { return carry; } // Validate storage config contains 'type' and 'name' properties with non-empty string values // 'type' must be a value currently enabled in the browser if (config.storage && !utils.isEmptyStr(config.storage.type) && !utils.isEmptyStr(config.storage.name) && activeStorageTypes.indexOf(config.storage.type) !== -1) { carry.push(config); } else if (utils.isPlainObject(config.value)) { carry.push(config); } else if (!config.storage && !config.value) { carry.push(config); } return carry; }, []); } /** * update submodules by validating against existing configs and storage types */ function updateSubmodules() { const configs = getValidSubmoduleConfigs(configRegistry, submoduleRegistry, validStorageTypes); if (!configs.length) { return; } // do this to avoid reprocessing submodules const addedSubmodules = submoduleRegistry.filter(i => !find(submodules, j => j.name === i.name)); // find submodule and the matching configuration, if found create and append a SubmoduleContainer submodules = addedSubmodules.map(i => { const submoduleConfig = find(configs, j => j.name && (j.name.toLowerCase() === i.name.toLowerCase() || (i.aliasName && j.name.toLowerCase() === i.aliasName.toLowerCase()))); if (submoduleConfig && i.name !== submoduleConfig.name) submoduleConfig.name = i.name; i.findRootDomain = findRootDomain; return submoduleConfig ? { submodule: i, config: submoduleConfig, callback: undefined, idObj: undefined } : null; }).filter(submodule => submodule !== null); if (!addedUserIdHook && submodules.length) { // priority value 40 will load after consentManagement with a priority of 50 getGlobal().requestBids.before(requestBidsHook, 40); utils.logInfo(`${MODULE_NAME} - usersync config updated for ${submodules.length} submodules: `, submodules.map(a => a.submodule.name)); addedUserIdHook = true; } } /** * enable submodule in User ID * @param {Submodule} submodule */ export function attachIdSystem(submodule) { if (!find(submoduleRegistry, i => i.name === submodule.name)) { submoduleRegistry.push(submodule); updateSubmodules(); } } /** * test browser support for storage config types (local storage or cookie), initializes submodules but consentManagement is required, * so a callback is added to fire after the consentManagement module. * @param {{getConfig:function}} config */ export function init(config) { submodules = []; configRegistry = []; addedUserIdHook = false; initializedSubmodules = undefined; // list of browser enabled storage types validStorageTypes = [ coreStorage.localStorageIsEnabled() ? LOCAL_STORAGE : null, coreStorage.cookiesAreEnabled() ? COOKIE : null ].filter(i => i !== null); // exit immediately if opt out cookie or local storage keys exists. if (validStorageTypes.indexOf(COOKIE) !== -1 && coreStorage.getCookie(PBJS_USER_ID_OPTOUT_NAME)) { utils.logInfo(`${MODULE_NAME} - opt-out cookie found, exit module`); return; } if (validStorageTypes.indexOf(LOCAL_STORAGE) !== -1 && coreStorage.getDataFromLocalStorage(PBJS_USER_ID_OPTOUT_NAME)) { utils.logInfo(`${MODULE_NAME} - opt-out localStorage found, exit module`); return; } // listen for config userSyncs to be set config.getConfig(conf => { // Note: support for 'usersync' was dropped as part of Prebid.js 4.0 const userSync = conf.userSync; if (userSync && userSync.userIds) { configRegistry = userSync.userIds; syncDelay = utils.isNumber(userSync.syncDelay) ? userSync.syncDelay : DEFAULT_SYNC_DELAY; auctionDelay = utils.isNumber(userSync.auctionDelay) ? userSync.auctionDelay : NO_AUCTION_DELAY; updateSubmodules(); } }); // exposing getUserIds function in global-name-space so that userIds stored in Prebid can be used by external codes. (getGlobal()).getUserIds = getUserIds; (getGlobal()).getUserIdsAsEids = getUserIdsAsEids; (getGlobal()).refreshUserIds = refreshUserIds; } // init config update listener to start the application init(config); module('userId', attachIdSystem);