Skip to content

Commit

Permalink
Force refresh userId (#5819)
Browse files Browse the repository at this point in the history
* Added global function for refreshing user id's

* Refactored submodule initialization to allow for refresh

* Added submodule initialization when refreshing user id's

* Refactored refresh parameter to be optional

Refactored refresh user id's parameter to be optional where an empty list will result in all modules being refreshed.

* Added unit tests for refresh user id's

* Added single module refresh test

* Test callback in refreshUserIds test

* Remove zeotapIdPlus expiration on cookie in test because it caused it to intermittently fail

Co-authored-by: chammon <chammon@rubiconproject.com>
  • Loading branch information
TLadd and chammon authored Oct 13, 2020
1 parent ad41a19 commit 77293a4
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 66 deletions.
154 changes: 106 additions & 48 deletions modules/userId/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@
* @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';
Expand Down Expand Up @@ -466,13 +471,112 @@ function getUserIdsAsEids() {
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()

const storedConsentData = getStoredConsentData();
setStoredConsentData(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;
}

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

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, 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
Expand All @@ -491,54 +595,7 @@ function initSubmodules(submodules, consentData) {
}

return userIdModules.reduce((carry, submodule) => {
// 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 || !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, 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); }
}
}
populateSubmoduleId(submodule, consentData, storedConsentData, false);
carry.push(submodule);
return carry;
}, []);
Expand Down Expand Up @@ -661,6 +718,7 @@ export function init(config) {
// 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
Expand Down
102 changes: 102 additions & 0 deletions test/spec/modules/userId_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,108 @@ describe('User ID', function () {
expect(typeof (getGlobal()).getUserIdsAsEids).to.equal('function');
expect((getGlobal()).getUserIdsAsEids()).to.deep.equal(createEidsArray((getGlobal()).getUserIds()));
});

it('pbjs.refreshUserIds refreshes', function() {
let sandbox = sinon.createSandbox();

let mockIdCallback = sandbox.stub().returns({id: {'MOCKID': '1111'}});

let mockIdSystem = {
name: 'mockId',
decode: function(value) {
return {
'mid': value['MOCKID']
};
},
getId: mockIdCallback
};

setSubmoduleRegistry([mockIdSystem]);
init(config);
config.setConfig({
userSync: {
syncDelay: 0,
userIds: [{
name: 'mockId',
value: {id: {mockId: '1111'}}
}]
}
});
expect(typeof (getGlobal()).refreshUserIds).to.equal('function');

getGlobal().getUserIds(); // force initialization

// update config so that getId will be called
config.setConfig({
userSync: {
syncDelay: 0,
userIds: [{
name: 'mockId',
storage: {name: 'mockid', type: 'cookie'},
}]
}
});

getGlobal().refreshUserIds();
expect(mockIdCallback.callCount).to.equal(1);
});

it('pbjs.refreshUserIds refreshes single', function() {
coreStorage.setCookie('MOCKID', '', EXPIRED_COOKIE_DATE);
coreStorage.setCookie('REFRESH', '', EXPIRED_COOKIE_DATE);

let sandbox = sinon.createSandbox();
let mockIdCallback = sandbox.stub().returns({id: {'MOCKID': '1111'}});
let refreshUserIdsCallback = sandbox.stub();

let mockIdSystem = {
name: 'mockId',
decode: function(value) {
return {
'mid': value['MOCKID']
};
},
getId: mockIdCallback
};

let refreshedIdCallback = sandbox.stub().returns({id: {'REFRESH': '1111'}});

let refreshedIdSystem = {
name: 'refreshedId',
decode: function(value) {
return {
'refresh': value['REFRESH']
};
},
getId: refreshedIdCallback
};

setSubmoduleRegistry([refreshedIdSystem, mockIdSystem]);
init(config);
config.setConfig({
userSync: {
syncDelay: 0,
userIds: [
{
name: 'mockId',
storage: {name: 'MOCKID', type: 'cookie'},
},
{
name: 'refreshedId',
storage: {name: 'refreshedid', type: 'cookie'},
}
]
}
});

getGlobal().getUserIds(); // force initialization

getGlobal().refreshUserIds({submoduleNames: 'refreshedId'}, refreshUserIdsCallback);

expect(refreshedIdCallback.callCount).to.equal(2);
expect(mockIdCallback.callCount).to.equal(1);
expect(refreshUserIdsCallback.callCount).to.equal(1);
});
});

describe('Opt out', function () {
Expand Down
41 changes: 23 additions & 18 deletions test/spec/modules/zeotapIdPlusIdSystem_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,34 +34,31 @@ function getAdUnitMock(code = 'adUnit-code') {
};
}

describe('Zeotap ID System', function() {
let getDataFromLocalStorageStub, localStorageIsEnabledStub;
let getCookieStub, cookiesAreEnabledStub;
beforeEach(function () {
getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage');
localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled');
getCookieStub = sinon.stub(storage, 'getCookie');
cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled');
});
function unsetCookie() {
storage.setCookie(ZEOTAP_COOKIE_NAME, '');
}

afterEach(function () {
getDataFromLocalStorageStub.restore();
localStorageIsEnabledStub.restore();
getCookieStub.restore();
cookiesAreEnabledStub.restore();
});
function unsetLocalStorage() {
storage.setDataInLocalStorage(ZEOTAP_COOKIE_NAME, '');
}

describe('Zeotap ID System', function() {
describe('test method: getId', function() {
afterEach(() => {
unsetCookie();
unsetLocalStorage();
});

it('provides the stored Zeotap id if a cookie exists', function() {
getCookieStub.withArgs(ZEOTAP_COOKIE_NAME).returns(ENCODED_ZEOTAP_COOKIE);
storage.setCookie(ZEOTAP_COOKIE_NAME, ENCODED_ZEOTAP_COOKIE);
let id = zeotapIdPlusSubmodule.getId();
expect(id).to.deep.equal({
id: ENCODED_ZEOTAP_COOKIE
});
});

it('provides the stored Zeotap id if cookie is absent but present in local storage', function() {
getDataFromLocalStorageStub.withArgs(ZEOTAP_COOKIE_NAME).returns(ENCODED_ZEOTAP_COOKIE);
storage.setDataInLocalStorage(ZEOTAP_COOKIE_NAME, ENCODED_ZEOTAP_COOKIE);
let id = zeotapIdPlusSubmodule.getId();
expect(id).to.deep.equal({
id: ENCODED_ZEOTAP_COOKIE
Expand Down Expand Up @@ -99,10 +96,18 @@ describe('Zeotap ID System', function() {

beforeEach(function() {
adUnits = [getAdUnitMock()];
storage.setCookie(
ZEOTAP_COOKIE_NAME,
ENCODED_ZEOTAP_COOKIE
);
setSubmoduleRegistry([zeotapIdPlusSubmodule]);
init(config);
config.setConfig(getConfigMock());
getCookieStub.withArgs(ZEOTAP_COOKIE_NAME).returns(ENCODED_ZEOTAP_COOKIE);
});

afterEach(function() {
unsetCookie();
unsetLocalStorage();
});

it('when a stored Zeotap ID exists it is added to bids', function(done) {
Expand Down

0 comments on commit 77293a4

Please sign in to comment.