From 34824bb07b94efe03c5f80f282145703bf3a8d26 Mon Sep 17 00:00:00 2001 From: Joshua Poritz Date: Thu, 9 Nov 2023 14:59:30 -0500 Subject: [PATCH 1/8] Introduce first-party ID support to 33Across User ID sub-module. Resolves IDG-1216. --- modules/33acrossIdSystem.js | 42 ++++++--- test/spec/modules/33acrossIdSystem_spec.js | 99 +++++++++++++++++++++- 2 files changed, 129 insertions(+), 12 deletions(-) diff --git a/modules/33acrossIdSystem.js b/modules/33acrossIdSystem.js index 0f370237e21..c9703682d7c 100644 --- a/modules/33acrossIdSystem.js +++ b/modules/33acrossIdSystem.js @@ -9,32 +9,42 @@ import { logMessage, logError } from '../src/utils.js'; import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { uspDataHandler, coppaDataHandler, gppDataHandler } from '../src/adapterManager.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; const MODULE_NAME = '33acrossId'; const API_URL = 'https://lexicon.33across.com/v1/envelope'; const AJAX_TIMEOUT = 10000; const CALLER_NAME = 'pbjs'; +const GVLID = 58; -function getEnvelope(response) { +const STORAGE_FPID_KEY = '33acrossIdFp'; + +export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); + +function calculateResponseObj(response) { if (!response.succeeded) { if (response.error == 'Cookied User') { logMessage(`${MODULE_NAME}: Unsuccessful response`.concat(' ', response.error)); } else { logError(`${MODULE_NAME}: Unsuccessful response`.concat(' ', response.error)); } - return; + return {}; } if (!response.data.envelope) { logMessage(`${MODULE_NAME}: No envelope was received`); - return; + return {}; } - return response.data.envelope; + return { + envelope: response.data.envelope, + fp: response.data.fp + }; } -function calculateQueryStringParams(pid, gdprConsentData) { +function calculateQueryStringParams(pid, gdprConsentData, storedId) { const uspString = uspDataHandler.getConsentData(); const gdprApplies = Boolean(gdprConsentData?.gdprApplies); const coppaValue = coppaDataHandler.getCoppa(); @@ -63,6 +73,11 @@ function calculateQueryStringParams(pid, gdprConsentData) { params.gdpr_consent = gdprConsentData.consentString; } + const fp = storage.getDataFromLocalStorage(STORAGE_FPID_KEY); + if (storedId && fp) { + params.fp = fp; + } + return params; } @@ -74,7 +89,7 @@ export const thirthyThreeAcrossIdSubmodule = { */ name: MODULE_NAME, - gvlid: 58, + gvlid: GVLID, /** * decode the stored id value for passing to bid requests @@ -96,7 +111,7 @@ export const thirthyThreeAcrossIdSubmodule = { * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId({ params = { } }, gdprConsentData) { + getId({ params = { } }, gdprConsentData, storedId) { if (typeof params.pid !== 'string') { logError(`${MODULE_NAME}: Submodule requires a partner ID to be defined`); @@ -109,21 +124,26 @@ export const thirthyThreeAcrossIdSubmodule = { callback(cb) { ajaxBuilder(AJAX_TIMEOUT)(apiUrl, { success(response) { - let envelope; + let responseObj = { }; try { - envelope = getEnvelope(JSON.parse(response)) + responseObj = calculateResponseObj(JSON.parse(response)); } catch (err) { logError(`${MODULE_NAME}: ID reading error:`, err); } - cb(envelope); + + if (responseObj.fp) { + storage.setDataInLocalStorage(STORAGE_FPID_KEY, responseObj.fp); + } + + cb(responseObj.envelope); }, error(err) { logError(`${MODULE_NAME}: ID error response`, err); cb(); } - }, calculateQueryStringParams(pid, gdprConsentData), { method: 'GET', withCredentials: true }); + }, calculateQueryStringParams(pid, gdprConsentData, storedId), { method: 'GET', withCredentials: true }); } }; }, diff --git a/test/spec/modules/33acrossIdSystem_spec.js b/test/spec/modules/33acrossIdSystem_spec.js index 4f6d7c4a6c5..d0ad16c531c 100644 --- a/test/spec/modules/33acrossIdSystem_spec.js +++ b/test/spec/modules/33acrossIdSystem_spec.js @@ -1,4 +1,4 @@ -import { thirthyThreeAcrossIdSubmodule } from 'modules/33acrossIdSystem.js'; +import { thirthyThreeAcrossIdSubmodule, storage } from 'modules/33acrossIdSystem.js'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; @@ -50,6 +50,36 @@ describe('33acrossIdSystem', () => { expect(completeCallback.calledOnceWithExactly('foo')).to.be.true; }); + it('stores the first-party ID provided in the response', () => { + const completeCallback = () => {}; + const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setDataInLocalStorage.calledOnceWithExactly('33acrossIdFp', 'bar')).to.be.true; + + setDataInLocalStorage.restore(); + }); + context('when GDPR applies', () => { it('should call endpoint with \'gdpr=1\'', () => { const completeCallback = () => {}; @@ -252,6 +282,73 @@ describe('33acrossIdSystem', () => { }); }); + context('when a 33Across ID is present in storage', () => { + context('and a first-party ID is also present in storage', () => { + it('should call endpoint with the first-party ID included', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }, + { }, + '33acrossIdValue'); + + sinon.stub(storage, 'getDataFromLocalStorage') + .withArgs('33acrossIdFp') + .returns('33acrossIdFpValue'); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('fp=33acrossIdFpValue'); + + storage.getDataFromLocalStorage.restore(); + }); + }); + context('and a first-party ID is not present in storage', () => { + it('should not call endpoint with the first-party ID included', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }, + { }, + '33acrossIdValue'); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).not.to.contain('fp='); + }); + }); + }); + context('when a 33Across ID is not present in storage', () => { + it('should not call endpoint with the first-party ID included', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); + + sinon.stub(storage, 'getDataFromLocalStorage') + .withArgs('33acrossIdFp') + .returns('33acrossIdFpValue'); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).not.to.contain('fp='); + + storage.getDataFromLocalStorage.restore(); + }); + }); + context('when the partner ID is not given', () => { it('should log an error', () => { const logErrorSpy = sinon.spy(utils, 'logError'); From 9ccfa5dcc86ab61abb6b786139015770d167fed0 Mon Sep 17 00:00:00 2001 From: Joshua Poritz Date: Thu, 9 Nov 2023 18:20:21 -0500 Subject: [PATCH 2/8] Ensure first-party ID is removed for local storage in situations like GPP conssent change --- modules/33acrossIdSystem.js | 10 +- test/spec/modules/33acrossIdSystem_spec.js | 138 +++++++++++---------- 2 files changed, 79 insertions(+), 69 deletions(-) diff --git a/modules/33acrossIdSystem.js b/modules/33acrossIdSystem.js index c9703682d7c..21425b6c3c3 100644 --- a/modules/33acrossIdSystem.js +++ b/modules/33acrossIdSystem.js @@ -44,7 +44,7 @@ function calculateResponseObj(response) { }; } -function calculateQueryStringParams(pid, gdprConsentData, storedId) { +function calculateQueryStringParams(pid, gdprConsentData) { const uspString = uspDataHandler.getConsentData(); const gdprApplies = Boolean(gdprConsentData?.gdprApplies); const coppaValue = coppaDataHandler.getCoppa(); @@ -74,7 +74,7 @@ function calculateQueryStringParams(pid, gdprConsentData, storedId) { } const fp = storage.getDataFromLocalStorage(STORAGE_FPID_KEY); - if (storedId && fp) { + if (fp) { params.fp = fp; } @@ -111,7 +111,7 @@ export const thirthyThreeAcrossIdSubmodule = { * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId({ params = { } }, gdprConsentData, storedId) { + getId({ params = { } }, gdprConsentData) { if (typeof params.pid !== 'string') { logError(`${MODULE_NAME}: Submodule requires a partner ID to be defined`); @@ -134,6 +134,8 @@ export const thirthyThreeAcrossIdSubmodule = { if (responseObj.fp) { storage.setDataInLocalStorage(STORAGE_FPID_KEY, responseObj.fp); + } else { + storage.removeDataFromLocalStorage(STORAGE_FPID_KEY); } cb(responseObj.envelope); @@ -143,7 +145,7 @@ export const thirthyThreeAcrossIdSubmodule = { cb(); } - }, calculateQueryStringParams(pid, gdprConsentData, storedId), { method: 'GET', withCredentials: true }); + }, calculateQueryStringParams(pid, gdprConsentData), { method: 'GET', withCredentials: true }); } }; }, diff --git a/test/spec/modules/33acrossIdSystem_spec.js b/test/spec/modules/33acrossIdSystem_spec.js index d0ad16c531c..a1718797209 100644 --- a/test/spec/modules/33acrossIdSystem_spec.js +++ b/test/spec/modules/33acrossIdSystem_spec.js @@ -50,34 +50,69 @@ describe('33acrossIdSystem', () => { expect(completeCallback.calledOnceWithExactly('foo')).to.be.true; }); - it('stores the first-party ID provided in the response', () => { - const completeCallback = () => {}; - const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + context('if the response includes a first-party ID', () => { + it('should store the first-party ID provided in the response', () => { + const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ - params: { - pid: '12345' - } + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setDataInLocalStorage.calledOnceWithExactly('33acrossIdFp', 'bar')).to.be.true; + + setDataInLocalStorage.restore(); }); + }); - callback(completeCallback); + context('if the response lacks a first-party ID', () => { + it('should wipe any existing first-party ID from storage', () => { + const completeCallback = () => {}; - const [request] = server.requests; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); - request.respond(200, { - 'Content-Type': 'application/json' - }, JSON.stringify({ - succeeded: true, - data: { - envelope: 'foo', - fp: 'bar' - }, - expires: 1645667805067 - })); + callback(completeCallback); + + const [request] = server.requests; - expect(setDataInLocalStorage.calledOnceWithExactly('33acrossIdFp', 'bar')).to.be.true; + const removeDataFromLocalStorage = sinon.stub(storage, 'removeDataFromLocalStorage'); - setDataInLocalStorage.restore(); + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo' + }, + expires: 1645667805067 + })); + + expect(removeDataFromLocalStorage.calledOnceWithExactly('33acrossIdFp')).to.be.true; + + removeDataFromLocalStorage.restore(); + }); }); context('when GDPR applies', () => { @@ -282,51 +317,30 @@ describe('33acrossIdSystem', () => { }); }); - context('when a 33Across ID is present in storage', () => { - context('and a first-party ID is also present in storage', () => { - it('should call endpoint with the first-party ID included', () => { - const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ - params: { - pid: '12345' - } - }, - { }, - '33acrossIdValue'); - - sinon.stub(storage, 'getDataFromLocalStorage') - .withArgs('33acrossIdFp') - .returns('33acrossIdFpValue'); - - callback(completeCallback); - - const [request] = server.requests; + context('when a first-party ID is present in storage', () => { + it('should call endpoint with the first-party ID included', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); - expect(request.url).to.contain('fp=33acrossIdFpValue'); + sinon.stub(storage, 'getDataFromLocalStorage') + .withArgs('33acrossIdFp') + .returns('33acrossIdFpValue'); - storage.getDataFromLocalStorage.restore(); - }); - }); - context('and a first-party ID is not present in storage', () => { - it('should not call endpoint with the first-party ID included', () => { - const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ - params: { - pid: '12345' - } - }, - { }, - '33acrossIdValue'); + callback(completeCallback); - callback(completeCallback); + const [request] = server.requests; - const [request] = server.requests; + expect(request.url).to.contain('fp=33acrossIdFpValue'); - expect(request.url).not.to.contain('fp='); - }); + storage.getDataFromLocalStorage.restore(); }); }); - context('when a 33Across ID is not present in storage', () => { + + context('when a first-party ID is not present in storage', () => { it('should not call endpoint with the first-party ID included', () => { const completeCallback = () => {}; const { callback } = thirthyThreeAcrossIdSubmodule.getId({ @@ -335,17 +349,11 @@ describe('33acrossIdSystem', () => { } }); - sinon.stub(storage, 'getDataFromLocalStorage') - .withArgs('33acrossIdFp') - .returns('33acrossIdFpValue'); - callback(completeCallback); const [request] = server.requests; expect(request.url).not.to.contain('fp='); - - storage.getDataFromLocalStorage.restore(); }); }); From c815cbd22f2980f92f31f0b341eec6ee72b21465 Mon Sep 17 00:00:00 2001 From: Carlos Felix Date: Thu, 16 Nov 2023 21:04:37 -0600 Subject: [PATCH 3/8] 33Across User ID sub-module: Add cookie storage support for first-party ID, --- modules/33acrossIdSystem.js | 43 ++++++-- test/spec/modules/33acrossIdSystem_spec.js | 120 +++++++++++++++++++-- 2 files changed, 146 insertions(+), 17 deletions(-) diff --git a/modules/33acrossIdSystem.js b/modules/33acrossIdSystem.js index 21425b6c3c3..46a95c8e413 100644 --- a/modules/33acrossIdSystem.js +++ b/modules/33acrossIdSystem.js @@ -9,7 +9,7 @@ import { logMessage, logError } from '../src/utils.js'; import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { uspDataHandler, coppaDataHandler, gppDataHandler } from '../src/adapterManager.js'; -import { getStorageManager } from '../src/storageManager.js'; +import { getStorageManager, STORAGE_TYPE_COOKIES, STORAGE_TYPE_LOCALSTORAGE } from '../src/storageManager.js'; import { MODULE_TYPE_UID } from '../src/activities/modules.js'; const MODULE_NAME = '33acrossId'; @@ -44,7 +44,7 @@ function calculateResponseObj(response) { }; } -function calculateQueryStringParams(pid, gdprConsentData) { +function calculateQueryStringParams(pid, gdprConsentData, storageConfig) { const uspString = uspDataHandler.getConsentData(); const gdprApplies = Boolean(gdprConsentData?.gdprApplies); const coppaValue = coppaDataHandler.getCoppa(); @@ -73,7 +73,7 @@ function calculateQueryStringParams(pid, gdprConsentData) { params.gdpr_consent = gdprConsentData.consentString; } - const fp = storage.getDataFromLocalStorage(STORAGE_FPID_KEY); + const fp = getStoredValue(STORAGE_FPID_KEY, storageConfig); if (fp) { params.fp = fp; } @@ -81,6 +81,33 @@ function calculateQueryStringParams(pid, gdprConsentData) { return params; } +function handleFpId(fpId, storageConfig = {}) { + if (!fpId) { + return storage.removeDataFromLocalStorage(STORAGE_FPID_KEY); + } + + storeValue(STORAGE_FPID_KEY, fpId, storageConfig); +} + +function storeValue(key, value, storageConfig = {}) { + if (storageConfig.type === STORAGE_TYPE_COOKIES && storage.cookiesAreEnabled()) { + const expirationInMs = 60 * 60 * 24 * 1000 * storageConfig.expires; + const expirationTime = new Date(Date.now() + expirationInMs); + + storage.setCookie(key, value, expirationTime.toUTCString(), 'Lax'); + } else if (storageConfig.type === STORAGE_TYPE_LOCALSTORAGE) { + storage.setDataInLocalStorage(key, value); + } +} + +function getStoredValue(key, storageConfig = {}) { + if (storageConfig.type === STORAGE_TYPE_COOKIES && storage.cookiesAreEnabled()) { + return storage.getCookie(key); + } else if (storageConfig.type === STORAGE_TYPE_LOCALSTORAGE) { + return storage.getDataFromLocalStorage(key); + } +} + /** @type {Submodule} */ export const thirthyThreeAcrossIdSubmodule = { /** @@ -111,7 +138,7 @@ export const thirthyThreeAcrossIdSubmodule = { * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId({ params = { } }, gdprConsentData) { + getId({ params = { }, storage }, gdprConsentData) { if (typeof params.pid !== 'string') { logError(`${MODULE_NAME}: Submodule requires a partner ID to be defined`); @@ -132,11 +159,7 @@ export const thirthyThreeAcrossIdSubmodule = { logError(`${MODULE_NAME}: ID reading error:`, err); } - if (responseObj.fp) { - storage.setDataInLocalStorage(STORAGE_FPID_KEY, responseObj.fp); - } else { - storage.removeDataFromLocalStorage(STORAGE_FPID_KEY); - } + handleFpId(responseObj.fp, storage); cb(responseObj.envelope); }, @@ -145,7 +168,7 @@ export const thirthyThreeAcrossIdSubmodule = { cb(); } - }, calculateQueryStringParams(pid, gdprConsentData), { method: 'GET', withCredentials: true }); + }, calculateQueryStringParams(pid, gdprConsentData, storage), { method: 'GET', withCredentials: true }); } }; }, diff --git a/test/spec/modules/33acrossIdSystem_spec.js b/test/spec/modules/33acrossIdSystem_spec.js index a1718797209..eedeef45a74 100644 --- a/test/spec/modules/33acrossIdSystem_spec.js +++ b/test/spec/modules/33acrossIdSystem_spec.js @@ -51,12 +51,90 @@ describe('33acrossIdSystem', () => { }); context('if the response includes a first-party ID', () => { - it('should store the first-party ID provided in the response', () => { + context('and the storage type is "cookie"', () => { + it('should store the provided first-party ID in a cookie', () => { + const completeCallback = () => {}; + + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + }, + storage: { + type: 'cookie', + expires: 90 + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setCookie = sinon.stub(storage, 'setCookie'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setCookie.calledOnceWithExactly('33acrossIdFp', 'bar', sinon.match.string, 'Lax')).to.be.true; + + setCookie.restore(); + }); + }); + + context('and the storage type is "html5"', () => { + it('should store the provided first-party ID in local storage', () => { + const completeCallback = () => {}; + + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + }, + storage: { + type: 'html5' + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setDataInLocalStorage.calledOnceWithExactly('33acrossIdFp', 'bar')).to.be.true; + + setDataInLocalStorage.restore(); + }); + }); + }); + + context('if the response doesn\'t include a first-party ID', () => { + it('should try to remove any first-party ID that could be stored', () => { const completeCallback = () => {}; const { callback } = thirthyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' + }, + storage: { + type: 'html5' } }); @@ -64,22 +142,21 @@ describe('33acrossIdSystem', () => { const [request] = server.requests; - const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + const removeDataFromLocalStorage = sinon.stub(storage, 'removeDataFromLocalStorage'); request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ succeeded: true, data: { - envelope: 'foo', - fp: 'bar' + envelope: 'foo' // no 'fp' field }, expires: 1645667805067 })); - expect(setDataInLocalStorage.calledOnceWithExactly('33acrossIdFp', 'bar')).to.be.true; + expect(removeDataFromLocalStorage.calledOnceWithExactly('33acrossIdFp')).to.be.true; - setDataInLocalStorage.restore(); + removeDataFromLocalStorage.restore(); }); }); @@ -317,12 +394,15 @@ describe('33acrossIdSystem', () => { }); }); - context('when a first-party ID is present in storage', () => { + context('when a first-party ID is present in local storage', () => { it('should call endpoint with the first-party ID included', () => { const completeCallback = () => {}; const { callback } = thirthyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' + }, + storage: { + type: 'html5' } }); @@ -340,6 +420,32 @@ describe('33acrossIdSystem', () => { }); }); + context('when a first-party ID is present in cookie storage', () => { + it('should call endpoint with the first-party ID included', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + }, + storage: { + type: 'cookie' + } + }); + + sinon.stub(storage, 'getCookie') + .withArgs('33acrossIdFp') + .returns('33acrossIdFpValue'); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('fp=33acrossIdFpValue'); + + storage.getCookie.restore(); + }); + }); + context('when a first-party ID is not present in storage', () => { it('should not call endpoint with the first-party ID included', () => { const completeCallback = () => {}; From f16a5fbb70d60ee49e2fe82881dacf74f06dd375 Mon Sep 17 00:00:00 2001 From: Carlos Felix Date: Fri, 17 Nov 2023 17:24:01 -0600 Subject: [PATCH 4/8] 33Across User ID sub-module: Also remove first-party ID from cookie storage --- modules/33acrossIdSystem.js | 16 ++++++++++++---- test/spec/modules/33acrossIdSystem_spec.js | 7 +++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/modules/33acrossIdSystem.js b/modules/33acrossIdSystem.js index 46a95c8e413..0a24ef54731 100644 --- a/modules/33acrossIdSystem.js +++ b/modules/33acrossIdSystem.js @@ -81,12 +81,14 @@ function calculateQueryStringParams(pid, gdprConsentData, storageConfig) { return params; } -function handleFpId(fpId, storageConfig = {}) { - if (!fpId) { - return storage.removeDataFromLocalStorage(STORAGE_FPID_KEY); +function deleteFromStorage(key) { + if (storage.cookiesAreEnabled()) { + const expiredDate = new Date(0).toUTCString(); + + storage.setCookie(key, '', expiredDate, 'Lax'); } - storeValue(STORAGE_FPID_KEY, fpId, storageConfig); + storage.removeDataFromLocalStorage(key); } function storeValue(key, value, storageConfig = {}) { @@ -108,6 +110,12 @@ function getStoredValue(key, storageConfig = {}) { } } +function handleFpId(fpId, storageConfig = {}) { + fpId + ? storeValue(STORAGE_FPID_KEY, fpId, storageConfig) + : deleteFromStorage(STORAGE_FPID_KEY); +} + /** @type {Submodule} */ export const thirthyThreeAcrossIdSubmodule = { /** diff --git a/test/spec/modules/33acrossIdSystem_spec.js b/test/spec/modules/33acrossIdSystem_spec.js index eedeef45a74..8f23d4614b5 100644 --- a/test/spec/modules/33acrossIdSystem_spec.js +++ b/test/spec/modules/33acrossIdSystem_spec.js @@ -70,6 +70,7 @@ describe('33acrossIdSystem', () => { const [request] = server.requests; const setCookie = sinon.stub(storage, 'setCookie'); + const cookiesAreEnabled = sinon.stub(storage, 'cookiesAreEnabled').returns(true); request.respond(200, { 'Content-Type': 'application/json' @@ -85,6 +86,7 @@ describe('33acrossIdSystem', () => { expect(setCookie.calledOnceWithExactly('33acrossIdFp', 'bar', sinon.match.string, 'Lax')).to.be.true; setCookie.restore(); + cookiesAreEnabled.restore(); }); }); @@ -143,6 +145,8 @@ describe('33acrossIdSystem', () => { const [request] = server.requests; const removeDataFromLocalStorage = sinon.stub(storage, 'removeDataFromLocalStorage'); + const setCookie = sinon.stub(storage, 'setCookie'); + const cookiesAreEnabled = sinon.stub(storage, 'cookiesAreEnabled').returns(true); request.respond(200, { 'Content-Type': 'application/json' @@ -155,8 +159,11 @@ describe('33acrossIdSystem', () => { })); expect(removeDataFromLocalStorage.calledOnceWithExactly('33acrossIdFp')).to.be.true; + expect(setCookie.calledOnceWithExactly('33acrossIdFp', '', sinon.match.string, 'Lax')).to.be.true; removeDataFromLocalStorage.restore(); + setCookie.restore(); + cookiesAreEnabled.restore(); }); }); From 6ffeb3d0b40003c19cd6682e163010f2ae6fa314 Mon Sep 17 00:00:00 2001 From: Carlos Felix Date: Fri, 15 Dec 2023 12:30:35 -0600 Subject: [PATCH 5/8] remove duplicated 33across ID test --- test/spec/modules/33acrossIdSystem_spec.js | 36 ++-------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/test/spec/modules/33acrossIdSystem_spec.js b/test/spec/modules/33acrossIdSystem_spec.js index 8f23d4614b5..f59a334b058 100644 --- a/test/spec/modules/33acrossIdSystem_spec.js +++ b/test/spec/modules/33acrossIdSystem_spec.js @@ -127,8 +127,8 @@ describe('33acrossIdSystem', () => { }); }); - context('if the response doesn\'t include a first-party ID', () => { - it('should try to remove any first-party ID that could be stored', () => { + context('if the response lacks a first-party ID', () => { + it('should wipe any existing first-party ID from storage', () => { const completeCallback = () => {}; const { callback } = thirthyThreeAcrossIdSubmodule.getId({ @@ -167,38 +167,6 @@ describe('33acrossIdSystem', () => { }); }); - context('if the response lacks a first-party ID', () => { - it('should wipe any existing first-party ID from storage', () => { - const completeCallback = () => {}; - - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ - params: { - pid: '12345' - } - }); - - callback(completeCallback); - - const [request] = server.requests; - - const removeDataFromLocalStorage = sinon.stub(storage, 'removeDataFromLocalStorage'); - - request.respond(200, { - 'Content-Type': 'application/json' - }, JSON.stringify({ - succeeded: true, - data: { - envelope: 'foo' - }, - expires: 1645667805067 - })); - - expect(removeDataFromLocalStorage.calledOnceWithExactly('33acrossIdFp')).to.be.true; - - removeDataFromLocalStorage.restore(); - }); - }); - context('when GDPR applies', () => { it('should call endpoint with \'gdpr=1\'', () => { const completeCallback = () => {}; From 54fd3a6e2bc7f48487095a246235cca02d923692 Mon Sep 17 00:00:00 2001 From: Carlos Felix Date: Fri, 15 Dec 2023 18:20:11 -0600 Subject: [PATCH 6/8] clear 33across ID from localstorage --- modules/33acrossIdSystem.js | 10 ++++-- test/spec/modules/33acrossIdSystem_spec.js | 40 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/modules/33acrossIdSystem.js b/modules/33acrossIdSystem.js index 0a24ef54731..5320f562ae4 100644 --- a/modules/33acrossIdSystem.js +++ b/modules/33acrossIdSystem.js @@ -146,7 +146,7 @@ export const thirthyThreeAcrossIdSubmodule = { * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId({ params = { }, storage }, gdprConsentData) { + getId({ params = { }, storage: storageConfig }, gdprConsentData) { if (typeof params.pid !== 'string') { logError(`${MODULE_NAME}: Submodule requires a partner ID to be defined`); @@ -167,7 +167,11 @@ export const thirthyThreeAcrossIdSubmodule = { logError(`${MODULE_NAME}: ID reading error:`, err); } - handleFpId(responseObj.fp, storage); + if (!responseObj.envelope) { + deleteFromStorage(MODULE_NAME); + } + + handleFpId(responseObj.fp, storageConfig); cb(responseObj.envelope); }, @@ -176,7 +180,7 @@ export const thirthyThreeAcrossIdSubmodule = { cb(); } - }, calculateQueryStringParams(pid, gdprConsentData, storage), { method: 'GET', withCredentials: true }); + }, calculateQueryStringParams(pid, gdprConsentData, storageConfig), { method: 'GET', withCredentials: true }); } }; }, diff --git a/test/spec/modules/33acrossIdSystem_spec.js b/test/spec/modules/33acrossIdSystem_spec.js index f59a334b058..0c91d54aba7 100644 --- a/test/spec/modules/33acrossIdSystem_spec.js +++ b/test/spec/modules/33acrossIdSystem_spec.js @@ -167,6 +167,46 @@ describe('33acrossIdSystem', () => { }); }); + context('if the response lacks the 33across "envelope" ID', () => { + it('should wipe any existing "envelope" ID from storage', () => { + const completeCallback = () => {}; + + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + }, + storage: { + type: 'html5' + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + const removeDataFromLocalStorage = sinon.stub(storage, 'removeDataFromLocalStorage'); + const setCookie = sinon.stub(storage, 'setCookie'); + const cookiesAreEnabled = sinon.stub(storage, 'cookiesAreEnabled').returns(true); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: '' // no 'envelope' field + }, + expires: 1645667805067 + })); + + expect(removeDataFromLocalStorage.calledWith('33acrossId')).to.be.true; + expect(setCookie.calledWith('33acrossId', '', sinon.match.string, 'Lax')).to.be.true; + + removeDataFromLocalStorage.restore(); + setCookie.restore(); + cookiesAreEnabled.restore(); + }); + }); + context('when GDPR applies', () => { it('should call endpoint with \'gdpr=1\'', () => { const completeCallback = () => {}; From 548a4808ac5781f736dacb361cf9bd77cc144a4b Mon Sep 17 00:00:00 2001 From: Carlos Felix Date: Tue, 30 Jan 2024 17:19:58 -0600 Subject: [PATCH 7/8] Add configuration flag for 1PID --- modules/33acrossIdSystem.js | 6 +- modules/33acrossIdSystem.md | 1 + test/spec/modules/33acrossIdSystem_spec.js | 176 +++++++++++++++------ 3 files changed, 134 insertions(+), 49 deletions(-) diff --git a/modules/33acrossIdSystem.js b/modules/33acrossIdSystem.js index 5320f562ae4..36007103121 100644 --- a/modules/33acrossIdSystem.js +++ b/modules/33acrossIdSystem.js @@ -153,7 +153,7 @@ export const thirthyThreeAcrossIdSubmodule = { return; } - const { pid, apiUrl = API_URL } = params; + const { pid, storeFpid, apiUrl = API_URL } = params; return { callback(cb) { @@ -171,7 +171,9 @@ export const thirthyThreeAcrossIdSubmodule = { deleteFromStorage(MODULE_NAME); } - handleFpId(responseObj.fp, storageConfig); + if (storeFpid) { + handleFpId(responseObj.fp, storageConfig); + } cb(responseObj.envelope); }, diff --git a/modules/33acrossIdSystem.md b/modules/33acrossIdSystem.md index 1e4af89344f..930d0c8c824 100644 --- a/modules/33acrossIdSystem.md +++ b/modules/33acrossIdSystem.md @@ -51,3 +51,4 @@ The following settings are available in the `params` property in `userSync.userI | Param name | Scope | Type | Description | Example | | --- | --- | --- | --- | --- | | pid | Required | String | Partner ID provided by 33Across | `"0010b00002GYU4eBAH"` | +| storeFpid | Optional | Boolean | Indicates whether a supplemental first-party ID may be stored to improve addressability | `false` (default) or `true` | diff --git a/test/spec/modules/33acrossIdSystem_spec.js b/test/spec/modules/33acrossIdSystem_spec.js index 0c91d54aba7..0c5ac80de99 100644 --- a/test/spec/modules/33acrossIdSystem_spec.js +++ b/test/spec/modules/33acrossIdSystem_spec.js @@ -50,18 +50,97 @@ describe('33acrossIdSystem', () => { expect(completeCallback.calledOnceWithExactly('foo')).to.be.true; }); - context('if the response includes a first-party ID', () => { - context('and the storage type is "cookie"', () => { - it('should store the provided first-party ID in a cookie', () => { + context('if the use of a first-party ID has been enabled', () => { + context('and the response includes a first-party ID', () => { + context('and the storage type is "cookie"', () => { + it('should store the provided first-party ID in a cookie', () => { + const completeCallback = () => {}; + + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345', + storeFpid: true + }, + storage: { + type: 'cookie', + expires: 90 + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setCookie = sinon.stub(storage, 'setCookie'); + const cookiesAreEnabled = sinon.stub(storage, 'cookiesAreEnabled').returns(true); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setCookie.calledOnceWithExactly('33acrossIdFp', 'bar', sinon.match.string, 'Lax')).to.be.true; + + setCookie.restore(); + cookiesAreEnabled.restore(); + }); + }); + + context('and the storage type is "html5"', () => { + it('should store the provided first-party ID in local storage', () => { + const completeCallback = () => {}; + + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345', + storeFpid: true + }, + storage: { + type: 'html5' + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setDataInLocalStorage.calledOnceWithExactly('33acrossIdFp', 'bar')).to.be.true; + + setDataInLocalStorage.restore(); + }); + }); + }); + + context('and the response lacks a first-party ID', () => { + it('should wipe any existing first-party ID from storage', () => { const completeCallback = () => {}; const { callback } = thirthyThreeAcrossIdSubmodule.getId({ params: { - pid: '12345' + pid: '12345', + storeFpid: true }, storage: { - type: 'cookie', - expires: 90 + type: 'html5' } }); @@ -69,6 +148,7 @@ describe('33acrossIdSystem', () => { const [request] = server.requests; + const removeDataFromLocalStorage = sinon.stub(storage, 'removeDataFromLocalStorage'); const setCookie = sinon.stub(storage, 'setCookie'); const cookiesAreEnabled = sinon.stub(storage, 'cookiesAreEnabled').returns(true); @@ -77,29 +157,34 @@ describe('33acrossIdSystem', () => { }, JSON.stringify({ succeeded: true, data: { - envelope: 'foo', - fp: 'bar' + envelope: 'foo' // no 'fp' field }, expires: 1645667805067 })); - expect(setCookie.calledOnceWithExactly('33acrossIdFp', 'bar', sinon.match.string, 'Lax')).to.be.true; + expect(removeDataFromLocalStorage.calledOnceWithExactly('33acrossIdFp')).to.be.true; + expect(setCookie.calledOnceWithExactly('33acrossIdFp', '', sinon.match.string, 'Lax')).to.be.true; + removeDataFromLocalStorage.restore(); setCookie.restore(); cookiesAreEnabled.restore(); }); }); + }); - context('and the storage type is "html5"', () => { - it('should store the provided first-party ID in local storage', () => { + context('if the use of a first-party ID has been disabled (default value)', () => { + context('and the response includes a first-party ID', () => { + it('should not store the provided first-party ID in a cookie', () => { const completeCallback = () => {}; const { callback } = thirthyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' + // no storeFpid param }, storage: { - type: 'html5' + type: 'cookie', + expires: 90 } }); @@ -107,7 +192,8 @@ describe('33acrossIdSystem', () => { const [request] = server.requests; - const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + const setCookie = sinon.stub(storage, 'setCookie'); + const cookiesAreEnabled = sinon.stub(storage, 'cookiesAreEnabled').returns(true); request.respond(200, { 'Content-Type': 'application/json' @@ -120,50 +206,46 @@ describe('33acrossIdSystem', () => { expires: 1645667805067 })); - expect(setDataInLocalStorage.calledOnceWithExactly('33acrossIdFp', 'bar')).to.be.true; + expect(setCookie.calledOnceWithExactly('33acrossIdFp', 'bar', sinon.match.string, 'Lax')).to.be.false; - setDataInLocalStorage.restore(); + setCookie.restore(); + cookiesAreEnabled.restore(); }); - }); - }); - context('if the response lacks a first-party ID', () => { - it('should wipe any existing first-party ID from storage', () => { - const completeCallback = () => {}; + it('should not store the provided first-party ID in local storage', () => { + const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ - params: { - pid: '12345' - }, - storage: { - type: 'html5' - } - }); + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + // no storeFpid param + }, + storage: { + type: 'html5' + } + }); - callback(completeCallback); + callback(completeCallback); - const [request] = server.requests; + const [request] = server.requests; - const removeDataFromLocalStorage = sinon.stub(storage, 'removeDataFromLocalStorage'); - const setCookie = sinon.stub(storage, 'setCookie'); - const cookiesAreEnabled = sinon.stub(storage, 'cookiesAreEnabled').returns(true); + const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); - request.respond(200, { - 'Content-Type': 'application/json' - }, JSON.stringify({ - succeeded: true, - data: { - envelope: 'foo' // no 'fp' field - }, - expires: 1645667805067 - })); + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); - expect(removeDataFromLocalStorage.calledOnceWithExactly('33acrossIdFp')).to.be.true; - expect(setCookie.calledOnceWithExactly('33acrossIdFp', '', sinon.match.string, 'Lax')).to.be.true; + expect(setDataInLocalStorage.calledOnceWithExactly('33acrossIdFp', 'bar')).to.be.false; - removeDataFromLocalStorage.restore(); - setCookie.restore(); - cookiesAreEnabled.restore(); + setDataInLocalStorage.restore(); + }); }); }); From dce6c5a3ae7a6954b0935f1d67ea1e5813447541 Mon Sep 17 00:00:00 2001 From: Carlos Felix Date: Mon, 5 Feb 2024 12:40:57 -0600 Subject: [PATCH 8/8] Suppress 33across ID requests where GDPR applies --- modules/33acrossIdSystem.js | 11 ++++-- test/spec/modules/33acrossIdSystem_spec.js | 43 +++++++++++----------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/modules/33acrossIdSystem.js b/modules/33acrossIdSystem.js index 36007103121..d9816254d75 100644 --- a/modules/33acrossIdSystem.js +++ b/modules/33acrossIdSystem.js @@ -5,7 +5,7 @@ * @requires module:modules/userId */ -import { logMessage, logError } from '../src/utils.js'; +import { logMessage, logError, logWarn } from '../src/utils.js'; import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { uspDataHandler, coppaDataHandler, gppDataHandler } from '../src/adapterManager.js'; @@ -46,13 +46,12 @@ function calculateResponseObj(response) { function calculateQueryStringParams(pid, gdprConsentData, storageConfig) { const uspString = uspDataHandler.getConsentData(); - const gdprApplies = Boolean(gdprConsentData?.gdprApplies); const coppaValue = coppaDataHandler.getCoppa(); const gppConsent = gppDataHandler.getConsentData(); const params = { pid, - gdpr: Number(gdprApplies), + gdpr: 0, src: CALLER_NAME, ver: '$prebid.version$', coppa: Number(coppaValue) @@ -153,6 +152,12 @@ export const thirthyThreeAcrossIdSubmodule = { return; } + if (gdprConsentData?.gdprApplies === true) { + logWarn(`${MODULE_NAME}: Submodule cannot be used where GDPR applies`); + + return; + } + const { pid, storeFpid, apiUrl = API_URL } = params; return { diff --git a/test/spec/modules/33acrossIdSystem_spec.js b/test/spec/modules/33acrossIdSystem_spec.js index 0c5ac80de99..a54c4590f3f 100644 --- a/test/spec/modules/33acrossIdSystem_spec.js +++ b/test/spec/modules/33acrossIdSystem_spec.js @@ -290,9 +290,10 @@ describe('33acrossIdSystem', () => { }); context('when GDPR applies', () => { - it('should call endpoint with \'gdpr=1\'', () => { - const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + it('should log a warning and don\'t expect a call to the endpoint', () => { + const logWarnSpy = sinon.spy(utils, 'logWarn'); + + const result = thirthyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -300,11 +301,10 @@ describe('33acrossIdSystem', () => { gdprApplies: true }); - callback(completeCallback); + expect(logWarnSpy.calledOnceWithExactly('33acrossId: Submodule cannot be used where GDPR applies')).to.be.true; + expect(result).to.be.undefined; - const [request] = server.requests; - - expect(request.url).to.contain('gdpr=1'); + logWarnSpy.restore(); }); }); @@ -325,24 +325,25 @@ describe('33acrossIdSystem', () => { expect(request.url).to.contain('gdpr=0'); }); - }); - context('when the GDPR consent string is given', () => { - it('should call endpoint with the GDPR consent string', () => { - const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ - params: { - pid: '12345' - } - }, { - consentString: 'foo' - }); + context('but the GDPR consent string is given', () => { + it('should call endpoint with the GDPR consent string', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }, { + gdprApplies: false, + consentString: 'foo' + }); - callback(completeCallback); + callback(completeCallback); - const [request] = server.requests; + const [request] = server.requests; - expect(request.url).to.contain('gdpr_consent=foo'); + expect(request.url).to.contain('gdpr_consent=foo'); + }); }); });