mk9-prebid
Version:
Header Bidding Management Library
320 lines (285 loc) • 11.2 kB
JavaScript
/**
* This module adds ID5 to the User ID module
* The {@link module:modules/userId} module is required
* @module modules/id5IdSystem
* @requires module:modules/userId
*/
import * as utils from '../src/utils.js'
import { ajax } from '../src/ajax.js';
import { submodule } from '../src/hook.js';
import { getRefererInfo } from '../src/refererDetection.js';
import { getStorageManager } from '../src/storageManager.js';
import { uspDataHandler } from '../src/adapterManager.js';
const MODULE_NAME = 'id5Id';
const GVLID = 131;
const NB_EXP_DAYS = 30;
export const ID5_STORAGE_NAME = 'id5id';
export const ID5_PRIVACY_STORAGE_NAME = `${ID5_STORAGE_NAME}_privacy`;
const LOCAL_STORAGE = 'html5';
const LOG_PREFIX = 'User ID - ID5 submodule: ';
// order the legacy cookie names in reverse priority order so the last
// cookie in the array is the most preferred to use
const LEGACY_COOKIE_NAMES = [ 'pbjs-id5id', 'id5id.1st', 'id5id' ];
const storage = getStorageManager(GVLID, MODULE_NAME);
/** @type {Submodule} */
export const id5IdSubmodule = {
/**
* used to link submodule with config
* @type {string}
*/
name: 'id5Id',
/**
* Vendor id of ID5
* @type {Number}
*/
gvlid: GVLID,
/**
* decode the stored id value for passing to bid requests
* @function decode
* @param {(Object|string)} value
* @param {SubmoduleConfig|undefined} config
* @returns {(Object|undefined)}
*/
decode(value, config) {
let universalUid;
let linkType = 0;
if (value && typeof value.universal_uid === 'string') {
universalUid = value.universal_uid;
linkType = value.link_type || linkType;
} else {
return undefined;
}
let responseObj = {
id5id: {
uid: universalUid,
ext: {
linkType: linkType
}
}
};
const abTestingResult = utils.deepAccess(value, 'ab_testing.result');
switch (abTestingResult) {
case 'control':
// A/B Testing is enabled and user is in the Control Group
utils.logInfo(LOG_PREFIX + 'A/B Testing - user is in the Control Group: ID5 ID is NOT exposed');
utils.deepSetValue(responseObj, 'id5id.ext.abTestingControlGroup', true);
break;
case 'error':
// A/B Testing is enabled, but configured improperly, so skip A/B testing
utils.logError(LOG_PREFIX + 'A/B Testing ERROR! controlGroupPct must be a number >= 0 and <= 1');
break;
case 'normal':
// A/B Testing is enabled but user is not in the Control Group, so ID5 ID is shared
utils.logInfo(LOG_PREFIX + 'A/B Testing - user is NOT in the Control Group');
utils.deepSetValue(responseObj, 'id5id.ext.abTestingControlGroup', false);
break;
}
utils.logInfo(LOG_PREFIX + 'Decoded ID', responseObj);
return responseObj;
},
/**
* performs action to obtain id and return a value in the callback's response argument
* @function getId
* @param {SubmoduleConfig} config
* @param {ConsentData} consentData
* @param {(Object|undefined)} cacheIdObj
* @returns {IdResponse|undefined}
*/
getId(config, consentData, cacheIdObj) {
if (!hasRequiredConfig(config)) {
return undefined;
}
const url = `https://id5-sync.com/g/v2/${config.params.partner}.json`;
const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0;
const usp = uspDataHandler.getConsentData();
const referer = getRefererInfo();
const signature = (cacheIdObj && cacheIdObj.signature) ? cacheIdObj.signature : getLegacyCookieSignature();
const data = {
'partner': config.params.partner,
'gdpr': hasGdpr,
'nbPage': incrementNb(config.params.partner),
'o': 'pbjs',
'rf': referer.referer,
'top': referer.reachedTop ? 1 : 0,
'u': referer.stack[0] || window.location.href,
'v': '$prebid.version$'
};
// pass in optional data, but only if populated
if (hasGdpr && typeof consentData.consentString !== 'undefined' && !utils.isEmpty(consentData.consentString) && !utils.isEmptyStr(consentData.consentString)) {
data.gdpr_consent = consentData.consentString;
}
if (typeof usp !== 'undefined' && !utils.isEmpty(usp) && !utils.isEmptyStr(usp)) {
data.us_privacy = usp;
}
if (typeof signature !== 'undefined' && !utils.isEmptyStr(signature)) {
data.s = signature;
}
if (typeof config.params.pd !== 'undefined' && !utils.isEmptyStr(config.params.pd)) {
data.pd = config.params.pd;
}
if (typeof config.params.provider !== 'undefined' && !utils.isEmptyStr(config.params.provider)) {
data.provider = config.params.provider;
}
const abTestingConfig = getAbTestingConfig(config);
if (abTestingConfig.enabled === true) {
data.ab_testing = {
enabled: true,
control_group_pct: abTestingConfig.controlGroupPct // The server validates
};
}
const resp = function (callback) {
const callbacks = {
success: response => {
let responseObj;
if (response) {
try {
responseObj = JSON.parse(response);
utils.logInfo(LOG_PREFIX + 'response received from the server', responseObj);
resetNb(config.params.partner);
if (responseObj.privacy) {
storeInLocalStorage(ID5_PRIVACY_STORAGE_NAME, JSON.stringify(responseObj.privacy), NB_EXP_DAYS);
}
// TODO: remove after requiring publishers to use localstorage and
// all publishers have upgraded
if (config.storage.type === LOCAL_STORAGE) {
removeLegacyCookies(config.params.partner);
}
} catch (error) {
utils.logError(LOG_PREFIX + error);
}
}
callback(responseObj);
},
error: error => {
utils.logError(LOG_PREFIX + 'getId fetch encountered an error', error);
callback();
}
};
utils.logInfo(LOG_PREFIX + 'requesting an ID from the server', data);
ajax(url, callbacks, JSON.stringify(data), { method: 'POST', withCredentials: true });
};
return { callback: resp };
},
/**
* 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.
* @function extendId
* @param {SubmoduleConfig} config
* @param {ConsentData|undefined} consentData
* @param {Object} cacheIdObj - existing id, if any
* @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback.
*/
extendId(config, consentData, cacheIdObj) {
hasRequiredConfig(config);
const partnerId = (config && config.params && config.params.partner) || 0;
incrementNb(partnerId);
utils.logInfo(LOG_PREFIX + 'using cached ID', cacheIdObj);
return cacheIdObj;
}
};
function hasRequiredConfig(config) {
if (!config || !config.params || !config.params.partner || typeof config.params.partner !== 'number') {
utils.logError(LOG_PREFIX + 'partner required to be defined as a number');
return false;
}
if (!config.storage || !config.storage.type || !config.storage.name) {
utils.logError(LOG_PREFIX + 'storage required to be set');
return false;
}
// in a future release, we may return false if storage type or name are not set as required
if (config.storage.type !== LOCAL_STORAGE) {
utils.logWarn(LOG_PREFIX + `storage type recommended to be '${LOCAL_STORAGE}'. In a future release this may become a strict requirement`);
}
// in a future release, we may return false if storage type or name are not set as required
if (config.storage.name !== ID5_STORAGE_NAME) {
utils.logWarn(LOG_PREFIX + `storage name recommended to be '${ID5_STORAGE_NAME}'. In a future release this may become a strict requirement`);
}
return true;
}
export function expDaysStr(expDays) {
return (new Date(Date.now() + (1000 * 60 * 60 * 24 * expDays))).toUTCString();
}
export function nbCacheName(partnerId) {
return `${ID5_STORAGE_NAME}_${partnerId}_nb`;
}
export function storeNbInCache(partnerId, nb) {
storeInLocalStorage(nbCacheName(partnerId), nb, NB_EXP_DAYS);
}
export function getNbFromCache(partnerId) {
let cacheNb = getFromLocalStorage(nbCacheName(partnerId));
return (cacheNb) ? parseInt(cacheNb) : 0;
}
function incrementNb(partnerId) {
const nb = (getNbFromCache(partnerId) + 1);
storeNbInCache(partnerId, nb);
return nb;
}
function resetNb(partnerId) {
storeNbInCache(partnerId, 0);
}
function getLegacyCookieSignature() {
let legacyStoredValue;
LEGACY_COOKIE_NAMES.forEach(function(cookie) {
if (storage.getCookie(cookie)) {
legacyStoredValue = JSON.parse(storage.getCookie(cookie)) || legacyStoredValue;
}
});
return (legacyStoredValue && legacyStoredValue.signature) || '';
}
/**
* Remove our legacy cookie values. Needed until we move all publishers
* to html5 storage in a future release
* @param {integer} partnerId
*/
function removeLegacyCookies(partnerId) {
utils.logInfo(LOG_PREFIX + 'removing legacy cookies');
LEGACY_COOKIE_NAMES.forEach(function(cookie) {
storage.setCookie(`${cookie}`, ' ', expDaysStr(-1));
storage.setCookie(`${cookie}_nb`, ' ', expDaysStr(-1));
storage.setCookie(`${cookie}_${partnerId}_nb`, ' ', expDaysStr(-1));
storage.setCookie(`${cookie}_last`, ' ', expDaysStr(-1));
});
}
/**
* This will make sure we check for expiration before accessing local storage
* @param {string} key
*/
export function getFromLocalStorage(key) {
const storedValueExp = storage.getDataFromLocalStorage(`${key}_exp`);
// empty string means no expiration set
if (storedValueExp === '') {
return storage.getDataFromLocalStorage(key);
} else if (storedValueExp) {
if ((new Date(storedValueExp)).getTime() - Date.now() > 0) {
return storage.getDataFromLocalStorage(key);
}
}
// if we got here, then we have an expired item or we didn't set an
// expiration initially somehow, so we need to remove the item from the
// local storage
storage.removeDataFromLocalStorage(key);
return null;
}
/**
* Ensure that we always set an expiration in local storage since
* by default it's not required
* @param {string} key
* @param {any} value
* @param {integer} expDays
*/
export function storeInLocalStorage(key, value, expDays) {
storage.setDataInLocalStorage(`${key}_exp`, expDaysStr(expDays));
storage.setDataInLocalStorage(`${key}`, value);
}
/**
* gets the existing abTesting config or generates a default config with abTesting off
*
* @param {SubmoduleConfig|undefined} config
* @returns {Object} an object which always contains at least the property "enabled"
*/
function getAbTestingConfig(config) {
return utils.deepAccess(config, 'params.abTesting', { enabled: false });
}
submodule('userId', id5IdSubmodule);