diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js new file mode 100644 index 000000000000..59815f782fdd --- /dev/null +++ b/modules/parrableIdSystem.js @@ -0,0 +1,102 @@ +/** + * This module adds Parrable to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/parrableIdSystem + * @requires module:modules/userId + */ + +import * as utils from '../src/utils' +import {ajax} from '../src/ajax'; +import {submodule} from '../src/hook'; + +function buildUrl(config, currentStoredId) { + const endpoint = 'https://h.parrable.com/prebid'; + + const eid = currentStoreId ? currentStoredId : null; + + // @TODO: Any other parameters? Even null/empty ones? + const data = { + eid: eid, + trackers: config.partners + }; + + return endpoint + + '?data=' + btoa(JSON.stringify(data)) + + '&_rand=' + Math.random(); +}; + +function getOrRefreshId(configParams, consentData, currentStoredId) { + if (!configParams) { + utils.logError('User ID - parrableId submodule requires configParams'); + return; + } + if (configParams.partners && !Array.isArray(configParams.partners)) { + utils.logError('User ID - parrableId submodule requires partners to be an array'); + return; + } + if (!configParams.storage) { + utils.logError('User ID - parrableId submodule requires storage config'); + return; + } + // @TODO require storage type is cookie ??? + // @TODO require that cookie name is _parrable_eid ??? + + const url = buildUrl(configParams, currentStoredId); + return function (callback) { + ajax(url, response => { + let eid; + if (response) { + try { + let responseObj = JSON.parse(response); + eid = responseObj ? responseObj['eid'] : undefined; + } catch (error) { + utils.logError(error); + } + } + callback(eid); + }, undefined, { method: 'GET' }); + }; +}; + +/** @type {Submodule} */ +export const parrableIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: 'parrableId', + /** + * decode the stored id value for passing to bid requests + * @function + * @param {Object|string} value + * @return {(Object|undefined} + */ + decode(value) { + return (value && typeof value === 'string') ? { 'parrableid': value } : undefined; + }, + + /** + * performs action to refresh existing id and return a value in the callback's response argument + * @function + * @param {SubmoduleParams} [configParams] + * @param {ConsentData} [consentData] + * @param {Object|string} currentStoredId + * @returns {function(callback:function)} + */ + refreshId(configParams, consentData, currentStoredId) { + return getOrRefreshId(configParams, consentData, currentStoredId); + }, + + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleParams} [configParams] + * @param {ConsentData} [consentData] + * @returns {function(callback:function)} + */ + getId(configParams, consentData) { + return getOrRefreshId(configParams, consentData); + } +}; + +submodule('userId', parrableIdSubmodule); diff --git a/modules/userId/index.js b/modules/userId/index.js index 98d99f7d333c..491e75485d1d 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -16,6 +16,16 @@ * @return {(Object|function)} id data or a callback, the callback is called on the auction end event */ +/** + * @function + * @summary performs action to refresh stored id (only one of Submodule#getId or Submodule#refreshId will be called) + * @name Submodule#refreshId + * @param {SubmoduleParams} configParams + * @param {ConsentData} consentData + * @param {Object|string} currentStoredId + * @return {function} a callback, the callback is called on the auction end event + */ + /** * @function * @summary decode a stored value for passing to bid requests @@ -314,6 +324,16 @@ function initSubmodules(submodules, consentData) { if (storedId) { // cache decoded value (this is copied to every adUnit bid) submodule.idObj = submodule.submodule.decode(storedId); + + if (typeof submodule.submodule.refreshId === 'function') { + // if defined, refreshId will return a function that will load an updated id to be stored for subsequent use + const refreshIdResult = submodule.submodule.refreshId(submodule.config.params, consentData, storedId); + + // a function to be called later is expected, otherwise ignore + if (typeof refreshIdResult === 'function') { + submodule.callback = refreshIdResult; + } + } } else { // getId will return user id data or a function that will load the data const getIdResult = submodule.submodule.getId(submodule.config.params, consentData); diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index b581873089a9..ceda49b73cbf 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -11,12 +11,16 @@ import {unifiedIdSubmodule} from 'modules/userId/unifiedIdSystem'; import {pubCommonIdSubmodule} from 'modules/userId/pubCommonIdSystem'; import {id5IdSubmodule} from 'modules/id5IdSystem'; import {identityLinkSubmodule} from 'modules/identityLinkSystem'; +import {parrableIdSubmodule} from 'modules/parrableIdSystem'; let assert = require('chai').assert; let expect = require('chai').expect; const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; +const parrableCookieName = '_parrable_eid'; +const parrableCookieValue = '01.1563917337.test-eid' + describe('User ID', function() { - function getConfigMock(configArr1, configArr2, configArr3, configArr4) { + function getConfigMock(configArr1, configArr2, configArr3, configArr4, configArr5) { return { userSync: { syncDelay: 0, @@ -24,7 +28,8 @@ describe('User ID', function() { (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 + (configArr4 && configArr4.length === 3) ? getStorageMock.apply(null, configArr4) : null, + (configArr5 && configArr5.length === 3) ? getStorageMock.apply(null, configArr5) : null ].filter(i => i)} } } @@ -72,7 +77,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, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, parrableIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'])); @@ -98,7 +103,7 @@ describe('User ID', function() { let pubcid1; let pubcid2; - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, parrableIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'])); requestBidsHook((config) => { innerAdUnits1 = config.adUnits }, {adUnits: adUnits1}); @@ -112,7 +117,7 @@ describe('User ID', function() { }); }); - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, parrableIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'])); requestBidsHook((config) => { innerAdUnits2 = config.adUnits }, {adUnits: adUnits2}); @@ -133,7 +138,7 @@ describe('User ID', function() { let adUnits = [getAdUnitMock()]; let innerAdUnits; - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, parrableIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid_alt', 'cookie'])); requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); @@ -168,14 +173,14 @@ describe('User ID', function() { }); it('fails initialization if opt out cookie exists', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, parrableIdSubmodule]); 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, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, parrableIdSubmodule]); 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'); @@ -194,7 +199,7 @@ describe('User ID', function() { }); it('handles config with no usersync object', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, parrableIdSubmodule]); init(config); config.setConfig({}); // usersync is undefined, and no logInfo message for 'User ID - usersync config updated' @@ -202,14 +207,14 @@ describe('User ID', function() { }); it('handles config with empty usersync object', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, parrableIdSubmodule]); init(config); config.setConfig({ usersync: {} }); expect(typeof utils.logInfo.args[0]).to.equal('undefined'); }); it('handles config with usersync and userIds that are empty objs', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, parrableIdSubmodule]); init(config); config.setConfig({ usersync: { @@ -220,7 +225,7 @@ describe('User ID', function() { }); it('handles config with usersync and userIds with empty names or that dont match a submodule.name', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, parrableIdSubmodule]); init(config); config.setConfig({ usersync: { @@ -237,15 +242,15 @@ describe('User ID', function() { }); it('config with 1 configurations should create 1 submodules', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, parrableIdSubmodule]); init(config); config.setConfig(getConfigMock(['unifiedId', 'unifiedid', 'cookie'])); expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - usersync config updated for 1 submodules'); }); - it('config with 4 configurations should result in 4 submodules add', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); + it('config with 5 configurations should result in 5 submodules add', function () { + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, parrableIdSubmodule]); init(config); config.setConfig({ usersync: { @@ -261,14 +266,17 @@ describe('User ID', function() { }, { name: 'identityLink', storage: { name: 'idl_env', type: 'cookie' } + }, { + name: 'parrableId', + storage: { name: parrableCookieName, type: 'cookie' } }] } }); - expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - usersync config updated for 4 submodules'); + expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - usersync config updated for 5 submodules'); }); it('config syncDelay updates module correctly', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, parrableIdSubmodule]); init(config); config.setConfig({ usersync: { @@ -414,18 +422,40 @@ describe('User ID', function() { }, {adUnits}); }); + it('test hook from parrableId cookies', function(done) { + // simulate existing browser local storage values + utils.setCookie(parrableCookieName, parrableCookieValue, (new Date(Date.now() + 5000).toUTCString())); + + setSubmoduleRegistry([parrableIdSubmodule]); + init(config); + config.setConfig(getConfigMock(['parrableId', parrableCookieName, 'cookie'])); + + requestBidsHook(function() { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.parrableid'); + expect(bid.userId.parrableid).to.equal(parrableCookieValue); + }); + }); + utils.setCookie(parrableCookieName, '', EXPIRED_COOKIE_DATE); + done(); + }, {adUnits}); + }); + it('test hook when pubCommonId, unifiedId and id5Id have data to pass', function(done) { utils.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); utils.setCookie('unifiedid', JSON.stringify({'TDID': 'testunifiedid'}), (new Date(Date.now() + 5000).toUTCString())); utils.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); utils.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', (new Date(Date.now() + 5000).toUTCString())); + utils.setCookie(parrableCookieName, parrableCookieValue, (new Date(Date.now() + 5000).toUTCString())); - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, parrableIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], ['unifiedId', 'unifiedid', 'cookie'], ['id5Id', 'id5id', 'cookie'], - ['identityLink', 'idl_env', 'cookie'])); + ['identityLink', 'idl_env', 'cookie'], + ['parrableId', parrableCookieName, 'cookie'])); requestBidsHook(function() { adUnits.forEach(unit => { @@ -442,12 +472,16 @@ describe('User ID', function() { // check that identityLink id data was copied to bid expect(bid).to.have.deep.nested.property('userId.idl_env'); expect(bid.userId.idl_env).to.equal('AiGNC8Z5ONyZKSpIPf'); + // check that parrableId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.parrableid'); + expect(bid.userId.parrableid).to.equal(parrableCookieValue); }); }); utils.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); utils.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); utils.setCookie('id5id', '', EXPIRED_COOKIE_DATE); utils.setCookie('idl_env', '', EXPIRED_COOKIE_DATE); + utils.setCookie(parrableCookieName, '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); @@ -457,6 +491,7 @@ describe('User ID', function() { utils.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); utils.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); utils.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', new Date(Date.now() + 5000).toUTCString()); + utils.setCookie(parrableCookieName, parrableCookieValue, (new Date(Date.now() + 5000).toUTCString())); setSubmoduleRegistry([]); @@ -469,11 +504,13 @@ describe('User ID', function() { attachIdSystem(unifiedIdSubmodule); attachIdSystem(id5IdSubmodule); attachIdSystem(identityLinkSubmodule); + attachIdSystem(parrableIdSubmodule); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], ['unifiedId', 'unifiedid', 'cookie'], ['id5Id', 'id5id', 'cookie'], - ['identityLink', 'idl_env', 'cookie'])); + ['identityLink', 'idl_env', 'cookie'], + ['parrableId', parrableCookieName, 'cookie'])); requestBidsHook(function() { adUnits.forEach(unit => { @@ -490,12 +527,16 @@ describe('User ID', function() { // also check that identityLink id data was copied to bid expect(bid).to.have.deep.nested.property('userId.idl_env'); expect(bid.userId.idl_env).to.equal('AiGNC8Z5ONyZKSpIPf'); + // check that parrableId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.parrableid'); + expect(bid.userId.parrableid).to.equal(parrableCookieValue); }); }); utils.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); utils.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); utils.setCookie('id5id', '', EXPIRED_COOKIE_DATE); utils.setCookie('idl_env', '', EXPIRED_COOKIE_DATE); + utils.setCookie(parrableCookieName, '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); @@ -505,9 +546,10 @@ describe('User ID', function() { utils.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); utils.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); utils.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', new Date(Date.now() + 5000).toUTCString()); + utils.setCookie(parrableCookieName, parrableCookieValue, (new Date(Date.now() + 5000).toUTCString())); utils.setCookie('MOCKID', JSON.stringify({'MOCKID': '123456778'}), new Date(Date.now() + 5000).toUTCString()); - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, parrableIdSubmodule]); init(config); config.setConfig({ @@ -521,6 +563,8 @@ describe('User ID', function() { name: 'id5Id', storage: { name: 'id5id', type: 'cookie' } }, { name: 'identityLink', storage: { name: 'idl_env', type: 'cookie' } + }, { + name: 'parrableId', storage: { name: parrableCookieName, type: 'cookie' } }, { name: 'mockId', storage: { name: 'MOCKID', type: 'cookie' } }] @@ -555,6 +599,9 @@ describe('User ID', function() { // also check that identityLink id data was copied to bid expect(bid).to.have.deep.nested.property('userId.idl_env'); expect(bid.userId.idl_env).to.equal('AiGNC8Z5ONyZKSpIPf'); + // check that parrableId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.parrableid'); + expect(bid.userId.parrableid).to.equal(parrableCookieValue); // check MockId data was copied to bid expect(bid).to.have.deep.nested.property('userId.mid'); expect(bid.userId.mid).to.equal('123456778'); @@ -564,6 +611,7 @@ describe('User ID', function() { utils.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); utils.setCookie('id5id', '', EXPIRED_COOKIE_DATE); utils.setCookie('idl_env', '', EXPIRED_COOKIE_DATE); + utils.setCookie(parrableCookieName, '', EXPIRED_COOKIE_DATE); utils.setCookie('MOCKID', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits});