From 52c7514fa888673a0a80ea1153603bb1911df3ba Mon Sep 17 00:00:00 2001 From: Alex S <49695018+alexs-mparticle@users.noreply.github.com> Date: Tue, 23 Apr 2024 08:37:09 -0400 Subject: [PATCH 1/8] refactor: Migrate Consent State Persistence Methods (#867) --- src/identity.js | 7 +- src/mp-instance.js | 1 + src/persistence.interfaces.ts | 26 +- src/store.ts | 207 ++++++++++------ test/src/tests-store.ts | 446 +++++++++++++++++++++++++++++++++- 5 files changed, 598 insertions(+), 89 deletions(-) diff --git a/src/identity.js b/src/identity.js index 0cb77801a..1e90297dc 100644 --- a/src/identity.js +++ b/src/identity.js @@ -1233,7 +1233,7 @@ export default function Identity(mpInstance) { * @return a ConsentState object */ getConsentState: function() { - return mpInstance._Persistence.getConsentState(mpid); + return mpInstance._Store.getConsentState(mpid); }, /** * Sets the Consent State stored locally for this user. @@ -1241,10 +1241,7 @@ export default function Identity(mpInstance) { * @param {Object} consent state */ setConsentState: function(state) { - mpInstance._Persistence.saveUserConsentStateToCookies( - mpid, - state - ); + mpInstance._Store.setConsentState(mpid, state); mpInstance._Forwarders.initForwarders( this.getUserIdentities().userIdentities, mpInstance._APIClient.prepareForwardingStats diff --git a/src/mp-instance.js b/src/mp-instance.js index 787e5d4fd..f9da0c3c3 100644 --- a/src/mp-instance.js +++ b/src/mp-instance.js @@ -1310,6 +1310,7 @@ function completeSDKInitialization(apiKey, config, mpInstance) { // Load any settings/identities/attributes from cookie or localStorage mpInstance._Persistence.initializeStorage(); + mpInstance._Store.syncPersistenceData(); // Set up user identitiy variables for later use const currentUser = mpInstance.Identity.getCurrentUser(); diff --git a/src/persistence.interfaces.ts b/src/persistence.interfaces.ts index 332292050..0018f7ea5 100644 --- a/src/persistence.interfaces.ts +++ b/src/persistence.interfaces.ts @@ -14,8 +14,8 @@ import { SessionAttributes, } from './store'; import { Dictionary } from './utils'; +import { IMinifiedConsentJSONObject } from './consent'; -export type CookieSyncDate = Dictionary; export type UploadsTable = Dictionary; export interface iForwardingStatsBatches { uploadsTable: UploadsTable; @@ -48,7 +48,7 @@ export interface IPersistenceMinified extends Dictionary { // Persistence Minified can also store optional dictionaries with // an idex of MPID - // [mpid: MPID]: Dictionary; + // [mpid: MPID]: Dictionary; // For Example: // { @@ -70,14 +70,34 @@ export interface IPersistenceMinified extends Dictionary { // }, // l: false, // MPID1: { - // csd: [], + // csd: { + // [moduleid]: 1234567890, + // }, // ui: { // customerid: '12346', // }, + // ua: { + // age '42', + // }, // }, // }; } +export type CookieSyncDate = Dictionary; + +export interface IUserPersistenceMinified extends Dictionary { + csd: CookieSyncDate; // Cookie Sync Dates // list of timestamps for last cookie sync + con: IMinifiedConsentJSONObject; // Consent State + ui: UserIdentities; // User Identities + ua: UserAttributes; // User Attributes + + // https://go.mparticle.com/work/SQDSDKS-6048 + cp: Product[]; // Cart Products + + fst: number; // First Seen Time + lst: number; // Last Seen Time +} + export interface IPersistence { useLocalStorage(): boolean; initializeStorage(): void; diff --git a/src/store.ts b/src/store.ts index 42b3478f3..c59df5f2b 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,11 +1,11 @@ -import { Batch } from '@mparticle/event-models'; -import { Context } from '@mparticle/event-models'; +import { Batch, Context } from '@mparticle/event-models'; import { DataPlanConfig, MPID, IdentifyRequest, IdentityCallback, SDKEventCustomFlags, + ConsentState, } from '@mparticle/web-sdk'; import { IKitConfigs } from './configAPIClient'; import Constants from './constants'; @@ -30,9 +30,12 @@ import { parseNumber, returnConvertedBoolean, } from './utils'; -import { SDKConsentState } from './consent'; +import { IMinifiedConsentJSONObject, SDKConsentState } from './consent'; import { Kit, MPForwarder } from './forwarders.interfaces'; -import { IPersistenceMinified } from './persistence.interfaces'; +import { + IGlobalStoreV2MinifiedKeys, + IPersistenceMinified, +} from './persistence.interfaces'; // This represents the runtime configuration of the SDK AFTER // initialization has been complete and all settings and @@ -179,6 +182,12 @@ export interface IStore { persistenceData?: IPersistenceMinified; + getConsentState?(mpid: MPID): ConsentState | null; + setConsentState?(mpid: MPID, consentState: ConsentState): void; + + _getFromPersistence?(mpid: MPID, key: string): T; + _setPersistence?(mpid: MPID, key: string, value: T): void; + getDeviceId?(): string; setDeviceId?(deviceId: string): void; getFirstSeenTime?(mpid: MPID): number; @@ -190,6 +199,7 @@ export interface IStore { hasInvalidIdentifyRequest?: () => boolean; nullifySession?: () => void; processConfig(config: SDKInitConfig): void; + syncPersistenceData?: () => void; } // TODO: Merge this with SDKStoreApi in sdkRuntimeModels @@ -252,24 +262,8 @@ export default function Store( // Placeholder for in-memory persistence model persistenceData: { - cu: null, - gs: { - sid: null, - ie: null, - sa: null, - ss: null, - dt: null, - av: null, - cgid: null, - das: null, - ia: null, - c: null, - csm: null, - les: null, - ssd: null, - }, - l: null, - }, + gs: {} as IGlobalStoreV2MinifiedKeys, + } as IPersistenceMinified, }; for (var key in defaultStore) { @@ -478,6 +472,53 @@ export default function Store( } } + this._getFromPersistence = (mpid: MPID, key: string): T | null => { + if (!mpid) { + return null; + } + + this.syncPersistenceData(); + + if ( + this.persistenceData && + this.persistenceData[mpid] && + this.persistenceData[mpid][key] + ) { + return this.persistenceData[mpid][key] as T; + } else { + return null; + } + }; + + this._setPersistence = (mpid: MPID, key: string, value: T): void => { + if (!mpid) { + return; + } + + this.syncPersistenceData(); + + if (this.persistenceData) { + if (this.persistenceData[mpid]) { + this.persistenceData[mpid][key] = value; + } else { + this.persistenceData[mpid] = { + [key]: value, + }; + } + + // Clear out persistence attributes that are empty + // so that we don't upload empty or undefined values + if ( + isObject(this.persistenceData[mpid][key]) && + isEmpty(this.persistenceData[mpid][key]) + ) { + delete this.persistenceData[mpid][key]; + } + + mpInstance._Persistence.savePersistence(this.persistenceData); + } + }; + this.hasInvalidIdentifyRequest = (): boolean => { const { identifyRequest } = this.SDKConfig; return ( @@ -486,47 +527,52 @@ export default function Store( isEmpty(identifyRequest.userIdentities)) || !identifyRequest ); - }; + } - this.getDeviceId = () => this.deviceId; - this.setDeviceId = (deviceId: string) => { - this.deviceId = deviceId; - this.persistenceData.gs.das = deviceId; - mpInstance._Persistence.update(); - }; + - this.addMpidToSessionHistory = (mpid: MPID, previousMPID?: MPID): void => { - const indexOfMPID = this.currentSessionMPIDs.indexOf(mpid); + this.getConsentState = (mpid: MPID): ConsentState => { + const { + fromMinifiedJsonObject, + } = mpInstance._Consent.ConsentSerialization; + + const serializedConsentState = this._getFromPersistence< + IMinifiedConsentJSONObject + >(mpid, 'con'); - if (mpid && previousMPID !== mpid && indexOfMPID < 0) { - this.currentSessionMPIDs.push(mpid); - return; + if (!isEmpty(serializedConsentState)) { + return fromMinifiedJsonObject(serializedConsentState); } - if (indexOfMPID >= 0) { - this.currentSessionMPIDs = moveElementToEnd( - this.currentSessionMPIDs, - indexOfMPID - ); - } + return null; }; - this.getFirstSeenTime = (mpid: MPID) => { - if (!mpid) { - return null; + this.setConsentState = (mpid: MPID, consentState: ConsentState) => { + const { + toMinifiedJsonObject, + } = mpInstance._Consent.ConsentSerialization; + + // If ConsentState is null, we assume the intent is to clear out the consent state + if (consentState || consentState === null) { + this._setPersistence( + mpid, + 'con', + toMinifiedJsonObject(consentState) + ); } + }; - if ( - this.persistenceData && - this.persistenceData[mpid] && - this.persistenceData[mpid].fst - ) { - return this.persistenceData[mpid].fst; - } else { - return null; - } + this.getDeviceId = () => this.deviceId; + this.setDeviceId = (deviceId: string) => { + this.deviceId = deviceId; + this.persistenceData.gs.das = deviceId; + mpInstance._Persistence.update(); }; + + this.getFirstSeenTime = (mpid: MPID) => + this._getFromPersistence(mpid, 'fst'); + this.setFirstSeenTime = (mpid: MPID, _time?: number) => { if (!mpid) { return; @@ -534,35 +580,20 @@ export default function Store( const time = _time || new Date().getTime(); - if (this.persistenceData) { - if (!this.persistenceData[mpid]) { - this.persistenceData[mpid] = {}; - } - if (!this.persistenceData[mpid].fst) { - this.persistenceData[mpid].fst = time; - mpInstance._Persistence.savePersistence(this.persistenceData); - } - } + this._setPersistence(mpid, 'fst', time); }; - this.getLastSeenTime = (mpid: MPID) => { + this.getLastSeenTime = (mpid: MPID): number => { if (!mpid) { return null; } - // // https://go.mparticle.com/work/SQDSDKS-6315 + // https://go.mparticle.com/work/SQDSDKS-6315 const currentUser = mpInstance.Identity.getCurrentUser(); if (mpid === currentUser?.getMPID()) { // if the mpid is the current user, its last seen time is the current time return new Date().getTime(); - } else if ( - this.persistenceData && - this.persistenceData[mpid] && - this.persistenceData[mpid].lst - ) { - return this.persistenceData[mpid].lst; - } else { - return null; } + return this._getFromPersistence(mpid, 'lst'); }; this.setLastSeenTime = (mpid: MPID, _time?: number) => { @@ -572,14 +603,32 @@ export default function Store( const time = _time || new Date().getTime(); - if (this.persistenceData) { - if (!this.persistenceData[mpid]) { - this.persistenceData[mpid] = {}; - } - if (!this.persistenceData[mpid].lst) { - this.persistenceData[mpid].lst = time; - mpInstance._Persistence.savePersistence(this.persistenceData); - } + this._setPersistence(mpid, 'lst', time); + }; + + this.syncPersistenceData = () => { + const persistenceData = mpInstance._Persistence.getPersistence(); + + this.persistenceData = mpInstance._Helpers.extend( + {}, + this.persistenceData, + persistenceData, + ); + }; + + this.addMpidToSessionHistory = (mpid: MPID, previousMPID?: MPID): void => { + const indexOfMPID = this.currentSessionMPIDs.indexOf(mpid); + + if (mpid && previousMPID !== mpid && indexOfMPID < 0) { + this.currentSessionMPIDs.push(mpid); + return; + } + + if (indexOfMPID >= 0) { + this.currentSessionMPIDs = moveElementToEnd( + this.currentSessionMPIDs, + indexOfMPID + ); } }; diff --git a/test/src/tests-store.ts b/test/src/tests-store.ts index 726579db2..dc17291fc 100644 --- a/test/src/tests-store.ts +++ b/test/src/tests-store.ts @@ -17,6 +17,7 @@ import Utils from './config/utils'; import { Dictionary } from '../../src/utils'; import Constants from '../../src/constants'; import { IGlobalStoreV2MinifiedKeys } from '../../src/persistence.interfaces'; +import { IMinifiedConsentJSONObject } from '../../src/consent'; const MockSideloadedKit = Utils.MockSideloadedKit; describe('Store', () => { @@ -31,6 +32,77 @@ describe('Store', () => { flags: {}, } as SDKInitConfig; + type ConsentStateSample = Dictionary; + + const sampleConsentState: ConsentStateSample = { + con: { + gdpr: { + analytics: { + c: true, + d: 'foo gdpr document', + h: 'foo gdpr hardware id', + l: 'foo gdpr location', + ts: 10, + }, + }, + ccpa: { + data_sale_opt_out: { + c: false, + d: 'foo ccpa document', + h: 'foo ccpa hardware id', + l: 'foo ccpa location', + ts: 42, + }, + }, + }, + }; + + const sampleConsentStateFromStore: ConsentStateSample = { + con: { + gdpr: { + analytics: { + c: false, + d: 'foo gdpr document from store', + h: 'foo gdpr hardware id from store', + l: 'foo gdpr location from store', + ts: 101, + }, + }, + ccpa: { + data_sale_opt_out: { + c: true, + d: 'foo ccpa document from store', + h: 'foo ccpa hardware id from store', + l: 'foo ccpa location from store', + ts: 24, + }, + }, + }, + }; + + const sampleConsentStateFromPersistence: ConsentStateSample = { + con: { + gdpr: { + analytics: { + c: false, + d: 'foo gdpr document from persistence', + h: 'foo gdpr hardware id from persistence', + l: 'foo gdpr location from persistence', + ts: 101, + }, + }, + ccpa: { + data_sale_opt_out: { + c: true, + d: 'foo ccpa document from persistence', + h: 'foo ccpa hardware id from persistence', + l: 'foo ccpa location from persistence', + ts: 24, + }, + }, + }, + }; + beforeEach(function() { sandbox = sinon.createSandbox(); clock = sinon.useFakeTimers(now.getTime()); @@ -133,7 +205,7 @@ describe('Store', () => { .undefined; expect( - store.SDKConfig.flags?.eventBatchingIntervalMillis, + store.SDKConfig.flags.eventBatchingIntervalMillis, 'flags.eventBatchingIntervalMillis' ).to.eq(0); expect(store.SDKConfig.forceHttps, 'forceHttps').to.eq(true); @@ -307,6 +379,319 @@ describe('Store', () => { }); }); + describe('#getConsentState', () => { + it('should return a consent state object from the store', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.persistenceData[testMPID] = sampleConsentState; + + expect(store.getConsentState(testMPID)).to.be.ok; + expect(store.getConsentState(testMPID)).to.haveOwnProperty( + 'getGDPRConsentState' + ); + expect(store.getConsentState(testMPID)).to.haveOwnProperty( + 'getCCPAConsentState' + ); + + expect( + store.getConsentState(testMPID).getGDPRConsentState() + ).to.deep.equal({ + analytics: { + Consented: true, + ConsentDocument: 'foo gdpr document', + HardwareId: 'foo gdpr hardware id', + Location: 'foo gdpr location', + Timestamp: 10, + }, + }); + + expect( + store.getConsentState(testMPID).getCCPAConsentState() + ).to.deep.equal({ + Consented: false, + ConsentDocument: 'foo ccpa document', + HardwareId: 'foo ccpa hardware id', + Location: 'foo ccpa location', + Timestamp: 42, + }); + }); + + it('should return null if no consent state is found', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + expect(store.getConsentState(testMPID)).to.deep.equal(null); + }); + + it('should return in-memory consent state if persistence is empty', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.persistenceData[testMPID] = sampleConsentStateFromStore; + + localStorage.setItem(workspaceCookieName, ''); + + expect(store.getConsentState(testMPID)).to.be.ok; + + expect(store.getConsentState(testMPID)).to.haveOwnProperty( + 'getGDPRConsentState' + ); + expect( + store.getConsentState(testMPID).getGDPRConsentState() + ).to.deep.equal({ + analytics: { + Consented: false, + ConsentDocument: 'foo gdpr document from store', + HardwareId: 'foo gdpr hardware id from store', + Location: 'foo gdpr location from store', + Timestamp: 101, + }, + }); + + expect(store.getConsentState(testMPID)).to.haveOwnProperty( + 'getCCPAConsentState' + ); + + expect( + store.getConsentState(testMPID).getCCPAConsentState() + ).to.deep.equal({ + Consented: true, + ConsentDocument: 'foo ccpa document from store', + HardwareId: 'foo ccpa hardware id from store', + Location: 'foo ccpa location from store', + Timestamp: 24, + }); + }); + }); + + describe('#setConsentState', () => { + it('should set consent state as a minified object in the store', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + const consentState = window.mParticle.Consent.createConsentState(); + + const gdprConsent = window.mParticle + .getInstance() + .Consent.createGDPRConsent( + true, + 10, + 'foo gdpr document', + 'foo gdpr location', + 'foo gdpr hardware id' + ); + + const ccpaConsent = window.mParticle + .getInstance() + .Consent.createCCPAConsent( + false, + 42, + 'foo ccpa document', + 'foo ccpa location', + 'foo ccpa hardware id' + ); + + const expectedConsentState = sampleConsentState.con; + + consentState.addGDPRConsentState('analytics', gdprConsent); + consentState.setCCPAConsentState(ccpaConsent); + + store.setConsentState(testMPID, consentState); + + expect(store.persistenceData[testMPID].con).to.be.ok; + + expect(store.persistenceData[testMPID].con.gdpr).to.be.ok; + expect(store.persistenceData[testMPID].con.gdpr).to.deep.equal( + expectedConsentState.gdpr + ); + + expect(store.persistenceData[testMPID].con.ccpa).to.be.ok; + expect(store.persistenceData[testMPID].con.ccpa).to.deep.equal( + expectedConsentState.ccpa + ); + }); + + it('should set consent state as a minified object in persistence', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + const consentState = window.mParticle.Consent.createConsentState(); + + const gdprConsent = window.mParticle + .getInstance() + .Consent.createGDPRConsent( + true, + 10, + 'foo gdpr document', + 'foo gdpr location', + 'foo gdpr hardware id' + ); + + const ccpaConsent = window.mParticle + .getInstance() + .Consent.createCCPAConsent( + false, + 42, + 'foo ccpa document', + 'foo ccpa location', + 'foo ccpa hardware id' + ); + + const expectedConsentState = sampleConsentState.con; + + consentState.addGDPRConsentState('analytics', gdprConsent); + consentState.setCCPAConsentState(ccpaConsent); + + store.setConsentState(testMPID, consentState); + + const fromPersistence = window.mParticle + .getInstance() + ._Persistence.getPersistence(); + + expect(fromPersistence[testMPID]).to.be.ok; + expect(fromPersistence[testMPID].con).to.be.ok; + + expect(fromPersistence[testMPID].con.gdpr).to.be.ok; + expect(fromPersistence[testMPID].con.gdpr).to.deep.equal( + expectedConsentState.gdpr + ); + + expect(fromPersistence[testMPID].con.ccpa).to.be.ok; + expect(fromPersistence[testMPID].con.ccpa).to.deep.equal( + expectedConsentState.ccpa + ); + }); + + it('should override persistence with store values', () => { + const consentState = window.mParticle.Consent.createConsentState(); + + const analyticsGdprConsent = window.mParticle + .getInstance() + .Consent.createGDPRConsent( + false, + 101, + 'analytics gdpr document from store', + 'analytics gdpr location from store', + 'analytics gdpr hardware id from store' + ); + + const marketingGdprConsent = window.mParticle + .getInstance() + .Consent.createGDPRConsent( + true, + 202, + 'marketing gdpr document from store', + 'marketing gdpr location from store', + 'marketing gdpr hardware id from store' + ); + + const ccpaConsent = window.mParticle + .getInstance() + .Consent.createCCPAConsent( + true, + 24, + 'foo ccpa document from store', + 'foo ccpa location from store', + 'foo ccpa hardware id from store' + ); + + const expectedConsentState = sampleConsentStateFromStore.con; + + consentState.addGDPRConsentState('analytics', analyticsGdprConsent); + consentState.setCCPAConsentState(ccpaConsent); + + const persistenceValue = JSON.stringify({ + testMPID: { + con: sampleConsentStateFromPersistence.con, + }, + }); + + localStorage.setItem(workspaceCookieName, persistenceValue); + + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.setConsentState(testMPID, consentState); + + const fromPersistence = window.mParticle + .getInstance() + ._Persistence.getPersistence(); + + expect(fromPersistence[testMPID]).to.be.ok; + expect(fromPersistence[testMPID].con).to.be.ok; + expect(fromPersistence[testMPID].con.gdpr).to.be.ok; + expect(fromPersistence[testMPID].con.ccpa).to.be.ok; + + expect(fromPersistence[testMPID].con.gdpr).to.deep.equal({ + analytics: { + c: false, + ts: 101, + d: 'analytics gdpr document from store', + h: 'analytics gdpr hardware id from store', + l: 'analytics gdpr location from store', + }, + }); + + expect(fromPersistence[testMPID].con.ccpa).to.deep.equal( + expectedConsentState.ccpa + ); + + consentState.addGDPRConsentState('marketing', marketingGdprConsent); + store.setConsentState(testMPID, consentState); + + const retrieveFromPersistence = window.mParticle + .getInstance() + ._Persistence.getPersistence(); + + expect(retrieveFromPersistence[testMPID].con.gdpr).to.deep.equal({ + analytics: { + c: false, + ts: 101, + d: 'analytics gdpr document from store', + h: 'analytics gdpr hardware id from store', + l: 'analytics gdpr location from store', + }, + marketing: { + c: true, + ts: 202, + d: 'marketing gdpr document from store', + h: 'marketing gdpr hardware id from store', + l: 'marketing gdpr location from store', + }, + }); + }); + + it('should not set consent state if consent state is null', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + const expectedConsentState = sampleConsentState; + + store.persistenceData[testMPID] = expectedConsentState; + + store.setConsentState(testMPID, null); + + expect(store.persistenceData[testMPID]).to.deep.equal( + expectedConsentState + ); + }); + }); + describe('#getDeviceId', () => { it('should return the deviceId from the store', () => { const store: IStore = new Store( @@ -358,7 +743,6 @@ describe('Store', () => { expect(store.getFirstSeenTime(testMPID)).to.equal(12345); }); - it('should return null if mpid is null', () => { const store: IStore = new Store( sampleConfig, @@ -788,6 +1172,64 @@ describe('Store', () => { }); }); + describe('#syncPersistenceData', () => { + it('should sync store with persistence values', () => { + const persistenceValue = JSON.stringify({ + testMPID: { + lst: 12345, + fst: 54321, + con: sampleConsentStateFromPersistence.con, + }, + }); + + localStorage.setItem(workspaceCookieName, persistenceValue); + + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.syncPersistenceData(); + + expect(store.persistenceData[testMPID].lst).to.equal(12345); + expect(store.persistenceData[testMPID].fst).to.equal(54321); + expect(store.persistenceData[testMPID].con).to.deep.equal( + sampleConsentStateFromPersistence.con + ); + }); + + it('should override store with persistence data', () => { + const persistenceValue = JSON.stringify({ + testMPID: { + lst: 12345, + fst: 54321, + con: sampleConsentStateFromPersistence.con, + }, + }); + + localStorage.setItem(workspaceCookieName, persistenceValue); + + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.persistenceData[testMPID] = { + lst: 99999, + fst: 88888, + con: sampleConsentStateFromStore.con, + }; + + store.syncPersistenceData(); + + expect(store.persistenceData[testMPID].lst).to.equal(12345); + expect(store.persistenceData[testMPID].fst).to.equal(54321); + expect(store.persistenceData[testMPID].con).to.deep.equal( + sampleConsentStateFromPersistence.con + ); + }); + }); + describe('#processFlags', () => { it('should return an empty object if no featureFlags are passed', () => { const flags = processFlags({} as SDKInitConfig); From 5756c9f3deb5698ccbf949a745571cc212ab9839 Mon Sep 17 00:00:00 2001 From: Alex S <49695018+alexs-mparticle@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:08:45 -0400 Subject: [PATCH 2/8] refactor: Migrate User Identities Persistence Methods (#869) --- src/filteredMparticleUser.js | 2 +- src/identity.js | 11 ++-- src/persistence.interfaces.ts | 5 -- src/persistence.js | 25 --------- src/store.ts | 16 ++++-- test/src/tests-store.ts | 97 +++++++++++++++++++++++++++++++++-- 6 files changed, 111 insertions(+), 45 deletions(-) diff --git a/src/filteredMparticleUser.js b/src/filteredMparticleUser.js index ee5e200b1..e5f181357 100644 --- a/src/filteredMparticleUser.js +++ b/src/filteredMparticleUser.js @@ -10,7 +10,7 @@ export default function filteredMparticleUser( return { getUserIdentities: function() { var currentUserIdentities = {}; - var identities = mpInstance._Persistence.getUserIdentities(mpid); + var identities = mpInstance._Store.getUserIdentities(mpid); for (var identityType in identities) { if (identities.hasOwnProperty(identityType)) { diff --git a/src/identity.js b/src/identity.js index 1e90297dc..4546e74a2 100644 --- a/src/identity.js +++ b/src/identity.js @@ -806,11 +806,8 @@ export default function Identity(mpInstance) { * @return {Object} an object with userIdentities as its key */ getUserIdentities: function() { - var currentUserIdentities = {}; - - var identities = mpInstance._Persistence.getUserIdentities( - mpid - ); + const currentUserIdentities = {}; + const identities = mpInstance._Store.getUserIdentities(mpid); for (var identityType in identities) { if (identities.hasOwnProperty(identityType)) { @@ -1546,7 +1543,7 @@ export default function Identity(mpInstance) { identityApiData.userIdentities ); - mpInstance._Persistence.saveUserIdentitiesToPersistence( + mpInstance._Store.setUserIdentities( previousMPID, newIdentitiesByType ); @@ -1593,7 +1590,7 @@ export default function Identity(mpInstance) { } // https://go.mparticle.com/work/SQDSDKS-6041 - mpInstance._Persistence.saveUserIdentitiesToPersistence( + mpInstance._Store.setUserIdentities( identityApiResult.mpid, newIdentitiesByType ); diff --git a/src/persistence.interfaces.ts b/src/persistence.interfaces.ts index 0018f7ea5..5616f0dee 100644 --- a/src/persistence.interfaces.ts +++ b/src/persistence.interfaces.ts @@ -123,14 +123,9 @@ export interface IPersistence { decodePersistence(persistenceString: string): string; getCookieDomain(): string; getDomain(doc: string, locationHostname: string): string; - getUserIdentities(mpid: MPID): UserIdentities; getAllUserAttributes(mpid: MPID): AllUserAttributes; getCartProducts(mpid: MPID): Product[]; setCartProducts(allProducts: Product[]): void; - saveUserIdentitiesToPersistence( - mpid: MPID, - userIdentities: UserIdentities - ): void; saveUserAttributesToPersistence( mpid: MPID, userAttributes: UserAttributes diff --git a/src/persistence.js b/src/persistence.js index 256a604ff..462642878 100644 --- a/src/persistence.js +++ b/src/persistence.js @@ -882,16 +882,6 @@ export default function _Persistence(mpInstance) { return ''; }; - this.getUserIdentities = function(mpid) { - var persistence = self.getPersistence(); - - if (persistence && persistence[mpid] && persistence[mpid].ui) { - return persistence[mpid].ui; - } else { - return {}; - } - }; - this.getAllUserAttributes = function(mpid) { var persistence = self.getPersistence(); @@ -937,21 +927,6 @@ export default function _Persistence(mpInstance) { ); } }; - this.saveUserIdentitiesToPersistence = function(mpid, userIdentities) { - if (userIdentities) { - var persistence = self.getPersistence(); - if (persistence) { - if (persistence[mpid]) { - persistence[mpid].ui = userIdentities; - } else { - persistence[mpid] = { - ui: userIdentities, - }; - } - self.savePersistence(persistence); - } - } - }; this.saveUserAttributesToPersistence = function(mpid, userAttributes) { var persistence = self.getPersistence(); diff --git a/src/store.ts b/src/store.ts index c59df5f2b..beb9f438e 100644 --- a/src/store.ts +++ b/src/store.ts @@ -6,6 +6,7 @@ import { IdentityCallback, SDKEventCustomFlags, ConsentState, + UserIdentities, } from '@mparticle/web-sdk'; import { IKitConfigs } from './configAPIClient'; import Constants from './constants'; @@ -194,6 +195,8 @@ export interface IStore { setFirstSeenTime?(mpid: MPID, time?: number): void; getLastSeenTime?(mpid: MPID): number; setLastSeenTime?(mpid: MPID, time?: number): void; + getUserIdentities?(mpid: MPID): UserIdentities; + setUserIdentities?(mpid: MPID, userIdentities: UserIdentities): void; addMpidToSessionHistory?(mpid: MPID, previousMpid?: MPID): void; hasInvalidIdentifyRequest?: () => boolean; @@ -527,15 +530,13 @@ export default function Store( isEmpty(identifyRequest.userIdentities)) || !identifyRequest ); - } - - + }; this.getConsentState = (mpid: MPID): ConsentState => { const { fromMinifiedJsonObject, } = mpInstance._Consent.ConsentSerialization; - + const serializedConsentState = this._getFromPersistence< IMinifiedConsentJSONObject >(mpid, 'con'); @@ -616,6 +617,13 @@ export default function Store( ); }; + this.getUserIdentities = (mpid: MPID): UserIdentities => + this._getFromPersistence(mpid, 'ui') || {}; + + this.setUserIdentities = (mpid: MPID, userIdentities: UserIdentities) => { + this._setPersistence(mpid, 'ui', userIdentities); + }; + this.addMpidToSessionHistory = (mpid: MPID, previousMPID?: MPID): void => { const indexOfMPID = this.currentSessionMPIDs.indexOf(mpid); diff --git a/test/src/tests-store.ts b/test/src/tests-store.ts index dc17291fc..32e679be5 100644 --- a/test/src/tests-store.ts +++ b/test/src/tests-store.ts @@ -729,8 +729,6 @@ describe('Store', () => { }); }); - - describe('#getFirstSeenTime', () => { it('should return the firstSeenTime from the store', () => { const store: IStore = new Store( @@ -923,6 +921,99 @@ describe('Store', () => { expect(fromPersistence[testMPID].lst).to.equal(54321); }); }); + + describe('#getUserIdentities', () => { + it('should return the userIdentities from the store', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.persistenceData[testMPID] = { + ui: { customerid: '12345' }, + }; + + expect(store.getUserIdentities(testMPID)).to.deep.equal({ + customerid: '12345', + }); + }); + + it('should return an empty object if mpid is null', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + expect(store.getUserIdentities(null)).to.deep.equal({}); + }); + + it('should return an empty object if no userIdentities are found', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + expect(store.getUserIdentities(testMPID)).to.deep.equal({}); + }); + }); + + describe('#setUserIdentities', () => { + it('should set userIdentities in the store', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.setUserIdentities(testMPID, { customerid: '12345' }); + expect(store.persistenceData[testMPID].ui).to.deep.equal({ + customerid: '12345', + }); + }); + + it('should set userIdentities in persistence', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.setUserIdentities(testMPID, { customerid: '12345' }); + const fromPersistence = window.mParticle + .getInstance() + ._Persistence.getPersistence(); + + expect(fromPersistence[testMPID]).to.be.ok; + expect(fromPersistence[testMPID].ui).to.be.ok; + expect(fromPersistence[testMPID].ui).to.deep.equal({ + customerid: '12345', + }); + }); + + it('should override persistence with store values', () => { + const persistenceValue = JSON.stringify({ + testMPID: { + ui: { customerid: '12345' }, + }, + }); + + localStorage.setItem(workspaceCookieName, persistenceValue); + + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.setUserIdentities(testMPID, { customerid: '54321' }); + const fromPersistence = window.mParticle + .getInstance() + ._Persistence.getPersistence(); + + expect(fromPersistence[testMPID]).to.be.ok; + expect(fromPersistence[testMPID].ui).to.be.ok; + expect(fromPersistence[testMPID].ui).to.deep.equal({ + customerid: '54321', + }); + }); + }); describe('#nullifySessionData', () => { it('should nullify session data on the store', () => { @@ -1386,4 +1477,4 @@ describe('Store', () => { }); }); }); -}); +}); \ No newline at end of file From 147b80fb89284ca1071b61e2986e0de0763deb1e Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Thu, 18 Apr 2024 14:45:43 -0400 Subject: [PATCH 3/8] refactor: Migrate globalStorageAttribute getter --- src/persistence.interfaces.ts | 2 +- src/persistence.js | 41 +------------ src/store.ts | 18 ++++++ test/src/tests-store.ts | 110 ++++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 40 deletions(-) diff --git a/src/persistence.interfaces.ts b/src/persistence.interfaces.ts index 5616f0dee..e828b21b6 100644 --- a/src/persistence.interfaces.ts +++ b/src/persistence.interfaces.ts @@ -36,7 +36,7 @@ export interface IGlobalStoreV2MinifiedKeys { das: string; // Device ID/ Device Application String ia: IntegrationAttributes; c: Context; - csm: MPID[]; // Current Session MPIDs + csm?: MPID[]; // Current Session MPIDs les: number; // Last Event Sent Timestamp ssd: number; // Session Start Date } diff --git a/src/persistence.js b/src/persistence.js index 462642878..047775c5c 100644 --- a/src/persistence.js +++ b/src/persistence.js @@ -356,15 +356,8 @@ export default function _Persistence(mpInstance) { } if (!mpInstance._Store.SDKConfig.useCookieStorage) { - localStorageData.gs = localStorageData.gs || {}; - localStorageData.l = mpInstance._Store.isLoggedIn ? 1 : 0; - - if (mpInstance._Store.sessionId) { - localStorageData.gs.csm = mpInstance._Store.currentSessionMPIDs; - } - - localStorageData.gs.ie = mpInstance._Store.isEnabled; + localStorageData.gs = mpInstance._Store.getGlobalStorageAttributes(); if (mpid) { localStorageData.cu = mpid; @@ -379,8 +372,6 @@ export default function _Persistence(mpInstance) { mpInstance._Store.nonCurrentUserMPIDs = {}; } - localStorageData = setGlobalStorageAttributes(localStorageData); - try { window.localStorage.setItem( encodeURIComponent(key), @@ -394,28 +385,6 @@ export default function _Persistence(mpInstance) { } }; - function setGlobalStorageAttributes(data) { - var store = mpInstance._Store; - data.gs.sid = store.sessionId; - data.gs.ie = store.isEnabled; - data.gs.sa = store.sessionAttributes; - data.gs.ss = store.serverSettings; - data.gs.dt = store.devToken; - data.gs.les = store.dateLastEventSent - ? store.dateLastEventSent.getTime() - : null; - data.gs.av = store.SDKConfig.appVersion; - data.gs.cgid = store.clientId; - data.gs.das = store.deviceId; - data.gs.c = store.context; - data.gs.ssd = store.sessionStartDate - ? store.sessionStartDate.getTime() - : 0; - data.gs.ia = store.integrationAttributes; - - return data; - } - this.getLocalStorage = function() { if (!mpInstance._Store.isLocalStorageAvailable) { return null; @@ -538,11 +507,7 @@ export default function _Persistence(mpInstance) { domain = ';domain=' + cookieDomain; } - cookies.gs = cookies.gs || {}; - - if (mpInstance._Store.sessionId) { - cookies.gs.csm = mpInstance._Store.currentSessionMPIDs; - } + cookies.gs = mpInstance._Store.getGlobalStorageAttributes(); if (mpid) { cookies.cu = mpid; @@ -550,8 +515,6 @@ export default function _Persistence(mpInstance) { cookies.l = mpInstance._Store.isLoggedIn ? 1 : 0; - cookies = setGlobalStorageAttributes(cookies); - if (Object.keys(mpInstance._Store.nonCurrentUserMPIDs).length) { cookies = mpInstance._Helpers.extend( {}, diff --git a/src/store.ts b/src/store.ts index beb9f438e..399f9446c 100644 --- a/src/store.ts +++ b/src/store.ts @@ -199,6 +199,7 @@ export interface IStore { setUserIdentities?(mpid: MPID, userIdentities: UserIdentities): void; addMpidToSessionHistory?(mpid: MPID, previousMpid?: MPID): void; + getGlobalStorageAttributes?(): IGlobalStoreV2MinifiedKeys; hasInvalidIdentifyRequest?: () => boolean; nullifySession?: () => void; processConfig(config: SDKInitConfig): void; @@ -522,6 +523,23 @@ export default function Store( } }; + this.getGlobalStorageAttributes = () => ({ + sid: this.sessionId, + ie: this.isEnabled, + sa: this.sessionAttributes, + ss: this.serverSettings, + dt: this.devToken, + les: this.dateLastEventSent ? this.dateLastEventSent.getTime() : null, + av: this.SDKConfig.appVersion, + cgid: this.clientId, + das: this.deviceId, + c: this.context, + ssd: this.sessionStartDate ? this.sessionStartDate.getTime() : 0, + ia: this.integrationAttributes, + + csm: this.sessionId ? this.currentSessionMPIDs : undefined, + }); + this.hasInvalidIdentifyRequest = (): boolean => { const { identifyRequest } = this.SDKConfig; return ( diff --git a/test/src/tests-store.ts b/test/src/tests-store.ts index 32e679be5..89c661362 100644 --- a/test/src/tests-store.ts +++ b/test/src/tests-store.ts @@ -1263,6 +1263,116 @@ describe('Store', () => { }); }); + describe('#getGlobalStorageAttributes', () => { + it('should return the global storage attributes from the store', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + const dateLastEventSent = new Date(); + const sessionStartDate = new Date(); + + store.sessionId = 'test-session-id'; + store.isEnabled = true; + store.sessionAttributes = { foo: 'bar ' }; + store.serverSettings = { fizz: 'buzz' }; + store.devToken = 'test-dev'; + store.dateLastEventSent = dateLastEventSent; + store.SDKConfig.appVersion = '1.0'; + store.clientId = 'test-client-id'; + store.deviceId = 'test-device-id'; + store.context = { data_plan: { plan_id: 'test-plan-id' } }; + store.sessionStartDate = sessionStartDate; + store.integrationAttributes = { 128: { MCID: 'abcdefg' } }; + store.currentSessionMPIDs = ['test-mpid', 'another-mpid']; + + const expectedGS = { + sid: 'test-session-id', + ie: true, + sa: { foo: 'bar ' }, + ss: { fizz: 'buzz' }, + dt: 'test-dev', + av: '1.0', + cgid: 'test-client-id', + das: 'test-device-id', + ia: { 128: { MCID: 'abcdefg' } }, + c: { data_plan: { plan_id: 'test-plan-id' } }, + les: dateLastEventSent.getTime(), + ssd: sessionStartDate.getTime(), + csm: ['test-mpid', 'another-mpid'], + }; + + const actualGS = store.getGlobalStorageAttributes(); + + expect(actualGS.sid, 'session id').to.deep.equal(expectedGS.sid); + expect(actualGS.ie, 'is enabled').to.deep.equal(expectedGS.ie); + expect(actualGS.sa, 'session attributes').to.deep.equal( + expectedGS.sa + ); + expect(actualGS.ss, 'server settings').to.deep.equal(expectedGS.ss); + expect(actualGS.dt, 'dev token').to.deep.equal(expectedGS.dt); + expect(actualGS.av, 'app version').to.deep.equal(expectedGS.av); + expect(actualGS.cgid, 'client id').to.deep.equal(expectedGS.cgid); + expect(actualGS.das, 'device id').to.deep.equal(expectedGS.das); + expect(actualGS.ia, 'integration attributes').to.deep.equal( + expectedGS.ia + ); + expect(actualGS.c, 'context').to.deep.equal(expectedGS.c); + expect(actualGS.les, 'last event sent').to.deep.equal( + expectedGS.les + ); + expect(actualGS.ssd, 'session start date').to.deep.equal( + expectedGS.ssd + ); + expect(actualGS.csm, 'current session mpids').to.deep.equal( + expectedGS.csm + ); + + // Tests to make sure we're not accidentally adding anything extra to the global storage attributes + expect(actualGS).to.deep.equal(expectedGS); + }); + + it('should return null if last event sent is null', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.dateLastEventSent = null; + + const actualGS = store.getGlobalStorageAttributes(); + + expect(actualGS.les).to.be.null; + }); + + it('should return 0 if session start date is null', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.sessionStartDate = null; + + const actualGS = store.getGlobalStorageAttributes(); + + expect(actualGS.ssd).to.equal(0); + }); + + it('should not include csm if there is no session', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.sessionId = null; + + const actualGS = store.getGlobalStorageAttributes(); + + expect(actualGS.csm).to.be.undefined; + }); + }); + describe('#syncPersistenceData', () => { it('should sync store with persistence values', () => { const persistenceValue = JSON.stringify({ From f70e4d18b8ee1c407ce4f8929c74ec63931e9fb0 Mon Sep 17 00:00:00 2001 From: Alex S <49695018+alexs-mparticle@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:07:40 -0400 Subject: [PATCH 4/8] refactor: Use mpid for current user within persistence (#871) --- src/persistence.js | 58 +++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/src/persistence.js b/src/persistence.js index 047775c5c..9e27723ad 100644 --- a/src/persistence.js +++ b/src/persistence.js @@ -330,16 +330,17 @@ export default function _Persistence(mpInstance) { return; } - var key = mpInstance._Store.storageName, - allLocalStorageProducts = self.getAllUserProductsFromLS(), - localStorageData = self.getLocalStorage() || {}, - currentUser = mpInstance.Identity.getCurrentUser(), - mpid = currentUser ? currentUser.getMPID() : null, - currentUserProducts = { - cp: allLocalStorageProducts[mpid] - ? allLocalStorageProducts[mpid].cp - : [], - }; + const mpid = mpInstance._Store.mpid; + + const key = mpInstance._Store.storageName; + let allLocalStorageProducts = self.getAllUserProductsFromLS(); + let localStorageData = self.getLocalStorage() || {}; + const currentUserProducts = { + cp: allLocalStorageProducts[mpid] + ? allLocalStorageProducts[mpid].cp + : [], + }; + if (mpid) { allLocalStorageProducts = allLocalStorageProducts || {}; allLocalStorageProducts[mpid] = currentUserProducts; @@ -479,27 +480,22 @@ export default function _Persistence(mpInstance) { // https://go.mparticle.com/work/SQDSDKS-5022 // https://go.mparticle.com/work/SQDSDKS-6021 this.setCookie = function() { - var mpid, - currentUser = mpInstance.Identity.getCurrentUser(); - if (currentUser) { - mpid = currentUser.getMPID(); - } - var date = new Date(), - key = mpInstance._Store.storageName, - cookies = self.getCookie() || {}, - expires = new Date( - date.getTime() + - mpInstance._Store.SDKConfig.cookieExpiration * - 24 * - 60 * - 60 * - 1000 - ).toGMTString(), - cookieDomain, - domain, - encodedCookiesWithExpirationAndPath; - - cookieDomain = self.getCookieDomain(); + const mpid = mpInstance._Store.mpid; + + const date = new Date(); + const key = mpInstance._Store.storageName; + let cookies = self.getCookie() || {}; + const expires = new Date( + date.getTime() + + mpInstance._Store.SDKConfig.cookieExpiration * + 24 * + 60 * + 60 * + 1000 + ).toGMTString(); + const cookieDomain = self.getCookieDomain(); + let domain; + let encodedCookiesWithExpirationAndPath; if (cookieDomain === '') { domain = ''; From 6b83adb24c511bea7a2ead037733d9a38d035cc7 Mon Sep 17 00:00:00 2001 From: Alex S <49695018+alexs-mparticle@users.noreply.github.com> Date: Wed, 24 Apr 2024 14:22:01 -0400 Subject: [PATCH 5/8] refactor: Migrate User Attribute Persistence Methods (#868) --- src/filteredMparticleUser.js | 4 +- src/identity.js | 56 +++++++----------- src/persistence.interfaces.ts | 5 -- src/persistence.js | 29 ---------- src/sdkRuntimeModels.ts | 14 ++--- src/store.ts | 13 ++++- test/src/tests-serverModel.ts | 6 +- test/src/tests-store.ts | 103 ++++++++++++++++++++++++++++++++++ 8 files changed, 146 insertions(+), 84 deletions(-) diff --git a/src/filteredMparticleUser.js b/src/filteredMparticleUser.js index e5f181357..640fd356a 100644 --- a/src/filteredMparticleUser.js +++ b/src/filteredMparticleUser.js @@ -68,9 +68,7 @@ export default function filteredMparticleUser( }, getAllUserAttributes: function() { var userAttributesCopy = {}; - var userAttributes = mpInstance._Persistence.getAllUserAttributes( - mpid - ); + var userAttributes = mpInstance._Store.getUserAttributes(mpid); if (userAttributes) { for (var prop in userAttributes) { diff --git a/src/identity.js b/src/identity.js index 4546e74a2..dc008650f 100644 --- a/src/identity.js +++ b/src/identity.js @@ -863,12 +863,9 @@ export default function Identity(mpInstance) { * @param {String} key * @param {String} value */ + // https://go.mparticle.com/work/SQDSDKS-4576 + // https://go.mparticle.com/work/SQDSDKS-6373 setUserAttribute: function(key, newValue) { - var cookies, - userAttributes, - previousUserAttributeValue, - isNewAttribute; - mpInstance._SessionManager.resetSessionTimer(); if (mpInstance._Helpers.canLog()) { @@ -893,11 +890,11 @@ export default function Identity(mpInstance) { JSON.stringify({ key: key, value: newValue }) ); } else { - cookies = mpInstance._Persistence.getPersistence(); - - userAttributes = this.getAllUserAttributes(); + const userAttributes = this.getAllUserAttributes(); + let previousUserAttributeValue; + let isNewAttribute; - var existingProp = mpInstance._Helpers.findKeyInObject( + const existingProp = mpInstance._Helpers.findKeyInObject( userAttributes, key ); @@ -912,13 +909,10 @@ export default function Identity(mpInstance) { } userAttributes[key] = newValue; - if (cookies && cookies[mpid]) { - cookies[mpid].ua = userAttributes; - mpInstance._Persistence.savePersistence( - cookies, - mpid - ); - } + mpInstance._Store.setUserAttributes( + mpid, + userAttributes + ); self.sendUserAttributeChangeEvent( key, @@ -946,6 +940,7 @@ export default function Identity(mpInstance) { * @method setUserAttributes * @param {Object} user attribute object with keys of the attribute type, and value of the attribute value */ + // https://go.mparticle.com/work/SQDSDKS-6373 setUserAttributes: function(userAttributes) { mpInstance._SessionManager.resetSessionTimer(); if (isObject(userAttributes)) { @@ -1033,13 +1028,8 @@ export default function Identity(mpInstance) { * @param {String} key * @param {Array} value an array of values */ + // https://go.mparticle.com/work/SQDSDKS-6373 setUserAttributeList: function(key, newValue) { - var cookies, - userAttributes, - previousUserAttributeValue, - isNewAttribute, - userAttributeChange; - mpInstance._SessionManager.resetSessionTimer(); if (!mpInstance._Helpers.Validators.isValidKeyValue(key)) { @@ -1055,7 +1045,7 @@ export default function Identity(mpInstance) { return; } - var arrayCopy = newValue.slice(); + const arrayCopy = newValue.slice(); if (mpInstance._Store.webviewBridgeEnabled) { mpInstance._NativeSdkHelpers.sendToNative( @@ -1063,11 +1053,12 @@ export default function Identity(mpInstance) { JSON.stringify({ key: key, value: arrayCopy }) ); } else { - cookies = mpInstance._Persistence.getPersistence(); - - userAttributes = this.getAllUserAttributes(); + const userAttributes = this.getAllUserAttributes(); + let previousUserAttributeValue; + let isNewAttribute; + let userAttributeChange; - var existingProp = mpInstance._Helpers.findKeyInObject( + const existingProp = mpInstance._Helpers.findKeyInObject( userAttributes, key ); @@ -1082,12 +1073,9 @@ export default function Identity(mpInstance) { } userAttributes[key] = arrayCopy; - if (cookies && cookies[mpid]) { - cookies[mpid].ua = userAttributes; - mpInstance._Persistence.savePersistence(cookies, mpid); - } + mpInstance._Store.setUserAttributes(mpid, userAttributes); - // If the new attributeList length is different previous, then there is a change event. + // If the new attributeList length is different than the previous, then there is a change event. // Loop through new attributes list, see if they are all in the same index as previous user attributes list // If there are any changes, break, and immediately send a userAttributeChangeEvent with full array as a value if ( @@ -1192,9 +1180,7 @@ export default function Identity(mpInstance) { */ getAllUserAttributes: function() { var userAttributesCopy = {}; - var userAttributes = mpInstance._Persistence.getAllUserAttributes( - mpid - ); + var userAttributes = mpInstance._Store.getUserAttributes(mpid); if (userAttributes) { for (var prop in userAttributes) { diff --git a/src/persistence.interfaces.ts b/src/persistence.interfaces.ts index e828b21b6..a43b0f066 100644 --- a/src/persistence.interfaces.ts +++ b/src/persistence.interfaces.ts @@ -123,13 +123,8 @@ export interface IPersistence { decodePersistence(persistenceString: string): string; getCookieDomain(): string; getDomain(doc: string, locationHostname: string): string; - getAllUserAttributes(mpid: MPID): AllUserAttributes; getCartProducts(mpid: MPID): Product[]; setCartProducts(allProducts: Product[]): void; - saveUserAttributesToPersistence( - mpid: MPID, - userAttributes: UserAttributes - ): void; saveUserCookieSyncDatesToPersistence(mpid: MPID, csd: CookieSyncDate): void; saveUserConsentStateToCookies(mpid, consentState: ConsentState): void; savePersistence(persistance: IPersistenceMinified): void; diff --git a/src/persistence.js b/src/persistence.js index 9e27723ad..d24a77600 100644 --- a/src/persistence.js +++ b/src/persistence.js @@ -841,16 +841,6 @@ export default function _Persistence(mpInstance) { return ''; }; - this.getAllUserAttributes = function(mpid) { - var persistence = self.getPersistence(); - - if (persistence && persistence[mpid] && persistence[mpid].ua) { - return persistence[mpid].ua; - } else { - return {}; - } - }; - this.getCartProducts = function(mpid) { var allCartProducts, cartProductsString = localStorage.getItem( @@ -887,25 +877,6 @@ export default function _Persistence(mpInstance) { } }; - this.saveUserAttributesToPersistence = function(mpid, userAttributes) { - var persistence = self.getPersistence(); - if (userAttributes) { - if (persistence) { - if (persistence[mpid]) { - // TODO: Investigate why setting this to UI still shows up as UA - // when running `mParticle.getInstance()._Persistence.getLocalStorage()` - // https://go.mparticle.com/work/SQDSDKS-5195 - persistence[mpid].ui = userAttributes; - } else { - persistence[mpid] = { - ui: userAttributes, - }; - } - } - self.savePersistence(persistence); - } - }; - this.saveUserCookieSyncDatesToPersistence = function(mpid, csd) { if (csd) { var persistence = self.getPersistence(); diff --git a/src/sdkRuntimeModels.ts b/src/sdkRuntimeModels.ts index cabe2fddb..53be4be8d 100644 --- a/src/sdkRuntimeModels.ts +++ b/src/sdkRuntimeModels.ts @@ -26,10 +26,7 @@ export interface SDKEvent { IsFirstRun: boolean; EventName: string; EventCategory: number; - - // https://go.mparticle.com/work/SQDSDKS-5196 - UserAttributes?: { [key: string]: string | string[] | null }; - + UserAttributes?: SDKUserAttribute; UserIdentities?: SDKUserIdentity[]; SourceMessageId: string; MPID: string; @@ -73,6 +70,10 @@ export interface SDKDataPlan { PlanId?: string | null; } +export interface SDKUserAttribute { + [key: string]: string | string[] | null; +} + export interface SDKUserIdentity { Identity?: string; Type: number; @@ -311,10 +312,7 @@ export interface SDKConfigApi { export interface MParticleUser { getMPID?(): string; getConsentState?(): SDKConsentState; - - // https://go.mparticle.com/work/SQDSDKS-5196 - getAllUserAttributes?(): any; - + getAllUserAttributes?(): SDKUserAttribute; getUserIdentities?(): IdentityApiData; isLoggedIn?(): boolean; } diff --git a/src/store.ts b/src/store.ts index 399f9446c..3e5f9a293 100644 --- a/src/store.ts +++ b/src/store.ts @@ -36,6 +36,7 @@ import { Kit, MPForwarder } from './forwarders.interfaces'; import { IGlobalStoreV2MinifiedKeys, IPersistenceMinified, + UserAttributes, } from './persistence.interfaces'; // This represents the runtime configuration of the SDK AFTER @@ -195,6 +196,8 @@ export interface IStore { setFirstSeenTime?(mpid: MPID, time?: number): void; getLastSeenTime?(mpid: MPID): number; setLastSeenTime?(mpid: MPID, time?: number): void; + getUserAttributes?(mpid: MPID): UserAttributes; + setUserAttributes?(mpid: MPID, attributes: UserAttributes): void; getUserIdentities?(mpid: MPID): UserIdentities; setUserIdentities?(mpid: MPID, userIdentities: UserIdentities): void; @@ -640,7 +643,15 @@ export default function Store( this.setUserIdentities = (mpid: MPID, userIdentities: UserIdentities) => { this._setPersistence(mpid, 'ui', userIdentities); - }; + } + + this.getUserAttributes = (mpid: MPID): UserAttributes => + this._getFromPersistence(mpid, 'ua') || {}; + + this.setUserAttributes = ( + mpid: MPID, + userAttributes: UserAttributes + ): void => this._setPersistence(mpid, 'ua', userAttributes); this.addMpidToSessionHistory = (mpid: MPID, previousMPID?: MPID): void => { const indexOfMPID = this.currentSessionMPIDs.indexOf(mpid); diff --git a/test/src/tests-serverModel.ts b/test/src/tests-serverModel.ts index bdbd27985..b90c9f840 100644 --- a/test/src/tests-serverModel.ts +++ b/test/src/tests-serverModel.ts @@ -3,8 +3,8 @@ import sinon from 'sinon'; import { urls, testMPID, apiKey } from './config/constants'; import { expect } from 'chai'; import { IUploadObject } from '../../src/serverModel'; -import { AllUserAttributes, IdentityApiData } from '@mparticle/web-sdk'; -import { BaseEvent, MParticleUser, SDKEvent } from '../../src/sdkRuntimeModels'; +import { IdentityApiData } from '@mparticle/web-sdk'; +import { BaseEvent, MParticleUser, SDKEvent, SDKUserAttribute } from '../../src/sdkRuntimeModels'; import Constants from '../../src/constants'; import { SDKConsentState, @@ -1571,7 +1571,7 @@ describe('ServerModel', () => { getUserIdentities: (): IdentityApiData => ({ userIdentities: {}, }), - getAllUserAttributes: (): AllUserAttributes => { + getAllUserAttributes: (): SDKUserAttribute => { return attributes; }, getMPID: () => { diff --git a/test/src/tests-store.ts b/test/src/tests-store.ts index 89c661362..739bd3eec 100644 --- a/test/src/tests-store.ts +++ b/test/src/tests-store.ts @@ -705,6 +705,109 @@ describe('Store', () => { }); }); + describe('#getUserAttributes', () => { + it('should return user attributes from the store', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.persistenceData[testMPID] = { + ua: { foo: 'bar' }, + }; + + expect(store.getUserAttributes(testMPID)).to.deep.equal({ + foo: 'bar', + }); + }); + + it('should return an empty object if mpid is null', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + expect(store.getUserAttributes(null)).to.deep.equal({}); + }); + + it('should return an empty object if no user attributes are found', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + expect(store.getUserAttributes(testMPID)).to.deep.equal({}); + }); + }); + + describe('#setUserAttributes', () => { + it('should set user attributes in the store', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.setUserAttributes(testMPID, { foo: 'bar' }); + expect(store.persistenceData[testMPID].ua).to.deep.equal({ + foo: 'bar', + }); + + store.setUserAttributes(testMPID, { fiz: 'buzz' }); + expect(store.persistenceData[testMPID].ua).to.deep.equal({ + fiz: 'buzz', + }); + }); + + it('should set user attributes in persistence', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.setUserAttributes(testMPID, { foo: 'bar' }); + const fromPersistence = window.mParticle + .getInstance() + ._Persistence.getPersistence(); + + expect(fromPersistence[testMPID]).to.be.ok; + expect(fromPersistence[testMPID].ua).to.be.ok; + expect(fromPersistence[testMPID].ua).to.deep.equal({ + foo: 'bar', + }); + }); + + it('should override persistence with store values', () => { + const persistenceValue = JSON.stringify({ + gs: { + sid: 'sid', + les: new Date().getTime(), + }, + testMPID: { + ua: { foo: 'bar' }, + }, + cu: testMPID, + }); + + localStorage.setItem(workspaceCookieName, persistenceValue); + + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.setUserAttributes(testMPID, { fizz: 'buzz' }); + const fromPersistence = window.mParticle + .getInstance() + ._Persistence.getPersistence(); + + expect(fromPersistence[testMPID]).to.be.ok; + expect(fromPersistence[testMPID].ua).to.be.ok; + expect(fromPersistence[testMPID].ua).to.deep.equal({ + fizz: 'buzz', + }); + }); + }); + describe('#setDeviceId', () => { it('should set the deviceId in the store', () => { const store: IStore = new Store( From 68f9d2330e67ac13f84e5df428f7b10eb2b0d506 Mon Sep 17 00:00:00 2001 From: Alex S <49695018+alexs-mparticle@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:02:20 -0400 Subject: [PATCH 6/8] refactor: Add test cases for setting isLoggedIn value via persistence (#872) --- src/persistence.interfaces.ts | 4 +++ test/src/tests-persistence.ts | 67 +++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/persistence.interfaces.ts b/src/persistence.interfaces.ts index a43b0f066..1f062f8ad 100644 --- a/src/persistence.interfaces.ts +++ b/src/persistence.interfaces.ts @@ -44,6 +44,10 @@ export interface IGlobalStoreV2MinifiedKeys { export interface IPersistenceMinified extends Dictionary { cu: MPID; // Current User MPID gs: IGlobalStoreV2MinifiedKeys; + + // Stored as 0 or 1 in device persistence but returned as a + // boolean when decoding from device persistence via + // _Persistence.getPersistence and _Persistence.decodePersistence l: boolean; // IsLoggedIn // Persistence Minified can also store optional dictionaries with diff --git a/test/src/tests-persistence.ts b/test/src/tests-persistence.ts index c8ea6e7c5..996a56c17 100644 --- a/test/src/tests-persistence.ts +++ b/test/src/tests-persistence.ts @@ -20,6 +20,7 @@ import { IPersistenceMinified, } from '../../src/persistence.interfaces'; import { ConsentState } from '@mparticle/web-sdk'; +import { MParticleUser } from '../../src/sdkRuntimeModels'; const { findCookie, @@ -1483,6 +1484,72 @@ describe('persistence', () => { done(); }); + it('get/set isLoggedIn for localStorage', done => { + mParticle._resetForTests(MPConfig); + + mockServer.respondWith(urls.login, [ + 200, + {}, + JSON.stringify({ mpid: 'mpid1', is_logged_in: true }), + ]); + + + mParticle.init(apiKey, mParticle.config); + let user: MParticleUser = mParticle + .getInstance() + .Identity.getCurrentUser() + expect(user).to.be.ok; + expect(user.isLoggedIn()).to.be.false; + + let localStorageData = mParticle.getInstance()._Persistence.getPersistence(); + + // The `l` property of Persistence is a boolean, but when saved + // to local storage, Persistence encodes this as a 0 or 1. + // It is then re-encoded as a boolean when retrieved from local storage. + expect(localStorageData.l).to.equal(false); + + mParticle.Identity.login(); + + localStorageData = mParticle.getInstance()._Persistence.getPersistence(); + expect(localStorageData.l).to.equal(true); + + done(); + }); + + + it('get/set isLoggedIn for cookies', done => { + mParticle._resetForTests(MPConfig); + mParticle.config.useCookieStorage = true; + + mockServer.respondWith(urls.login, [ + 200, + {}, + JSON.stringify({ mpid: 'mpid1', is_logged_in: true }), + ]); + + mParticle.init(apiKey, mParticle.config); + + let user: MParticleUser = mParticle + .getInstance() + .Identity.getCurrentUser() + expect(user).to.be.ok; + expect(user.isLoggedIn()).to.be.false; + + let cookieData = findCookie(); + + // The `l` property of Persistence is a boolean, but when saved + // to cookie storage, Persistence encodes this as a 0 or 1. + // It is then re-encoded as a boolean when retrieved from cookies storage + cookieData.l.should.equal(false); + + mParticle.Identity.login(); + + cookieData = findCookie(); + cookieData.l.should.equal(true); + + done(); + }); + it('get/set consent state for single user', done => { mParticle._resetForTests(MPConfig); From 6aacc00a42841f0e80567aca03c45630939c5c37 Mon Sep 17 00:00:00 2001 From: Alex S <49695018+alexs-mparticle@users.noreply.github.com> Date: Thu, 25 Apr 2024 12:29:54 -0400 Subject: [PATCH 7/8] refactor: Migrate cookieSyncDates setters and getters to Store (#873) --- src/cookieSyncManager.js | 37 ++++------ src/persistence.interfaces.ts | 5 +- src/persistence.js | 16 ----- src/store.ts | 9 +++ test/src/tests-store.ts | 128 +++++++++++++++++++++++++++++++++- 5 files changed, 149 insertions(+), 46 deletions(-) diff --git a/src/cookieSyncManager.js b/src/cookieSyncManager.js index 0eadf9201..acf9a4e54 100644 --- a/src/cookieSyncManager.js +++ b/src/cookieSyncManager.js @@ -6,6 +6,7 @@ export default function cookieSyncManager(mpInstance) { var self = this; // Public + // https://go.mparticle.com/work/SQDSDKS-6375 this.attemptCookieSync = function(previousMPID, mpid, mpidIsNotInCookies) { // TODO: These should move inside the for loop var pixelConfig, @@ -17,6 +18,8 @@ export default function cookieSyncManager(mpInstance) { // TODO: Make this exit quicker instead of nested if (mpid && !mpInstance._Store.webviewBridgeEnabled) { + const cookieSyncDates = mpInstance._Store.getCookieSyncDates(mpid); + mpInstance._Store.pixelConfigurations.forEach(function( pixelSettings ) { @@ -60,22 +63,15 @@ export default function cookieSyncManager(mpInstance) { : ''; urlWithRedirect = url + encodeURIComponent(redirect); - // TODO: Refactor so that Persistence is only called once - // outside of the loop - var persistence = mpInstance._Persistence.getPersistence(); - // TODO: Is there a historic reason for checking for previousMPID? // it does not appear to be passed in anywhere if (previousMPID && previousMPID !== mpid) { - if (persistence && persistence[mpid]) { - if (!persistence[mpid].csd) { - persistence[mpid].csd = {}; - } + if (cookieSyncDates) { self.performCookieSync( urlWithRedirect, pixelConfig.moduleId, mpid, - persistence[mpid].csd, + cookieSyncDates, pixelConfig.filteringConsentRuleValues, mpidIsNotInCookies, requiresConsent @@ -85,16 +81,10 @@ export default function cookieSyncManager(mpInstance) { } else { // TODO: Refactor to check for the inverse and exit early // rather than nesting - if (persistence[mpid]) { - if (!persistence[mpid].csd) { - persistence[mpid].csd = {}; - } - lastSyncDateForModule = persistence[mpid].csd[ - pixelConfig.moduleId.toString() - ] - ? persistence[mpid].csd[ - pixelConfig.moduleId.toString() - ] + if (cookieSyncDates) { + const moduleId = pixelConfig.moduleId.toString(); + lastSyncDateForModule = cookieSyncDates[moduleId] + ? cookieSyncDates[moduleId] : null; if (lastSyncDateForModule) { @@ -117,7 +107,7 @@ export default function cookieSyncManager(mpInstance) { urlWithRedirect, pixelConfig.moduleId, mpid, - persistence[mpid].csd, + cookieSyncDates, pixelConfig.filteringConsentRuleValues, mpidIsNotInCookies, requiresConsent @@ -128,7 +118,7 @@ export default function cookieSyncManager(mpInstance) { urlWithRedirect, pixelConfig.moduleId, mpid, - persistence[mpid].csd, + cookieSyncDates, pixelConfig.filteringConsentRuleValues, mpidIsNotInCookies, requiresConsent @@ -186,10 +176,7 @@ export default function cookieSyncManager(mpInstance) { img.onload = function() { // TODO: Break this out into a convenience method so we can unit test cookieSyncDates[moduleId.toString()] = new Date().getTime(); - mpInstance._Persistence.saveUserCookieSyncDatesToPersistence( - mpid, - cookieSyncDates - ); + mpInstance._Store.setCookieSyncDates(mpid, cookieSyncDates); }; img.src = url; } diff --git a/src/persistence.interfaces.ts b/src/persistence.interfaces.ts index 1f062f8ad..520d46707 100644 --- a/src/persistence.interfaces.ts +++ b/src/persistence.interfaces.ts @@ -87,10 +87,10 @@ export interface IPersistenceMinified extends Dictionary { // }; } -export type CookieSyncDate = Dictionary; +export type CookieSyncDates = Dictionary; export interface IUserPersistenceMinified extends Dictionary { - csd: CookieSyncDate; // Cookie Sync Dates // list of timestamps for last cookie sync + csd: CookieSyncDates; // Cookie Sync Dates // list of timestamps for last cookie sync con: IMinifiedConsentJSONObject; // Consent State ui: UserIdentities; // User Identities ua: UserAttributes; // User Attributes @@ -129,7 +129,6 @@ export interface IPersistence { getDomain(doc: string, locationHostname: string): string; getCartProducts(mpid: MPID): Product[]; setCartProducts(allProducts: Product[]): void; - saveUserCookieSyncDatesToPersistence(mpid: MPID, csd: CookieSyncDate): void; saveUserConsentStateToCookies(mpid, consentState: ConsentState): void; savePersistence(persistance: IPersistenceMinified): void; getPersistence(): IPersistenceMinified; diff --git a/src/persistence.js b/src/persistence.js index d24a77600..fce96621f 100644 --- a/src/persistence.js +++ b/src/persistence.js @@ -877,22 +877,6 @@ export default function _Persistence(mpInstance) { } }; - this.saveUserCookieSyncDatesToPersistence = function(mpid, csd) { - if (csd) { - var persistence = self.getPersistence(); - if (persistence) { - if (persistence[mpid]) { - persistence[mpid].csd = csd; - } else { - persistence[mpid] = { - csd: csd, - }; - } - } - self.savePersistence(persistence); - } - }; - this.saveUserConsentStateToCookies = function(mpid, consentState) { //it's currently not supported to set persistence //for any MPID that's not the current one. diff --git a/src/store.ts b/src/store.ts index 3e5f9a293..d909a0a59 100644 --- a/src/store.ts +++ b/src/store.ts @@ -34,6 +34,7 @@ import { import { IMinifiedConsentJSONObject, SDKConsentState } from './consent'; import { Kit, MPForwarder } from './forwarders.interfaces'; import { + CookieSyncDates, IGlobalStoreV2MinifiedKeys, IPersistenceMinified, UserAttributes, @@ -184,6 +185,8 @@ export interface IStore { persistenceData?: IPersistenceMinified; + getCookieSyncDates?(mpid: MPID): CookieSyncDates; + setCookieSyncDates?(mpid: MPID, cookieSyncDates: CookieSyncDates): void; getConsentState?(mpid: MPID): ConsentState | null; setConsentState?(mpid: MPID, consentState: ConsentState): void; @@ -553,6 +556,12 @@ export default function Store( ); }; + this.getCookieSyncDates = (mpid: MPID): CookieSyncDates => + this._getFromPersistence(mpid, 'csd') || {}; + + this.setCookieSyncDates = (mpid: MPID, cookieSyncDates: CookieSyncDates) => + this._setPersistence(mpid, 'csd', cookieSyncDates); + this.getConsentState = (mpid: MPID): ConsentState => { const { fromMinifiedJsonObject, diff --git a/test/src/tests-store.ts b/test/src/tests-store.ts index 739bd3eec..1320341e0 100644 --- a/test/src/tests-store.ts +++ b/test/src/tests-store.ts @@ -379,6 +379,130 @@ describe('Store', () => { }); }); + describe('#getCookieSyncDates', () => { + it('should return cookie sync dates from the store', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + const expectedCookieSyncDates = { + 42: 12345, + }; + + store.persistenceData[testMPID] = { + csd: expectedCookieSyncDates, + }; + + expect(store.getCookieSyncDates(testMPID)).to.deep.equal( + expectedCookieSyncDates + ); + }); + + it('should return an empty object if no cookie sync dates are found', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + expect(store.getCookieSyncDates(testMPID)).to.deep.equal({}); + }); + + it('should return in-memory cookie sync dates if persistence is empty', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + const expectedCookieSyncDates = { + 42: 12345, + }; + + store.persistenceData[testMPID] = { + csd: expectedCookieSyncDates, + }; + + localStorage.setItem(workspaceCookieName, ''); + + expect(store.getCookieSyncDates(testMPID)).to.deep.equal( + expectedCookieSyncDates + ); + }); + }); + + describe('#setCookieSyncDates', () => { + it('should set cookie sync dates in the store', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + const cookieSyncDates = { + 42: 12345, + }; + + store.setCookieSyncDates(testMPID, cookieSyncDates); + + expect(store.persistenceData[testMPID].csd).to.deep.equal( + cookieSyncDates + ); + }); + + it('should set cookie sync dates in persistence', () => { + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + const cookieSyncDates = { + 42: 12345, + }; + + store.setCookieSyncDates(testMPID, cookieSyncDates); + + const fromPersistence = window.mParticle + .getInstance() + ._Persistence.getPersistence(); + + expect(fromPersistence[testMPID]).to.be.ok; + expect(fromPersistence[testMPID].csd).to.be.ok; + expect(fromPersistence[testMPID].csd).to.deep.equal( + cookieSyncDates + ); + }); + + it('should override persistence with store values', () => { + const cookieSyncDates = { + 42: 12345, + }; + + const persistenceValue = JSON.stringify({ + testMPID: { + csd: { + 42: 54321, + }, + }, + }); + + localStorage.setItem(workspaceCookieName, persistenceValue); + + const store: IStore = new Store( + sampleConfig, + window.mParticle.getInstance() + ); + + store.setCookieSyncDates(testMPID, cookieSyncDates); + + const fromPersistence = window.mParticle + .getInstance() + ._Persistence.getPersistence(); + + expect(fromPersistence[testMPID].csd).to.deep.equal( + cookieSyncDates + ); + }); + }); + describe('#getConsentState', () => { it('should return a consent state object from the store', () => { const store: IStore = new Store( @@ -1117,7 +1241,7 @@ describe('Store', () => { }); }); }); - + describe('#nullifySessionData', () => { it('should nullify session data on the store', () => { const store: IStore = new Store( @@ -1690,4 +1814,4 @@ describe('Store', () => { }); }); }); -}); \ No newline at end of file +}); From 54dff3d75df0241c9876cf9a44b03ae6462fb4b1 Mon Sep 17 00:00:00 2001 From: Alex S <49695018+alexs-mparticle@users.noreply.github.com> Date: Fri, 26 Apr 2024 13:53:09 -0400 Subject: [PATCH 8/8] refactor: Remove usages of Persistence Consent State (#875) --- src/persistence.interfaces.ts | 2 - src/persistence.js | 37 --------- test/src/tests-identity.js | 148 ++++++++++++++++++++++++++++++++++ test/src/tests-persistence.ts | 142 -------------------------------- 4 files changed, 148 insertions(+), 181 deletions(-) diff --git a/src/persistence.interfaces.ts b/src/persistence.interfaces.ts index 520d46707..599da9f7a 100644 --- a/src/persistence.interfaces.ts +++ b/src/persistence.interfaces.ts @@ -129,10 +129,8 @@ export interface IPersistence { getDomain(doc: string, locationHostname: string): string; getCartProducts(mpid: MPID): Product[]; setCartProducts(allProducts: Product[]): void; - saveUserConsentStateToCookies(mpid, consentState: ConsentState): void; savePersistence(persistance: IPersistenceMinified): void; getPersistence(): IPersistenceMinified; - getConsentState(mpid: MPID): ConsentState | null; getFirstSeenTime(mpid: MPID): string | null; setFirstSeenTime(mpid: MPID, time: number): void; getLastSeenTime(mpid: MPID): number | null; diff --git a/src/persistence.js b/src/persistence.js index fce96621f..0fcb7680e 100644 --- a/src/persistence.js +++ b/src/persistence.js @@ -876,31 +876,6 @@ export default function _Persistence(mpInstance) { ); } }; - - this.saveUserConsentStateToCookies = function(mpid, consentState) { - //it's currently not supported to set persistence - //for any MPID that's not the current one. - if (consentState || consentState === null) { - var persistence = self.getPersistence(); - if (persistence) { - if (persistence[mpid]) { - persistence[ - mpid - ].con = mpInstance._Consent.ConsentSerialization.toMinifiedJsonObject( - consentState - ); - } else { - persistence[mpid] = { - con: mpInstance._Consent.ConsentSerialization.toMinifiedJsonObject( - consentState - ), - }; - } - self.savePersistence(persistence); - } - } - }; - this.swapCurrentUser = function( previousMPID, currentMPID, @@ -969,18 +944,6 @@ export default function _Persistence(mpInstance) { return persistence; }; - this.getConsentState = function(mpid) { - var persistence = self.getPersistence(); - - if (persistence && persistence[mpid] && persistence[mpid].con) { - return mpInstance._Consent.ConsentSerialization.fromMinifiedJsonObject( - persistence[mpid].con - ); - } else { - return null; - } - }; - this.getFirstSeenTime = function(mpid) { if (!mpid) { return null; diff --git a/test/src/tests-identity.js b/test/src/tests-identity.js index fd70be8d0..aaa67011b 100644 --- a/test/src/tests-identity.js +++ b/test/src/tests-identity.js @@ -1,6 +1,7 @@ import Constants from '../../src/constants'; import Utils from './config/utils'; import sinon from 'sinon'; +import { expect } from 'chai'; import fetchMock from 'fetch-mock/esm/client'; import { urls, apiKey, testMPID, @@ -3084,6 +3085,153 @@ describe('identity', function() { done(); }); + describe('mParticle User', function () { + describe('Consent State', function () { + it('get/set consent state for single user', done => { + mParticle._resetForTests(MPConfig); + + mParticle.init(apiKey, mParticle.config); + let consentState = mParticle + .getInstance() + .Identity.getCurrentUser() + .getConsentState(); + + expect(consentState).to.equal(null); + consentState = mParticle.Consent.createConsentState(); + consentState.addGDPRConsentState( + 'foo purpose', + mParticle.Consent.createGDPRConsent(true, 10) + ); + + mParticle + .getInstance() + .Identity.getCurrentUser() + .setConsentState(consentState); + + const storedConsentState = mParticle + .getInstance() + .Identity.getCurrentUser() + .getConsentState(); + storedConsentState.should.be.ok(); + storedConsentState + .getGDPRConsentState() + .should.have.property('foo purpose'); + storedConsentState + .getGDPRConsentState() + ['foo purpose'].should.have.property('Consented', true); + storedConsentState + .getGDPRConsentState() + ['foo purpose'].should.have.property('Timestamp', 10); + done(); + }); + + it('get/set consent state for multiple users', done => { + mParticle._resetForTests(MPConfig); + + mParticle.init(apiKey, mParticle.config); + + mockServer.respondWith(urls.login, [ + 200, + {}, + JSON.stringify({ mpid: 'MPID1', is_logged_in: false }), + ]); + + const userIdentities1 = { + userIdentities: { + customerid: 'foo1', + }, + }; + + mParticle.Identity.login(userIdentities1); + let user1StoredConsentState = mParticle + .getInstance() + .Identity.getCurrentUser() + .getConsentState(); + expect(user1StoredConsentState).to.equal(null); + const consentState = mParticle.Consent.createConsentState(); + consentState.addGDPRConsentState( + 'foo purpose', + mParticle.Consent.createGDPRConsent(true, 10) + ); + + mParticle + .getInstance() + .Identity.getCurrentUser() + .setConsentState(consentState); + + mParticle._resetForTests(MPConfig, true); + mParticle.init(apiKey, mParticle.config); + + mockServer.respondWith(urls.login, [ + 200, + {}, + JSON.stringify({ mpid: 'MPID2', is_logged_in: false }), + ]); + + const userIdentities2 = { + userIdentities: { + customerid: 'foo2', + }, + }; + + mParticle.Identity.login(userIdentities2); + + let user2StoredConsentState = mParticle + .getInstance() + .Identity.getCurrentUser() + .getConsentState(); + expect(user2StoredConsentState).to.equal(null); + + consentState.removeGDPRConsentState('foo purpose'); + + consentState.addGDPRConsentState( + 'foo purpose 2', + mParticle.Consent.createGDPRConsent(false, 11) + ); + + mParticle + .getInstance() + .Identity.getCurrentUser() + .setConsentState(consentState); + + user1StoredConsentState = mParticle + .getInstance() + ._Store.getConsentState('MPID1'); + user2StoredConsentState = mParticle + .getInstance() + ._Store.getConsentState('MPID2'); + + user1StoredConsentState + .getGDPRConsentState() + .should.have.property('foo purpose'); + user1StoredConsentState + .getGDPRConsentState() + .should.not.have.property('foo purpose 2'); + user1StoredConsentState + .getGDPRConsentState() + ['foo purpose'].should.have.property('Consented', true); + user1StoredConsentState + .getGDPRConsentState() + ['foo purpose'].should.have.property('Timestamp', 10); + + user2StoredConsentState + .getGDPRConsentState() + .should.have.property('foo purpose 2'); + user1StoredConsentState + .getGDPRConsentState() + .should.not.have.property('foo purpose 1'); + user2StoredConsentState + .getGDPRConsentState() + ['foo purpose 2'].should.have.property('Consented', false); + user2StoredConsentState + .getGDPRConsentState() + ['foo purpose 2'].should.have.property('Timestamp', 11); + + done(); + }); + }); + }); + describe('identity caching', function() { afterEach(function() { sinon.restore(); diff --git a/test/src/tests-persistence.ts b/test/src/tests-persistence.ts index 996a56c17..d450b885e 100644 --- a/test/src/tests-persistence.ts +++ b/test/src/tests-persistence.ts @@ -1550,148 +1550,6 @@ describe('persistence', () => { done(); }); - it('get/set consent state for single user', done => { - mParticle._resetForTests(MPConfig); - - mParticle.init(apiKey, mParticle.config); - let consentState: ConsentState = mParticle - .getInstance() - .Identity.getCurrentUser() - .getConsentState(); - expect(consentState).to.equal(null); - consentState = mParticle.Consent.createConsentState(); - consentState.addGDPRConsentState( - 'foo purpose', - mParticle.Consent.createGDPRConsent(true, 10) - ); - - mParticle - .getInstance() - .Identity.getCurrentUser() - .setConsentState(consentState); - - const storedConsentState = mParticle - .getInstance() - .Identity.getCurrentUser() - .getConsentState(); - storedConsentState.should.be.ok(); - storedConsentState - .getGDPRConsentState() - .should.have.property('foo purpose'); - storedConsentState - .getGDPRConsentState() - ['foo purpose'].should.have.property('Consented', true); - storedConsentState - .getGDPRConsentState() - ['foo purpose'].should.have.property('Timestamp', 10); - done(); - }); - - it('get/set consent state for multiple users', done => { - mParticle._resetForTests(MPConfig); - - mParticle.init(apiKey, mParticle.config); - - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID1', is_logged_in: false }), - ]); - - const userIdentities1 = { - userIdentities: { - customerid: 'foo1', - }, - }; - - mParticle.Identity.login(userIdentities1); - let user1StoredConsentState: ConsentState = mParticle - .getInstance() - .Identity.getCurrentUser() - .getConsentState(); - expect(user1StoredConsentState).to.equal(null); - const consentState = mParticle.Consent.createConsentState(); - consentState.addGDPRConsentState( - 'foo purpose', - mParticle.Consent.createGDPRConsent(true, 10) - ); - - mParticle - .getInstance() - .Identity.getCurrentUser() - .setConsentState(consentState); - - mParticle._resetForTests(MPConfig, true); - mParticle.init(apiKey, mParticle.config); - - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID2', is_logged_in: false }), - ]); - - const userIdentities2 = { - userIdentities: { - customerid: 'foo2', - }, - }; - - mParticle.Identity.login(userIdentities2); - - let user2StoredConsentState: ConsentState = mParticle - .getInstance() - .Identity.getCurrentUser() - .getConsentState(); - expect(user2StoredConsentState).to.equal(null); - - consentState.removeGDPRConsentState('foo purpose'); - - consentState.addGDPRConsentState( - 'foo purpose 2', - mParticle.Consent.createGDPRConsent(false, 11) - ); - - mParticle - .getInstance() - .Identity.getCurrentUser() - .setConsentState(consentState); - - user1StoredConsentState = mParticle - .getInstance() - ._Persistence.getConsentState('MPID1'); - user2StoredConsentState = mParticle - .getInstance() - ._Persistence.getConsentState('MPID2'); - - user1StoredConsentState - .getGDPRConsentState() - .should.have.property('foo purpose'); - user1StoredConsentState - .getGDPRConsentState() - .should.not.have.property('foo purpose 2'); - user1StoredConsentState - .getGDPRConsentState() - ['foo purpose'].should.have.property('Consented', true); - user1StoredConsentState - .getGDPRConsentState() - ['foo purpose'].should.have.property('Timestamp', 10); - - user2StoredConsentState - .getGDPRConsentState() - .should.have.property('foo purpose 2'); - user1StoredConsentState - .getGDPRConsentState() - .should.not.have.property('foo purpose 1'); - user2StoredConsentState - .getGDPRConsentState() - ['foo purpose 2'].should.have.property('Consented', false); - user2StoredConsentState - .getGDPRConsentState() - ['foo purpose 2'].should.have.property('Timestamp', 11); - - done(); - }); - it('integration test - clears and creates new LS on reload if LS is corrupt', done => { const les = new Date().getTime();