From 302411b3c98fc4d5bbcaf8a292271d3acf1facfd Mon Sep 17 00:00:00 2001 From: Nick-Merkle <105746498+Nick-Merkle@users.noreply.github.com> Date: Tue, 7 Jun 2022 09:18:05 -0400 Subject: [PATCH] Added support for IDs for multiple sources (#8499) Co-authored-by: Nick Curry --- modules/merkleIdSystem.js | 74 ++++++++++-------- modules/userId/eids.js | 30 ++++++-- test/spec/modules/eids_spec.js | 51 ++++++++++++- test/spec/modules/merkleIdSystem_spec.js | 95 +++++++++++++++--------- test/spec/modules/userId_spec.js | 28 ++++++- 5 files changed, 201 insertions(+), 77 deletions(-) diff --git a/modules/merkleIdSystem.js b/modules/merkleIdSystem.js index 352c2d074e8..19602c27093 100644 --- a/modules/merkleIdSystem.js +++ b/modules/merkleIdSystem.js @@ -11,7 +11,7 @@ import {submodule} from '../src/hook.js' import {getStorageManager} from '../src/storageManager.js'; const MODULE_NAME = 'merkleId'; -const ID_URL = 'https://id2.sv.rkdms.com/identity/'; +const ID_URL = 'https://prebid.sv.rkdms.com/identity/'; const DEFAULT_REFRESH = 7 * 3600; const SESSION_COOKIE_NAME = '_svsid'; @@ -30,19 +30,19 @@ function getSession(configParams) { function setCookie(name, value, expires) { let expTime = new Date(); expTime.setTime(expTime.getTime() + expires * 1000 * 60); - storage.setCookie(name, value, expTime.toUTCString()); + storage.setCookie(name, value, expTime.toUTCString(), 'Lax'); } function setSession(storage, response) { - logInfo('Merkle setting session '); - if (response && response.c && response.c.value && typeof response.c.value === 'string') { - setCookie(SESSION_COOKIE_NAME, response.c.value, storage.expires); + logInfo('Merkle setting ' + `${SESSION_COOKIE_NAME}`); + if (response && response[SESSION_COOKIE_NAME] && typeof response[SESSION_COOKIE_NAME] === 'string') { + setCookie(SESSION_COOKIE_NAME, response[SESSION_COOKIE_NAME], storage.expires); } } function constructUrl(configParams) { const session = getSession(configParams); - let url = configParams.endpoint + `?vendor=${configParams.vendor}&sv_cid=${configParams.sv_cid}&sv_domain=${configParams.sv_domain}&sv_pubid=${configParams.sv_pubid}`; + let url = configParams.endpoint + `?sv_domain=${configParams.sv_domain}&sv_pubid=${configParams.sv_pubid}&ssp_ids=${configParams.ssp_ids.join()}`; if (session) { url = `${url}&sv_session=${session}`; } @@ -86,45 +86,52 @@ function generateId(configParams, configStorage) { /** @type {Submodule} */ export const merkleIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, + /** - * decode the stored id value for passing to bid requests - * @function - * @param {string} value - * @returns {{merkleId:string}} - */ + * decode the stored id value for passing to bid requests + * @function + * @param {string} value + * @returns {{eids:arrayofields}} + */ decode(value) { + // Legacy support for a single id const id = (value && value.pam_id && typeof value.pam_id.id === 'string') ? value.pam_id : undefined; logInfo('Merkle id ' + JSON.stringify(id)); - return id ? {'merkleId': id} : undefined; + + if (id) { + return {'merkleId': id} + } + + // Supports multiple IDs for different SSPs + const merkleIds = (value && value?.merkleId && Array.isArray(value.merkleId)) ? value.merkleId : undefined; + logInfo('merkleIds: ' + JSON.stringify(merkleIds)); + + return merkleIds ? {'merkleId': merkleIds} : undefined; }, + /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} [config] - * @param {ConsentData} [consentData] - * @returns {IdResponse|undefined} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @param {ConsentData} [consentData] + * @returns {IdResponse|undefined} + */ getId(config, consentData) { logInfo('User ID - merkleId generating id'); const configParams = (config && config.params) || {}; - if (!configParams || typeof configParams.vendor !== 'string') { - logError('User ID - merkleId submodule requires a valid vendor to be defined'); - return; - } - - if (typeof configParams.sv_cid !== 'string') { - logError('User ID - merkleId submodule requires a valid sv_cid string to be defined'); + if (typeof configParams.sv_pubid !== 'string') { + logError('User ID - merkleId submodule requires a valid sv_pubid string to be defined'); return; } - if (typeof configParams.sv_pubid !== 'string') { - logError('User ID - merkleId submodule requires a valid sv_pubid string to be defined'); + if (!Array.isArray(configParams.ssp_ids)) { + logError('User ID - merkleId submodule requires a valid ssp_ids array to be defined'); return; } @@ -132,6 +139,7 @@ export const merkleIdSubmodule = { logError('User ID - merkleId submodule does not currently handle consent strings'); return; } + if (typeof configParams.endpoint !== 'string') { logWarn('User ID - merkleId submodule endpoint string is not defined'); configParams.endpoint = ID_URL @@ -146,7 +154,7 @@ export const merkleIdSubmodule = { return {callback: resp}; }, extendId: function (config = {}, consentData, storedId) { - logInfo('User ID - merkleId stored id ' + storedId); + logInfo('User ID - stored id ' + storedId); const configParams = (config && config.params) || {}; if (typeof configParams.endpoint !== 'string') { @@ -162,15 +170,18 @@ export const merkleIdSubmodule = { if (typeof configParams.sv_domain !== 'string') { configParams.sv_domain = merkleIdSubmodule.findRootDomain(); } + const configStorage = (config && config.storage) || {}; if (configStorage && configStorage.refreshInSeconds && typeof configParams.refreshInSeconds === 'number') { return {id: storedId}; } + let refreshInSeconds = DEFAULT_REFRESH; if (configParams && configParams.refreshInSeconds && typeof configParams.refreshInSeconds === 'number') { refreshInSeconds = configParams.refreshInSeconds; logInfo('User ID - merkleId param refreshInSeconds' + refreshInSeconds); } + const storedDate = new Date(storedId.date); let refreshNeeded = false; if (storedDate) { @@ -181,6 +192,7 @@ export const merkleIdSubmodule = { return {callback: resp}; } } + logInfo('User ID - merkleId not refreshed'); return {id: storedId}; } diff --git a/modules/userId/eids.js b/modules/userId/eids.js index 2f076088c3e..1713699e4f2 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -151,15 +151,25 @@ export const USER_IDS_CONFIG = { // merkleId 'merkleId': { - source: 'merkleinc.com', atype: 3, + getSource: function(data) { + if (data?.ext?.ssp) { + return `${data.ext.ssp}.merkleinc.com` + } + return 'merkleinc.com' + }, getValue: function(data) { return data.id; }, getUidExt: function(data) { - return (data && data.keyID) ? { - keyID: data.keyID - } : undefined; + if (data.keyID) { + return { + keyID: data.keyID + } + } + if (data.ext) { + return data.ext; + } } }, @@ -327,7 +337,8 @@ function createEidObject(userIdData, subModuleKey) { const conf = USER_IDS_CONFIG[subModuleKey]; if (conf && userIdData) { let eid = {}; - eid.source = conf['source']; + eid.source = isFn(conf['getSource']) ? conf['getSource'](userIdData) : conf['source']; + const value = isFn(conf['getValue']) ? conf['getValue'](userIdData) : userIdData; if (isStr(value)) { const uid = { id: value, atype: conf['atype'] }; @@ -357,10 +368,19 @@ function createEidObject(userIdData, subModuleKey) { // if any adapter does not want any particular userId to be passed then adapter can use Array.filter(e => e.source != 'tdid') export function createEidsArray(bidRequestUserId) { let eids = []; + for (const subModuleKey in bidRequestUserId) { if (bidRequestUserId.hasOwnProperty(subModuleKey)) { if (subModuleKey === 'pubProvidedId') { eids = eids.concat(bidRequestUserId['pubProvidedId']); + } else if (Array.isArray(bidRequestUserId[subModuleKey])) { + bidRequestUserId[subModuleKey].forEach((config, index, arr) => { + const eid = createEidObject(config, subModuleKey); + + if (eid) { + eids.push(eid); + } + }) } else { const eid = createEidObject(bidRequestUserId[subModuleKey], subModuleKey); if (eid) { diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index a92f7145747..1ba4eed2f29 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -82,20 +82,65 @@ describe('eids array generation for known sub-modules', function() { }); }); - it('merkleId', function() { + it('merkleId (legacy) - supports single id', function() { const userId = { merkleId: { id: 'some-random-id-value', keyID: 1 } }; const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); expect(newEids[0]).to.deep.equal({ source: 'merkleinc.com', + uids: [{ + id: 'some-random-id-value', + atype: 3, + ext: { keyID: 1 } + }] + }); + }); + + it('merkleId supports multiple source providers', function() { + const userId = { + merkleId: [{ + id: 'some-random-id-value', ext: { enc: 1, keyID: 16, idName: 'pamId', ssp: 'ssp1' } + }, { + id: 'another-random-id-value', + ext: { + enc: 1, + idName: 'pamId', + third: 4, + ssp: 'ssp2' + } + }] + } + + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(2); + expect(newEids[0]).to.deep.equal({ + source: 'ssp1.merkleinc.com', uids: [{id: 'some-random-id-value', atype: 3, - ext: { keyID: 1 - }}] + ext: { + enc: 1, + keyID: 16, + idName: 'pamId', + ssp: 'ssp1' + } + }] + }); + expect(newEids[1]).to.deep.equal({ + source: 'ssp2.merkleinc.com', + uids: [{id: 'another-random-id-value', + atype: 3, + ext: { + third: 4, + enc: 1, + idName: 'pamId', + ssp: 'ssp2' + } + }] }); }); diff --git a/test/spec/modules/merkleIdSystem_spec.js b/test/spec/modules/merkleIdSystem_spec.js index 63a2791ba3c..82c17336d20 100644 --- a/test/spec/modules/merkleIdSystem_spec.js +++ b/test/spec/modules/merkleIdSystem_spec.js @@ -7,9 +7,8 @@ import sinon from 'sinon'; let expect = require('chai').expect; const CONFIG_PARAMS = { - endpoint: 'https://test/id', - vendor: 'idsv2', - sv_cid: '5344_04531', + endpoint: undefined, + ssp_ids: ['ssp-1'], sv_pubid: '11314', sv_domain: 'www.testDomain.com', sv_session: 'testsession' @@ -38,6 +37,42 @@ function mockResponse( } describe('Merkle System', function () { + describe('merkleIdSystem.decode()', function() { + it('provides multiple Merkle IDs (EID) from a stored object', function() { + let storage = { + merkleId: [{ + id: 'some-random-id-value', ext: { enc: 1, keyID: 16, idName: 'pamId', ssp: 'ssp1' } + }, { + id: 'another-random-id-value', + ext: { + enc: 1, + idName: 'pamId', + third: 4, + ssp: 'ssp2' + } + }], + _svsid: 'some-identifier' + }; + + expect(merkleIdSubmodule.decode(storage)).to.deep.equal({ + merkleId: storage.merkleId + }); + }); + + it('can decode legacy stored object', function() { + let merkleId = {'pam_id': {'id': 'testmerkleId', 'keyID': 1}}; + + expect(merkleIdSubmodule.decode(merkleId)).to.deep.equal({ + merkleId: {'id': 'testmerkleId', 'keyID': 1} + }); + }) + + it('returns undefined', function() { + let merkleId = {}; + expect(merkleIdSubmodule.decode(merkleId)).to.be.undefined; + }) + }); + describe('Merkle System getId()', function () { const callbackSpy = sinon.spy(); let sandbox; @@ -59,60 +94,32 @@ describe('Merkle System', function () { ajaxStub.restore(); }); - it('getId() should fail on missing vendor', function () { - let config = { - params: { - ...CONFIG_PARAMS, - vendor: undefined - }, - storage: STORAGE_PARAMS - }; - - let submoduleCallback = merkleIdSubmodule.getId(config, undefined); - expect(submoduleCallback).to.be.undefined; - expect(utils.logError.args[0][0]).to.exist.and.to.equal('User ID - merkleId submodule requires a valid vendor to be defined'); - }); - - it('getId() should fail on missing vendor', function () { - let config = { - params: { - ...CONFIG_PARAMS, - vendor: undefined - }, - storage: STORAGE_PARAMS - }; - - let submoduleCallback = merkleIdSubmodule.getId(config, undefined); - expect(submoduleCallback).to.be.undefined; - expect(utils.logError.args[0][0]).to.exist.and.to.equal('User ID - merkleId submodule requires a valid vendor to be defined'); - }); - - it('getId() should fail on missing sv_cid', function () { + it('getId() should fail on missing sv_pubid', function () { let config = { params: { ...CONFIG_PARAMS, - sv_cid: undefined + sv_pubid: undefined }, storage: STORAGE_PARAMS }; let submoduleCallback = merkleIdSubmodule.getId(config, undefined); expect(submoduleCallback).to.be.undefined; - expect(utils.logError.args[0][0]).to.exist.and.to.equal('User ID - merkleId submodule requires a valid sv_cid string to be defined'); + expect(utils.logError.args[0][0]).to.exist.and.to.equal('User ID - merkleId submodule requires a valid sv_pubid string to be defined'); }); - it('getId() should fail on missing sv_pubid', function () { + it('getId() should fail on missing ssp_ids', function () { let config = { params: { ...CONFIG_PARAMS, - sv_pubid: undefined + ssp_ids: undefined }, storage: STORAGE_PARAMS }; let submoduleCallback = merkleIdSubmodule.getId(config, undefined); expect(submoduleCallback).to.be.undefined; - expect(utils.logError.args[0][0]).to.exist.and.to.equal('User ID - merkleId submodule requires a valid sv_pubid string to be defined'); + expect(utils.logError.args[0][0]).to.exist.and.to.equal('User ID - merkleId submodule requires a valid ssp_ids array to be defined'); }); it('getId() should warn on missing endpoint', function () { @@ -140,6 +147,20 @@ describe('Merkle System', function () { submoduleCallback(callbackSpy); expect(callbackSpy.calledOnce).to.be.true; }); + + it('getId() does not handle consent strings', function () { + let config = { + params: { + ...CONFIG_PARAMS, + ssp_ids: [] + }, + storage: STORAGE_PARAMS + }; + + let submoduleCallback = merkleIdSubmodule.getId(config, { gdprApplies: true }); + expect(submoduleCallback).to.be.undefined; + expect(utils.logError.args[0][0]).to.exist.and.to.equal('User ID - merkleId submodule does not currently handle consent strings'); + }); }); describe('Merkle System extendId()', function () { diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index a90056a7538..35242b92c0c 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -1821,7 +1821,7 @@ describe('User ID', function () { }, {adUnits}); }); - it('test hook from merkleId cookies', function (done) { + it('test hook from merkleId cookies - legacy', function (done) { // simulate existing browser local storage values coreStorage.setCookie('merkleId', JSON.stringify({'pam_id': {'id': 'testmerkleId', 'keyID': 1}}), (new Date(Date.now() + 5000).toUTCString())); @@ -1845,6 +1845,32 @@ describe('User ID', function () { }, {adUnits}); }); + it('test hook from merkleId cookies', function (done) { + // simulate existing browser local storage values + coreStorage.setCookie('merkleId', JSON.stringify({ + 'merkleId': [{id: 'testmerkleId', ext: { keyID: 1, ssp: 'ssp1' }}, {id: 'another-random-id-value', ext: { ssp: 'ssp2' }}], + '_svsid': 'svs-id-1' + }), (new Date(Date.now() + 5000).toUTCString())); + + init(config); + setSubmoduleRegistry([merkleIdSubmodule]); + config.setConfig(getConfigMock(['merkleId', 'merkleId', 'cookie'])); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.merkleId'); + expect(bid.userId.merkleId.length).to.equal(2); + expect(bid.userIdAsEids.length).to.equal(2); + expect(bid.userIdAsEids[0]).to.deep.equal({ source: 'ssp1.merkleinc.com', uids: [{id: 'testmerkleId', atype: 3, ext: { keyID: 1, ssp: 'ssp1' }}] }); + expect(bid.userIdAsEids[1]).to.deep.equal({ source: 'ssp2.merkleinc.com', uids: [{id: 'another-random-id-value', atype: 3, ext: { ssp: 'ssp2' }}] }); + }); + }); + coreStorage.setCookie('merkleId', '', EXPIRED_COOKIE_DATE); + done(); + }, {adUnits}); + }); + it('test hook from zeotapIdPlus cookies', function (done) { // simulate existing browser local storage values coreStorage.setCookie('IDP', btoa(JSON.stringify('abcdefghijk')), (new Date(Date.now() + 5000).toUTCString()));