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

userId - Add support for refreshing the cached user id #4082

Merged
merged 7 commits into from
Sep 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions integrationExamples/gpt/userId_example.html
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,13 @@
}, {
name: "id5Id",
params: {
partner: 173 // @TODO: Set your real ID5 partner ID here for production, please ask for one contact@id5.io
partner: 173 //Set your real ID5 partner ID here for production, please ask for one at http://id5.io/prebid
},
storage: {
type: "cookie",
name: "id5id",
expires: 90
expires: 90,
refreshInSeconds: 8*3600 // Refresh frequency of cookies, defaulting to 'expires'
},

}, {
Expand Down
20 changes: 11 additions & 9 deletions modules/id5IdSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ export const id5IdSubmodule = {
name: 'id5Id',
/**
* decode the stored id value for passing to bid requests
* @function
* @param {{ID5ID:Object}} value
* @returns {{id5id:String}}
* @function decode
* @param {(Object|string)} value
* @returns {(Object|undefined)}
*/
decode(value) {
return (value && typeof value['ID5ID'] === 'string') ? { 'id5id': value['ID5ID'] } : undefined;
Expand All @@ -30,16 +30,18 @@ export const id5IdSubmodule = {
* @function
* @param {SubmoduleParams} [configParams]
* @param {ConsentData} [consentData]
* @returns {function(callback:function)}
* @param {(Object|undefined)} cacheIdObj
* @returns {(Object|function(callback:function))}
*/
getId(configParams, consentData) {
getId(configParams, consentData, cacheIdObj) {
if (!configParams || typeof configParams.partner !== 'number') {
utils.logError(`User ID - ID5 submodule requires partner to be defined as a number`);
return;
return undefined;
}
const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0;
const hasGdpr = (typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0;
const gdprConsentString = hasGdpr ? consentData.consentString : '';
const url = `https://id5-sync.com/g/v1/${configParams.partner}.json?gdpr=${hasGdpr}&gdpr_consent=${gdprConsentString}`;
const storedUserId = this.decode(cacheIdObj);
const url = `https://id5-sync.com/g/v1/${configParams.partner}.json?1puid=${storedUserId ? storedUserId.id5id : ''}&gdpr=${hasGdpr}&gdpr_consent=${gdprConsentString}`;

return function (callback) {
ajax(url, response => {
Expand All @@ -52,7 +54,7 @@ export const id5IdSubmodule = {
}
}
callback(responseObj);
}, undefined, { method: 'GET' });
}, undefined, { method: 'GET', withCredentials: true });
}
}
};
Expand Down
49 changes: 32 additions & 17 deletions modules/userId/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* @name Submodule#getId
* @param {SubmoduleParams} configParams
* @param {ConsentData} consentData
* @param {(Object|undefined)} cacheIdObj
* @return {(Object|function)} id data or a callback, the callback is called on the auction end event
*/

Expand Down Expand Up @@ -43,7 +44,8 @@
* @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|undefined)} expires - time to live for browser cookie
* @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
*/

/**
Expand Down Expand Up @@ -112,18 +114,23 @@ export function setSubmoduleRegistry(submodules) {

/**
* @param {SubmoduleStorage} storage
* @param {string} value
* @param {(number|string)} expires
* @param {(Object|string)} value
*/
function setStoredValue(storage, value, expires) {
function setStoredValue(storage, value) {
try {
const valueStr = utils.isPlainObject(value) ? JSON.stringify(value) : value;
const expiresStr = (new Date(Date.now() + (expires * (60 * 60 * 24 * 1000)))).toUTCString();
const expiresStr = (new Date(Date.now() + (storage.expires * (60 * 60 * 24 * 1000)))).toUTCString();
if (storage.type === COOKIE) {
utils.setCookie(storage.name, valueStr, expiresStr);
if (typeof storage.refreshInSeconds === 'number') {
utils.setCookie(`${storage.name}_last`, new Date().toUTCString(), expiresStr);
}
} else if (storage.type === LOCAL_STORAGE) {
localStorage.setItem(`${storage.name}_exp`, expiresStr);
localStorage.setItem(storage.name, encodeURIComponent(valueStr));
if (typeof storage.refreshInSeconds === 'number') {
localStorage.setItem(`${storage.name}_last`, new Date().toUTCString());
}
}
} catch (error) {
utils.logError(error);
Expand All @@ -132,21 +139,23 @@ function setStoredValue(storage, value, expires) {

/**
* @param {SubmoduleStorage} storage
* @param {String|undefined} key optional key of the value
* @returns {string}
*/
function getStoredValue(storage) {
function getStoredValue(storage, key = undefined) {
const storedKey = key ? `${storage.name}_${key}` : storage.name;
let storedValue;
try {
if (storage.type === COOKIE) {
storedValue = utils.getCookie(storage.name);
storedValue = utils.getCookie(storedKey);
} else if (storage.type === LOCAL_STORAGE) {
const storedValueExp = localStorage.getItem(`${storage.name}_exp`);
// empty string means no expiration set
if (storedValueExp === '') {
storedValue = localStorage.getItem(storage.name);
storedValue = localStorage.getItem(storedKey);
} else if (storedValueExp) {
if ((new Date(storedValueExp)).getTime() - Date.now() > 0) {
storedValue = decodeURIComponent(localStorage.getItem(storage.name));
storedValue = decodeURIComponent(localStorage.getItem(storedKey));
}
}
}
Expand Down Expand Up @@ -188,7 +197,7 @@ function processSubmoduleCallbacks(submodules) {
// if valid, id data should be saved to cookie/html storage
if (idObj) {
if (submodule.config.storage) {
setStoredValue(submodule.config.storage, idObj, submodule.config.storage.expires);
setStoredValue(submodule.config.storage, idObj);
}
// cache decoded value (this is copied to every adUnit bid)
submodule.idObj = submodule.submodule.decode(idObj);
Expand Down Expand Up @@ -244,15 +253,15 @@ function initializeSubmodulesAndExecuteCallbacks() {
if (typeof initializedSubmodules === 'undefined') {
initializedSubmodules = initSubmodules(submodules, gdprDataHandler.getConsentData());
if (initializedSubmodules.length) {
// list of sumodules that have callbacks that need to be executed
// list of submodules that have callbacks that need to be executed
const submodulesWithCallbacks = initializedSubmodules.filter(item => utils.isFn(item.callback));

if (submodulesWithCallbacks.length) {
// 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 dealy process with a setTimeout
// when syncDelay is zero, process callbacks now, otherwise delay process with a setTimeout
if (syncDelay > 0) {
setTimeout(function() {
processSubmoduleCallbacks(submodulesWithCallbacks);
Expand Down Expand Up @@ -314,16 +323,22 @@ function initSubmodules(submodules, consentData) {
if (storedId) {
// cache decoded value (this is copied to every adUnit bid)
submodule.idObj = submodule.submodule.decode(storedId);
} else {
}
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) {
// getId will return user id data or a function that will load the data
const getIdResult = submodule.submodule.getId(submodule.config.params, consentData);
const getIdResult = submodule.submodule.getId(submodule.config.params, consentData, storedId);

// If the getId result has a type of function, it is asynchronous and cannot be called until later
if (typeof getIdResult === 'function') {
submodule.callback = getIdResult;
} else {
} else if (getIdResult) {
// A getId result that is not a function is assumed to be valid user id data, which should be saved to users local storage or cookies
setStoredValue(submodule.config.storage, getIdResult, submodule.config.storage.expires);
setStoredValue(submodule.config.storage, getIdResult);
// cache decoded value (this is copied to every adUnit bid)
submodule.idObj = submodule.submodule.decode(getIdResult);
}
Expand All @@ -332,7 +347,7 @@ function initSubmodules(submodules, consentData) {
// cache decoded value (this is copied to every adUnit bid)
submodule.idObj = submodule.config.value;
} else {
const result = submodule.submodule.getId(submodule.config.params, consentData);
const result = submodule.submodule.getId(submodule.config.params, consentData, undefined);
if (typeof result === 'function') {
submodule.callback = result;
} else {
Expand Down
5 changes: 3 additions & 2 deletions modules/userId/userId.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ pbjs.setConfig({
}, {
name: "id5Id",
params: {
partner: 173 // @TODO: Set your real ID5 partner ID here for production, please ask for one at http://id5.io/prebid
partner: 173 //Set your real ID5 partner ID here for production, please ask for one at http://id5.io/prebid
},
storage: {
type: "cookie",
name: "id5id",
expires: 5
expires: 5, // Expiration of cookies in days
refreshInSeconds: 8*3600 // User Id cache lifetime in seconds, defaulting to 'expires'
}
}, {
name: 'identityLink',
Expand Down
74 changes: 44 additions & 30 deletions test/spec/modules/userId_spec.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import {
init,
requestBidsHook,
setSubmoduleRegistry,
syncDelay,
attachIdSystem
} from 'modules/userId/index.js';
import {attachIdSystem, init, requestBidsHook, setSubmoduleRegistry, syncDelay} from 'modules/userId/index.js';
import {config} from 'src/config';
import * as utils from 'src/utils';
import {unifiedIdSubmodule} from 'modules/userId/unifiedIdSystem';
import {pubCommonIdSubmodule} from 'modules/userId/pubCommonIdSystem';
import {id5IdSubmodule} from 'modules/id5IdSystem';
import {identityLinkSubmodule} from 'modules/identityLinkIdSystem';

let assert = require('chai').assert;
let expect = require('chai').expect;
const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT';
Expand All @@ -21,15 +16,20 @@ describe('User ID', function() {
userSync: {
syncDelay: 0,
userIds: [
(configArr1 && configArr1.length === 3) ? getStorageMock.apply(null, configArr1) : null,
(configArr2 && configArr2.length === 3) ? getStorageMock.apply(null, configArr2) : null,
(configArr3 && configArr3.length === 3) ? getStorageMock.apply(null, configArr3) : null,
(configArr4 && configArr4.length === 3) ? getStorageMock.apply(null, configArr4) : null
(configArr1 && configArr1.length >= 3) ? getStorageMock.apply(null, configArr1) : null,
(configArr2 && configArr2.length >= 3) ? getStorageMock.apply(null, configArr2) : null,
(configArr3 && configArr3.length >= 3) ? getStorageMock.apply(null, configArr3) : null,
(configArr4 && configArr4.length >= 3) ? getStorageMock.apply(null, configArr4) : null
].filter(i => i)}
}
}
function getStorageMock(name = 'pubCommonId', key = 'pubcid', type = 'cookie', expires = 30) {
return { name: name, storage: { name: key, type: type, expires: expires } }
function getStorageMock(name = 'pubCommonId', key = 'pubcid', type = 'cookie', expires = 30, refreshInSeconds) {
return { name: name, storage: { name: key, type: type, expires: expires, refreshInSeconds: refreshInSeconds } }
}
function getConfigValueMock(name, value) {
return {
userSync: { syncDelay: 0, userIds: [{ name: name, value: value }] }
}
}

function getAdUnitMock(code = 'adUnit-code') {
Expand Down Expand Up @@ -72,7 +72,7 @@ describe('User ID', function() {
let pubcid = utils.getCookie('pubcid');
expect(pubcid).to.be.null; // there should be no cookie initially

setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]);
setSubmoduleRegistry([pubCommonIdSubmodule]);
init(config);
config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie']));

Expand All @@ -98,7 +98,7 @@ describe('User ID', function() {
let pubcid1;
let pubcid2;

setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]);
setSubmoduleRegistry([pubCommonIdSubmodule]);
init(config);
config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie']));
requestBidsHook((config) => { innerAdUnits1 = config.adUnits }, {adUnits: adUnits1});
Expand All @@ -112,7 +112,7 @@ describe('User ID', function() {
});
});

setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]);
setSubmoduleRegistry([pubCommonIdSubmodule]);
init(config);
config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie']));
requestBidsHook((config) => { innerAdUnits2 = config.adUnits }, {adUnits: adUnits2});
Expand All @@ -133,7 +133,7 @@ describe('User ID', function() {
let adUnits = [getAdUnitMock()];
let innerAdUnits;

setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]);
setSubmoduleRegistry([pubCommonIdSubmodule]);
init(config);
config.setConfig(getConfigMock(['pubCommonId', 'pubcid_alt', 'cookie']));
requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits});
Expand Down Expand Up @@ -168,14 +168,14 @@ describe('User ID', function() {
});

it('fails initialization if opt out cookie exists', function () {
setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]);
setSubmoduleRegistry([pubCommonIdSubmodule]);
init(config);
config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie']));
expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - opt-out cookie found, exit module');
});

it('initializes if no opt out cookie exists', function () {
setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]);
setSubmoduleRegistry([pubCommonIdSubmodule]);
init(config);
config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie']));
expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - usersync config updated for 1 submodules');
Expand Down Expand Up @@ -236,7 +236,7 @@ describe('User ID', function() {
expect(typeof utils.logInfo.args[0]).to.equal('undefined');
});

it('config with 1 configurations should create 1 submodules', function () {
it('config with 1 configuration should create 1 submodule', function () {
setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]);
init(config);
config.setConfig(getConfigMock(['unifiedId', 'unifiedid', 'cookie']));
Expand Down Expand Up @@ -312,14 +312,7 @@ describe('User ID', function() {
it('test hook from pubcommonid config value object', function(done) {
setSubmoduleRegistry([pubCommonIdSubmodule]);
init(config);
config.setConfig({
usersync: {
syncDelay: 0,
userIds: [{
name: 'pubCommonId',
value: {'pubcidvalue': 'testpubcidvalue'}
}]}
});
config.setConfig(getConfigValueMock('pubCommonId', {'pubcidvalue': 'testpubcidvalue'}));

requestBidsHook(function() {
adUnits.forEach(unit => {
Expand Down Expand Up @@ -394,13 +387,16 @@ describe('User ID', function() {
}, {adUnits});
});

it('test hook from id5id cookies', function(done) {
it('test hook from id5id cookies when refresh needed', function(done) {
// simulate existing browser local storage values
utils.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString()));
utils.setCookie('id5id_last', (new Date(Date.now() - 7200 * 1000)).toUTCString(), (new Date(Date.now() + 5000).toUTCString()));

sinon.stub(utils, 'logError'); // getId should failed with a logError as it has no partnerId

setSubmoduleRegistry([id5IdSubmodule]);
init(config);
config.setConfig(getConfigMock(['id5Id', 'id5id', 'cookie']));
config.setConfig(getConfigMock(['id5Id', 'id5id', 'cookie', 10, 3600]));

requestBidsHook(function() {
adUnits.forEach(unit => {
Expand All @@ -409,7 +405,25 @@ describe('User ID', function() {
expect(bid.userId.id5id).to.equal('testid5id');
});
});
sinon.assert.calledOnce(utils.logError);
utils.setCookie('id5id', '', EXPIRED_COOKIE_DATE);
utils.logError.restore();
done();
}, {adUnits});
});

it('test hook from id5id value-based config', function(done) {
setSubmoduleRegistry([id5IdSubmodule]);
init(config);
config.setConfig(getConfigValueMock('id5Id', {'id5id': 'testid5id'}));

requestBidsHook(function() {
adUnits.forEach(unit => {
unit.bids.forEach(bid => {
expect(bid).to.have.deep.nested.property('userId.id5id');
expect(bid.userId.id5id).to.equal('testid5id');
});
});
done();
}, {adUnits});
});
Expand Down