From a9093a621012741369972dd95bd409da60498fec Mon Sep 17 00:00:00 2001 From: Jaissica Hora Date: Tue, 2 Sep 2025 15:04:39 -0400 Subject: [PATCH 01/15] feat: added noFunctional in batchUploader and updated tests --- src/batchUploader.ts | 2 +- test/jest/batchUploader.spec.ts | 50 ++++++++++++++- test/src/tests-batchUploader.ts | 109 ++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 2 deletions(-) diff --git a/src/batchUploader.ts b/src/batchUploader.ts index 54acfb5ac..5d13e09bb 100644 --- a/src/batchUploader.ts +++ b/src/batchUploader.ts @@ -72,7 +72,7 @@ export class BatchUploader { // Cache Offline Storage Availability boolean // so that we don't have to check it every time - this.offlineStorageEnabled = this.isOfflineStorageAvailable(); + this.offlineStorageEnabled = this.isOfflineStorageAvailable() && !mpInstance._Store.getNoFunctional(); if (this.offlineStorageEnabled) { this.eventVault = new SessionStorageVault( diff --git a/test/jest/batchUploader.spec.ts b/test/jest/batchUploader.spec.ts index 944d2a7a0..738dbbf33 100644 --- a/test/jest/batchUploader.spec.ts +++ b/test/jest/batchUploader.spec.ts @@ -15,18 +15,27 @@ describe('BatchUploader', () => { // Create a mock mParticle instance with mocked methods for instantiating a BatchUploader mockMPInstance = { _Store: { + storageName: 'mprtcl-v4_abcdef', + noFunctional: false, + getNoFunctional: function(this: any) { return this.noFunctional; }, + deviceId: 'device-1', SDKConfig: { flags: {} } }, _Helpers: { - getFeatureFlag: jest.fn().mockReturnValue(false), + getFeatureFlag: jest.fn().mockReturnValue('100'), createServiceUrl: jest.fn().mockReturnValue('https://mock-url.com') }, Identity: { getCurrentUser: jest.fn().mockReturnValue({ getMPID: () => 'test-mpid' }) + }, + Logger: { + verbose: jest.fn(), + error: jest.fn(), + warning: jest.fn(), } } as unknown as IMParticleWebSDKInstance; @@ -104,4 +113,43 @@ describe('BatchUploader', () => { expect(secondCallTime).toBe(firstCallTime); }); }); + + describe('noFunctional', () => { + beforeEach(() => { + localStorage.clear(); + sessionStorage.clear(); + }); + + it('should disable offline storage when noFunctional is true', () => { + mockMPInstance._Store.noFunctional = true; + + const uploader = new BatchUploader(mockMPInstance, 1000); + // @ts-expect-error private + expect(uploader.offlineStorageEnabled).toBe(false); + + uploader.queueEvent({ EventDataType: 4 } as any); + expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).toBeNull(); + expect(localStorage.getItem('mprtcl-v4_abcdef-batches')).toBeNull(); + }); + + it('should enable offline storage when noFunctional is default (false)', () => { + // mockMPInstance._Store.noFunctional = false; + const uploader = new BatchUploader(mockMPInstance, 1000); + + // @ts-expect-error private + expect(uploader.offlineStorageEnabled).toBe(true); + + uploader.queueEvent({ EventDataType: 4 } as any); + expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).not.toBeNull(); + }); + + it('should enable offline storage when noFunctional is false', () => { + mockMPInstance._Store.noFunctional = false; + + const uploader = new BatchUploader(mockMPInstance, 1000); + + uploader.queueEvent({ EventDataType: 4 } as any); + expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).not.toBeNull(); + }); + }); }); \ No newline at end of file diff --git a/test/src/tests-batchUploader.ts b/test/src/tests-batchUploader.ts index 5b76bb840..94320b753 100644 --- a/test/src/tests-batchUploader.ts +++ b/test/src/tests-batchUploader.ts @@ -1202,6 +1202,7 @@ describe('batch uploader', () => { }); window.sessionStorage.clear(); + window.localStorage.clear(); }); afterEach(() => { @@ -1715,5 +1716,113 @@ describe('batch uploader', () => { .catch((e) => { }) }); + + describe('noFunctional', () => { + const eventStorageKey = 'mprtcl-v4_abcdef-events'; + const batchStorageKey = 'mprtcl-v4_abcdef-batches'; + + it('should write session storage when noFunctional is default (false)', (done) => { + window.mParticle._resetForTests(MPConfig); + window.mParticle.config.flags = { + offlineStorage: '100', + ...enableBatchingConfigFlags, + }; + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + const mpInstance = window.mParticle.getInstance(); + const uploader = mpInstance._APIClient.uploader; + uploader.queueEvent(event0); + expect(!!window.sessionStorage.getItem(eventStorageKey)).to.equal(true); + done(); + }) + .catch(done); + }); + + it('should NOT write session storage when noFunctional is true', (done) => { + window.mParticle._resetForTests({ ...MPConfig, noFunctional: true }); + window.mParticle.config.flags = { + offlineStorage: '100', + ...enableBatchingConfigFlags, + }; + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(async () => { + const mpInstance = window.mParticle.getInstance(); + const uploader = mpInstance._APIClient.uploader; + uploader.queueEvent(event0); + await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); + expect(window.sessionStorage.getItem(eventStorageKey) === '').to.equal(true); + done(); + }) + .catch(done); + }); + + it('should write session storage when noFunctional is false', (done) => { + window.mParticle._resetForTests({ ...MPConfig, noFunctional: false }); + window.mParticle.config.flags = { + offlineStorage: '100', + ...enableBatchingConfigFlags, + }; + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + const mpInstance = window.mParticle.getInstance(); + const uploader = mpInstance._APIClient.uploader; + uploader.queueEvent(event0); + expect(!!window.sessionStorage.getItem(eventStorageKey)).to.equal(true); + done(); + }) + .catch(done); + }); + + it('should write local storage when noFunctional is default (false)', (done) => { + window.mParticle._resetForTests(MPConfig); + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(async () => { + const mpInstance = window.mParticle.getInstance(); + const uploader = mpInstance._APIClient.uploader; + fetchMock.post(urls.events, 500, { overwriteRoutes: true }); + uploader.queueEvent(event0); + await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); + expect(!!window.localStorage.getItem(batchStorageKey)).to.equal(true); + done(); + }) + .catch(done); + }); + + it('should NOT write local storage when noFunctional is true', (done) => { + window.mParticle._resetForTests({ ...MPConfig, noFunctional: true }); + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(async () => { + const mpInstance = window.mParticle.getInstance(); + const uploader = mpInstance._APIClient.uploader; + uploader.queueEvent(event0); + await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); + expect(window.localStorage.getItem(batchStorageKey) === '').to.equal(true); + done(); + }) + .catch(done); + }); + + it('should write local storage when noFunctional is false', (done) => { + window.mParticle._resetForTests({ ...MPConfig, noFunctional: false }); + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(async () => { + const mpInstance = window.mParticle.getInstance(); + const uploader = mpInstance._APIClient.uploader; + fetchMock.post(urls.events, 500, { overwriteRoutes: true }); + uploader.queueEvent(event0); + await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); + expect(!!window.localStorage.getItem(batchStorageKey)).to.equal(true); + done(); + }) + .catch(done); + }); + + }); }); }); \ No newline at end of file From 486b91c7c66abf4916bc5263ca3798742e9cb3d9 Mon Sep 17 00:00:00 2001 From: Jaissica Hora Date: Wed, 3 Sep 2025 14:36:54 -0400 Subject: [PATCH 02/15] fix: updated tests for offline storage for events, batches --- test/jest/batchUploader.spec.ts | 6 +- test/src/tests-batchUploader.ts | 108 +++++++++++++------------------- 2 files changed, 44 insertions(+), 70 deletions(-) diff --git a/test/jest/batchUploader.spec.ts b/test/jest/batchUploader.spec.ts index 738dbbf33..dced62e5a 100644 --- a/test/jest/batchUploader.spec.ts +++ b/test/jest/batchUploader.spec.ts @@ -124,8 +124,7 @@ describe('BatchUploader', () => { mockMPInstance._Store.noFunctional = true; const uploader = new BatchUploader(mockMPInstance, 1000); - // @ts-expect-error private - expect(uploader.offlineStorageEnabled).toBe(false); + expect(uploader['offlineStorageEnabled']).toBe(false); uploader.queueEvent({ EventDataType: 4 } as any); expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).toBeNull(); @@ -136,8 +135,7 @@ describe('BatchUploader', () => { // mockMPInstance._Store.noFunctional = false; const uploader = new BatchUploader(mockMPInstance, 1000); - // @ts-expect-error private - expect(uploader.offlineStorageEnabled).toBe(true); + expect(uploader['offlineStorageEnabled']).toBe(true); uploader.queueEvent({ EventDataType: 4 } as any); expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).not.toBeNull(); diff --git a/test/src/tests-batchUploader.ts b/test/src/tests-batchUploader.ts index 94320b753..d3db04d3c 100644 --- a/test/src/tests-batchUploader.ts +++ b/test/src/tests-batchUploader.ts @@ -1721,106 +1721,82 @@ describe('batch uploader', () => { const eventStorageKey = 'mprtcl-v4_abcdef-events'; const batchStorageKey = 'mprtcl-v4_abcdef-batches'; - it('should write session storage when noFunctional is default (false)', (done) => { + it('should write session storage when noFunctional is default (false)', async () => { window.mParticle._resetForTests(MPConfig); window.mParticle.config.flags = { offlineStorage: '100', ...enableBatchingConfigFlags, }; window.mParticle.init(apiKey, window.mParticle.config); - waitForCondition(hasIdentifyReturned) - .then(() => { - const mpInstance = window.mParticle.getInstance(); - const uploader = mpInstance._APIClient.uploader; - uploader.queueEvent(event0); - expect(!!window.sessionStorage.getItem(eventStorageKey)).to.equal(true); - done(); - }) - .catch(done); + await waitForCondition(hasIdentifyReturned); + const mpInstance = window.mParticle.getInstance(); + const uploader = mpInstance._APIClient.uploader; + uploader.queueEvent(event0); + expect(!!window.sessionStorage.getItem(eventStorageKey)).to.equal(true); }); - it('should NOT write session storage when noFunctional is true', (done) => { + it('should NOT write session storage when noFunctional is true', async () => { window.mParticle._resetForTests({ ...MPConfig, noFunctional: true }); window.mParticle.config.flags = { offlineStorage: '100', ...enableBatchingConfigFlags, }; window.mParticle.init(apiKey, window.mParticle.config); - waitForCondition(hasIdentifyReturned) - .then(async () => { - const mpInstance = window.mParticle.getInstance(); - const uploader = mpInstance._APIClient.uploader; - uploader.queueEvent(event0); - await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); - expect(window.sessionStorage.getItem(eventStorageKey) === '').to.equal(true); - done(); - }) - .catch(done); + await waitForCondition(hasIdentifyReturned); + const mpInstance = window.mParticle.getInstance(); + const uploader = mpInstance._APIClient.uploader; + uploader.queueEvent(event0); + await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); + expect(window.sessionStorage.getItem(eventStorageKey) === '').to.equal(true); }); - it('should write session storage when noFunctional is false', (done) => { + it('should write session storage when noFunctional is false', async () => { window.mParticle._resetForTests({ ...MPConfig, noFunctional: false }); window.mParticle.config.flags = { offlineStorage: '100', ...enableBatchingConfigFlags, }; window.mParticle.init(apiKey, window.mParticle.config); - waitForCondition(hasIdentifyReturned) - .then(() => { - const mpInstance = window.mParticle.getInstance(); - const uploader = mpInstance._APIClient.uploader; - uploader.queueEvent(event0); - expect(!!window.sessionStorage.getItem(eventStorageKey)).to.equal(true); - done(); - }) - .catch(done); + await waitForCondition(hasIdentifyReturned); + const mpInstance = window.mParticle.getInstance(); + const uploader = mpInstance._APIClient.uploader; + uploader.queueEvent(event0); + expect(!!window.sessionStorage.getItem(eventStorageKey)).to.equal(true); }); - it('should write local storage when noFunctional is default (false)', (done) => { + it('should write local storage when noFunctional is default (false)', async () => { window.mParticle._resetForTests(MPConfig); window.mParticle.init(apiKey, window.mParticle.config); - waitForCondition(hasIdentifyReturned) - .then(async () => { - const mpInstance = window.mParticle.getInstance(); - const uploader = mpInstance._APIClient.uploader; - fetchMock.post(urls.events, 500, { overwriteRoutes: true }); - uploader.queueEvent(event0); - await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); - expect(!!window.localStorage.getItem(batchStorageKey)).to.equal(true); - done(); - }) - .catch(done); + await waitForCondition(hasIdentifyReturned); + const mpInstance = window.mParticle.getInstance(); + const uploader = mpInstance._APIClient.uploader; + fetchMock.post(urls.events, 500, { overwriteRoutes: true }); + uploader.queueEvent(event0); + await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); + expect(!!window.localStorage.getItem(batchStorageKey)).to.equal(true); }); - it('should NOT write local storage when noFunctional is true', (done) => { + it('should NOT write local storage when noFunctional is true', async () => { window.mParticle._resetForTests({ ...MPConfig, noFunctional: true }); window.mParticle.init(apiKey, window.mParticle.config); - waitForCondition(hasIdentifyReturned) - .then(async () => { - const mpInstance = window.mParticle.getInstance(); - const uploader = mpInstance._APIClient.uploader; - uploader.queueEvent(event0); - await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); - expect(window.localStorage.getItem(batchStorageKey) === '').to.equal(true); - done(); - }) - .catch(done); + await waitForCondition(hasIdentifyReturned); + const mpInstance = window.mParticle.getInstance(); + const uploader = mpInstance._APIClient.uploader; + uploader.queueEvent(event0); + await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); + expect(window.localStorage.getItem(batchStorageKey) === '').to.equal(true); }); - it('should write local storage when noFunctional is false', (done) => { + it('should write local storage when noFunctional is false', async () => { window.mParticle._resetForTests({ ...MPConfig, noFunctional: false }); window.mParticle.init(apiKey, window.mParticle.config); - waitForCondition(hasIdentifyReturned) - .then(async () => { - const mpInstance = window.mParticle.getInstance(); - const uploader = mpInstance._APIClient.uploader; - fetchMock.post(urls.events, 500, { overwriteRoutes: true }); - uploader.queueEvent(event0); - await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); - expect(!!window.localStorage.getItem(batchStorageKey)).to.equal(true); - done(); - }) - .catch(done); + await waitForCondition(hasIdentifyReturned); + const mpInstance = window.mParticle.getInstance(); + const uploader = mpInstance._APIClient.uploader; + fetchMock.post(urls.events, 500, { overwriteRoutes: true }); + uploader.queueEvent(event0); + await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); + expect(!!window.localStorage.getItem(batchStorageKey)).to.equal(true); }); }); From 717d848834f00ef8e1d8d63be387a00958f43bc5 Mon Sep 17 00:00:00 2001 From: Jaissica Hora Date: Thu, 4 Sep 2025 13:53:51 -0400 Subject: [PATCH 03/15] refactor(privacy): centralize storage types --- src/batchUploader.ts | 4 +++- src/constants.ts | 14 ++++++++++++++ src/store.ts | 14 +++++++++++++- test/jest/batchUploader.spec.ts | 13 ++++++++++++- 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/batchUploader.ts b/src/batchUploader.ts index 5d13e09bb..4049e1819 100644 --- a/src/batchUploader.ts +++ b/src/batchUploader.ts @@ -72,7 +72,9 @@ export class BatchUploader { // Cache Offline Storage Availability boolean // so that we don't have to check it every time - this.offlineStorageEnabled = this.isOfflineStorageAvailable() && !mpInstance._Store.getNoFunctional(); + this.offlineStorageEnabled = + this.isOfflineStorageAvailable() && + !mpInstance._Store.getPrivacyFlagForStorage('Events'); if (this.offlineStorageEnabled) { this.eventVault = new SessionStorageVault( diff --git a/src/constants.ts b/src/constants.ts index ee8d4b721..babc92e22 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -229,3 +229,17 @@ export const HTTP_UNAUTHORIZED = 401 as const; export const HTTP_FORBIDDEN = 403 as const; export const HTTP_NOT_FOUND = 404 as const; export const HTTP_SERVER_ERROR = 500 as const; + +// Privacy dependency map for storage keys +export type PrivacyControl = 'functional' | 'targeting'; + +export const StorageTypes = ['UserData', 'Products', 'Events', 'Batches', 'IdCache', 'TimeOnSite']; + +export const StoragePrivacyMap: Record = { + 'UserData' : 'functional', + 'Products': 'targeting', + 'Events': 'functional', + 'Batches': 'functional', + 'IdCache': 'functional', + 'TimeOnSite': 'targeting', +}; diff --git a/src/store.ts b/src/store.ts index 637d57d3c..a34f07804 100644 --- a/src/store.ts +++ b/src/store.ts @@ -8,7 +8,7 @@ import { UserIdentities, } from '@mparticle/web-sdk'; import { IKitConfigs } from './configAPIClient'; -import Constants from './constants'; +import Constants, { StoragePrivacyMap, StorageTypes } from './constants'; import { DataPlanResult, KitBlockerOptions, @@ -216,6 +216,7 @@ export interface IStore { setNoFunctional?(noFunctional: boolean): void; getNoTargeting?(): boolean; setNoTargeting?(noTargeting: boolean): void; + getPrivacyFlagForStorage?(storageType: typeof StorageTypes[number]): boolean; getDeviceId?(): string; setDeviceId?(deviceId: string): void; @@ -608,6 +609,17 @@ export default function Store( this.noTargeting = noTargeting; }; + this.getPrivacyFlagForStorage = (storageType: typeof StorageTypes[number]): boolean => { + const privacyControl = StoragePrivacyMap[storageType]; + if (privacyControl === 'functional') { + return !!this.getNoFunctional(); + } + if (privacyControl === 'targeting') { + return !!this.getNoTargeting(); + } + return false; + }; + this.getDeviceId = () => this.deviceId; this.setDeviceId = (deviceId: string) => { this.deviceId = deviceId; diff --git a/test/jest/batchUploader.spec.ts b/test/jest/batchUploader.spec.ts index dced62e5a..4ed040e85 100644 --- a/test/jest/batchUploader.spec.ts +++ b/test/jest/batchUploader.spec.ts @@ -1,5 +1,6 @@ import { BatchUploader } from '../../src/batchUploader'; import { IMParticleWebSDKInstance } from '../../src/mp-instance'; +import { StoragePrivacyMap, StorageTypes } from '../../src/constants'; describe('BatchUploader', () => { let batchUploader: BatchUploader; @@ -17,7 +18,17 @@ describe('BatchUploader', () => { _Store: { storageName: 'mprtcl-v4_abcdef', noFunctional: false, - getNoFunctional: function(this: any) { return this.noFunctional; }, + noTargeting: false, + getPrivacyFlagForStorage: function(this: any, storageType: typeof StorageTypes[number]) { + const privacyControl = StoragePrivacyMap[storageType]; + if (privacyControl === 'functional') { + return !!this.noFunctional; + } + if (privacyControl === 'targeting') { + return !!this.noTargeting; + } + return false; + }, deviceId: 'device-1', SDKConfig: { flags: {} From 4067a0fe9f144987d4bc144cb47d6eb6201f0ef5 Mon Sep 17 00:00:00 2001 From: Jaissica Hora Date: Thu, 4 Sep 2025 14:01:57 -0400 Subject: [PATCH 04/15] test: updated getter and setter for privacy flags --- test/jest/batchUploader.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/jest/batchUploader.spec.ts b/test/jest/batchUploader.spec.ts index 4ed040e85..7a549e455 100644 --- a/test/jest/batchUploader.spec.ts +++ b/test/jest/batchUploader.spec.ts @@ -17,15 +17,15 @@ describe('BatchUploader', () => { mockMPInstance = { _Store: { storageName: 'mprtcl-v4_abcdef', - noFunctional: false, - noTargeting: false, + getNoFunctional: function(this: any) { return this.noFunctional; }, + getNoTargeting: function(this: any) { return this.noTargeting; }, getPrivacyFlagForStorage: function(this: any, storageType: typeof StorageTypes[number]) { const privacyControl = StoragePrivacyMap[storageType]; if (privacyControl === 'functional') { - return !!this.noFunctional; + return !!this.getNoFunctional(); } if (privacyControl === 'targeting') { - return !!this.noTargeting; + return !!this.getNoTargeting(); } return false; }, From 5d3172461d4fc382bdc1d7bbfe4d86af0bc872c5 Mon Sep 17 00:00:00 2001 From: Jaissica Hora Date: Fri, 5 Sep 2025 12:01:44 -0400 Subject: [PATCH 05/15] chore: removed extra comment and updated storage privacy map --- src/constants.ts | 12 ++++++------ test/jest/batchUploader.spec.ts | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index babc92e22..9e918effc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -236,10 +236,10 @@ export type PrivacyControl = 'functional' | 'targeting'; export const StorageTypes = ['UserData', 'Products', 'Events', 'Batches', 'IdCache', 'TimeOnSite']; export const StoragePrivacyMap: Record = { - 'UserData' : 'functional', - 'Products': 'targeting', - 'Events': 'functional', - 'Batches': 'functional', - 'IdCache': 'functional', - 'TimeOnSite': 'targeting', + UserData : 'functional', + Products: 'targeting', + Events: 'functional', + Batches: 'functional', + IdCache: 'functional', + TimeOnSite: 'targeting', }; diff --git a/test/jest/batchUploader.spec.ts b/test/jest/batchUploader.spec.ts index 7a549e455..ec3e62e23 100644 --- a/test/jest/batchUploader.spec.ts +++ b/test/jest/batchUploader.spec.ts @@ -143,7 +143,6 @@ describe('BatchUploader', () => { }); it('should enable offline storage when noFunctional is default (false)', () => { - // mockMPInstance._Store.noFunctional = false; const uploader = new BatchUploader(mockMPInstance, 1000); expect(uploader['offlineStorageEnabled']).toBe(true); From e706b5b0966cf7021e6b3985fe0874a5da477d68 Mon Sep 17 00:00:00 2001 From: Jaissica Hora Date: Fri, 5 Sep 2025 12:04:45 -0400 Subject: [PATCH 06/15] chore: remove redundant boolean coercion --- src/store.ts | 4 ++-- test/jest/batchUploader.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/store.ts b/src/store.ts index a34f07804..7738190b1 100644 --- a/src/store.ts +++ b/src/store.ts @@ -612,10 +612,10 @@ export default function Store( this.getPrivacyFlagForStorage = (storageType: typeof StorageTypes[number]): boolean => { const privacyControl = StoragePrivacyMap[storageType]; if (privacyControl === 'functional') { - return !!this.getNoFunctional(); + return this.getNoFunctional(); } if (privacyControl === 'targeting') { - return !!this.getNoTargeting(); + return this.getNoTargeting(); } return false; }; diff --git a/test/jest/batchUploader.spec.ts b/test/jest/batchUploader.spec.ts index ec3e62e23..d4e825ddb 100644 --- a/test/jest/batchUploader.spec.ts +++ b/test/jest/batchUploader.spec.ts @@ -22,10 +22,10 @@ describe('BatchUploader', () => { getPrivacyFlagForStorage: function(this: any, storageType: typeof StorageTypes[number]) { const privacyControl = StoragePrivacyMap[storageType]; if (privacyControl === 'functional') { - return !!this.getNoFunctional(); + return this.getNoFunctional(); } if (privacyControl === 'targeting') { - return !!this.getNoTargeting(); + return this.getNoTargeting(); } return false; }, From 47b711371139e77f09a45d16aff2d39a27bfe2f1 Mon Sep 17 00:00:00 2001 From: Jaissica Hora Date: Tue, 9 Sep 2025 18:58:33 -0400 Subject: [PATCH 07/15] test: updated tests for batchUploader --- src/batchUploader.ts | 14 +++++----- src/constants.ts | 4 +-- test/jest/batchUploader.spec.ts | 32 ++++++++++++++++------- test/src/tests-batchUploader.ts | 45 ++++++++++++++++++++++++--------- 4 files changed, 65 insertions(+), 30 deletions(-) diff --git a/src/batchUploader.ts b/src/batchUploader.ts index 4049e1819..13d41ac90 100644 --- a/src/batchUploader.ts +++ b/src/batchUploader.ts @@ -43,7 +43,7 @@ export class BatchUploader { batchingEnabled: boolean; private eventVault: SessionStorageVault; private batchVault: LocalStorageVault; - private offlineStorageEnabled: boolean = false; + private OfflineStorage: boolean = false; private uploader: AsyncUploader; private lastASTEventTime: number = 0; private readonly AST_DEBOUNCE_MS: number = 1000; // 1 second debounce @@ -72,11 +72,11 @@ export class BatchUploader { // Cache Offline Storage Availability boolean // so that we don't have to check it every time - this.offlineStorageEnabled = + this.OfflineStorage = this.isOfflineStorageAvailable() && !mpInstance._Store.getPrivacyFlagForStorage('Events'); - if (this.offlineStorageEnabled) { + if (this.OfflineStorage) { this.eventVault = new SessionStorageVault( `${mpInstance._Store.storageName}-events`, { @@ -262,7 +262,7 @@ export class BatchUploader { const { verbose } = this.mpInstance.Logger; this.eventsQueuedForProcessing.push(event); - if (this.offlineStorageEnabled && this.eventVault) { + if (this.OfflineStorage && this.eventVault) { this.eventVault.store(this.eventsQueuedForProcessing); } @@ -378,7 +378,7 @@ export class BatchUploader { const currentEvents: SDKEvent[] = this.eventsQueuedForProcessing; this.eventsQueuedForProcessing = []; - if (this.offlineStorageEnabled && this.eventVault) { + if (this.OfflineStorage && this.eventVault) { this.eventVault.store([]); } @@ -392,7 +392,7 @@ export class BatchUploader { } // Top Load any older Batches from Offline Storage so they go out first - if (this.offlineStorageEnabled && this.batchVault) { + if (this.OfflineStorage && this.batchVault) { this.batchesQueuedForProcessing.unshift( ...this.batchVault.retrieve() ); @@ -425,7 +425,7 @@ export class BatchUploader { } // Update Offline Storage with current state of batch queue - if (!useBeacon && this.offlineStorageEnabled && this.batchVault) { + if (!useBeacon && this.OfflineStorage && this.batchVault) { // Note: since beacon is "Fire and forget" it will empty `batchesThatDidNotUplod` // regardless of whether the batches were successfully uploaded or not. We should // therefore NOT overwrite Offline Storage when beacon returns, so that we can retry diff --git a/src/constants.ts b/src/constants.ts index 9e918effc..2c4aee389 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -233,10 +233,10 @@ export const HTTP_SERVER_ERROR = 500 as const; // Privacy dependency map for storage keys export type PrivacyControl = 'functional' | 'targeting'; -export const StorageTypes = ['UserData', 'Products', 'Events', 'Batches', 'IdCache', 'TimeOnSite']; +export const StorageTypes = ['SDKState', 'Products', 'Events', 'Batches', 'IdCache', 'TimeOnSite']; export const StoragePrivacyMap: Record = { - UserData : 'functional', + SDKState : 'functional', Products: 'targeting', Events: 'functional', Batches: 'functional', diff --git a/test/jest/batchUploader.spec.ts b/test/jest/batchUploader.spec.ts index d4e825ddb..cca6535e9 100644 --- a/test/jest/batchUploader.spec.ts +++ b/test/jest/batchUploader.spec.ts @@ -12,7 +12,8 @@ describe('BatchUploader', () => { now: now, advanceTimers: true // This improves the performance of nested timers, equivalent to Sinon's shouldAdvanceTime }); - + global.fetch = jest.fn().mockResolvedValue({ ok: true, status: 200 }); + // Create a mock mParticle instance with mocked methods for instantiating a BatchUploader mockMPInstance = { _Store: { @@ -36,11 +37,13 @@ describe('BatchUploader', () => { }, _Helpers: { getFeatureFlag: jest.fn().mockReturnValue('100'), - createServiceUrl: jest.fn().mockReturnValue('https://mock-url.com') + createServiceUrl: jest.fn().mockReturnValue('https://mock-url.com'), + generateUniqueId: jest.fn().mockReturnValue('req-1'), }, Identity: { getCurrentUser: jest.fn().mockReturnValue({ - getMPID: () => 'test-mpid' + getMPID: () => 'test-mpid', + getConsentState: jest.fn().mockReturnValue(null), }) }, Logger: { @@ -55,6 +58,7 @@ describe('BatchUploader', () => { afterEach(() => { jest.useRealTimers(); + delete global.fetch; }); describe('shouldDebounceAST', () => { @@ -135,29 +139,39 @@ describe('BatchUploader', () => { mockMPInstance._Store.noFunctional = true; const uploader = new BatchUploader(mockMPInstance, 1000); - expect(uploader['offlineStorageEnabled']).toBe(false); + expect(uploader['OfflineStorage']).toBe(false); uploader.queueEvent({ EventDataType: 4 } as any); expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).toBeNull(); expect(localStorage.getItem('mprtcl-v4_abcdef-batches')).toBeNull(); }); - it('should enable offline storage when noFunctional is default (false)', () => { + it('should enable offline storage when noFunctional is default (false)', async () => { const uploader = new BatchUploader(mockMPInstance, 1000); - expect(uploader['offlineStorageEnabled']).toBe(true); + expect(uploader['OfflineStorage']).toBe(true); - uploader.queueEvent({ EventDataType: 4 } as any); + uploader.queueEvent({ EventDataType: 4, SessionId: 's1' } as any); expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).not.toBeNull(); + + jest.advanceTimersByTime(1000); + await Promise.resolve(); + + expect(localStorage.getItem('mprtcl-v4_abcdef-batches')).not.toBeNull(); }); - it('should enable offline storage when noFunctional is false', () => { + it('should enable offline storage when noFunctional is false', async () => { mockMPInstance._Store.noFunctional = false; const uploader = new BatchUploader(mockMPInstance, 1000); - uploader.queueEvent({ EventDataType: 4 } as any); + uploader.queueEvent({ EventDataType: 4, SessionId: 's1' } as any); expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).not.toBeNull(); + + jest.advanceTimersByTime(1000); + await Promise.resolve(); + + expect(localStorage.getItem('mprtcl-v4_abcdef-batches')).not.toBeNull(); }); }); }); \ No newline at end of file diff --git a/test/src/tests-batchUploader.ts b/test/src/tests-batchUploader.ts index d3db04d3c..bc35ac5a7 100644 --- a/test/src/tests-batchUploader.ts +++ b/test/src/tests-batchUploader.ts @@ -1720,9 +1720,11 @@ describe('batch uploader', () => { describe('noFunctional', () => { const eventStorageKey = 'mprtcl-v4_abcdef-events'; const batchStorageKey = 'mprtcl-v4_abcdef-batches'; - - it('should write session storage when noFunctional is default (false)', async () => { + beforeEach(() => { window.mParticle._resetForTests(MPConfig); + }); + + it('should store events in session storage when noFunctional is default (false)', async () => { window.mParticle.config.flags = { offlineStorage: '100', ...enableBatchingConfigFlags, @@ -1735,12 +1737,15 @@ describe('batch uploader', () => { expect(!!window.sessionStorage.getItem(eventStorageKey)).to.equal(true); }); - it('should NOT write session storage when noFunctional is true', async () => { - window.mParticle._resetForTests({ ...MPConfig, noFunctional: true }); + it('should NOT store events in session storage when noFunctional is true', async () => { window.mParticle.config.flags = { offlineStorage: '100', ...enableBatchingConfigFlags, }; + window.mParticle.config.launcherOptions = { + ...(window.mParticle.config.launcherOptions || {}), + noFunctional: true, + }; window.mParticle.init(apiKey, window.mParticle.config); await waitForCondition(hasIdentifyReturned); const mpInstance = window.mParticle.getInstance(); @@ -1750,12 +1755,15 @@ describe('batch uploader', () => { expect(window.sessionStorage.getItem(eventStorageKey) === '').to.equal(true); }); - it('should write session storage when noFunctional is false', async () => { - window.mParticle._resetForTests({ ...MPConfig, noFunctional: false }); + it('should store events in session storage when noFunctional is false', async () => { window.mParticle.config.flags = { offlineStorage: '100', ...enableBatchingConfigFlags, }; + window.mParticle.config.launcherOptions = { + ...(window.mParticle.config.launcherOptions || {}), + noFunctional: false, + }; window.mParticle.init(apiKey, window.mParticle.config); await waitForCondition(hasIdentifyReturned); const mpInstance = window.mParticle.getInstance(); @@ -1764,8 +1772,7 @@ describe('batch uploader', () => { expect(!!window.sessionStorage.getItem(eventStorageKey)).to.equal(true); }); - it('should write local storage when noFunctional is default (false)', async () => { - window.mParticle._resetForTests(MPConfig); + it('should store batches in local storage when noFunctional is default (false)', async () => { window.mParticle.init(apiKey, window.mParticle.config); await waitForCondition(hasIdentifyReturned); const mpInstance = window.mParticle.getInstance(); @@ -1776,8 +1783,15 @@ describe('batch uploader', () => { expect(!!window.localStorage.getItem(batchStorageKey)).to.equal(true); }); - it('should NOT write local storage when noFunctional is true', async () => { - window.mParticle._resetForTests({ ...MPConfig, noFunctional: true }); + it('should NOT store batches in local storage when noFunctional is true', async () => { + window.mParticle.config.flags = { + offlineStorage: '100', + ...enableBatchingConfigFlags, + }; + window.mParticle.config.launcherOptions = { + ...(window.mParticle.config.launcherOptions || {}), + noFunctional: true, + }; window.mParticle.init(apiKey, window.mParticle.config); await waitForCondition(hasIdentifyReturned); const mpInstance = window.mParticle.getInstance(); @@ -1787,8 +1801,15 @@ describe('batch uploader', () => { expect(window.localStorage.getItem(batchStorageKey) === '').to.equal(true); }); - it('should write local storage when noFunctional is false', async () => { - window.mParticle._resetForTests({ ...MPConfig, noFunctional: false }); + it('should store batches in local storage when noFunctional is false', async () => { + window.mParticle.config.flags = { + offlineStorage: '100', + ...enableBatchingConfigFlags, + }; + window.mParticle.config.launcherOptions = { + ...(window.mParticle.config.launcherOptions || {}), + noFunctional: false, + }; window.mParticle.init(apiKey, window.mParticle.config); await waitForCondition(hasIdentifyReturned); const mpInstance = window.mParticle.getInstance(); From aaacb9ccb9dfa501ed7b12bbca62aeec5719df01 Mon Sep 17 00:00:00 2001 From: Jaissica Hora Date: Wed, 10 Sep 2025 09:38:02 -0400 Subject: [PATCH 08/15] test: updated tests for batchUploader --- test/jest/batchUploader.spec.ts | 8 +++++--- test/src/tests-batchUploader.ts | 4 ---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/test/jest/batchUploader.spec.ts b/test/jest/batchUploader.spec.ts index cca6535e9..f437f9162 100644 --- a/test/jest/batchUploader.spec.ts +++ b/test/jest/batchUploader.spec.ts @@ -5,6 +5,7 @@ import { StoragePrivacyMap, StorageTypes } from '../../src/constants'; describe('BatchUploader', () => { let batchUploader: BatchUploader; let mockMPInstance: IMParticleWebSDKInstance; + let originalFetch: typeof global.fetch; beforeEach(() => { const now = Date.now(); @@ -12,6 +13,7 @@ describe('BatchUploader', () => { now: now, advanceTimers: true // This improves the performance of nested timers, equivalent to Sinon's shouldAdvanceTime }); + originalFetch = global.fetch; global.fetch = jest.fn().mockResolvedValue({ ok: true, status: 200 }); // Create a mock mParticle instance with mocked methods for instantiating a BatchUploader @@ -58,7 +60,7 @@ describe('BatchUploader', () => { afterEach(() => { jest.useRealTimers(); - delete global.fetch; + global.fetch = originalFetch; }); describe('shouldDebounceAST', () => { @@ -151,7 +153,7 @@ describe('BatchUploader', () => { expect(uploader['OfflineStorage']).toBe(true); - uploader.queueEvent({ EventDataType: 4, SessionId: 's1' } as any); + uploader.queueEvent({ EventDataType: 4 } as any); expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).not.toBeNull(); jest.advanceTimersByTime(1000); @@ -165,7 +167,7 @@ describe('BatchUploader', () => { const uploader = new BatchUploader(mockMPInstance, 1000); - uploader.queueEvent({ EventDataType: 4, SessionId: 's1' } as any); + uploader.queueEvent({ EventDataType: 4 } as any); expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).not.toBeNull(); jest.advanceTimersByTime(1000); diff --git a/test/src/tests-batchUploader.ts b/test/src/tests-batchUploader.ts index bc35ac5a7..c9bd75e1a 100644 --- a/test/src/tests-batchUploader.ts +++ b/test/src/tests-batchUploader.ts @@ -1743,7 +1743,6 @@ describe('batch uploader', () => { ...enableBatchingConfigFlags, }; window.mParticle.config.launcherOptions = { - ...(window.mParticle.config.launcherOptions || {}), noFunctional: true, }; window.mParticle.init(apiKey, window.mParticle.config); @@ -1761,7 +1760,6 @@ describe('batch uploader', () => { ...enableBatchingConfigFlags, }; window.mParticle.config.launcherOptions = { - ...(window.mParticle.config.launcherOptions || {}), noFunctional: false, }; window.mParticle.init(apiKey, window.mParticle.config); @@ -1789,7 +1787,6 @@ describe('batch uploader', () => { ...enableBatchingConfigFlags, }; window.mParticle.config.launcherOptions = { - ...(window.mParticle.config.launcherOptions || {}), noFunctional: true, }; window.mParticle.init(apiKey, window.mParticle.config); @@ -1807,7 +1804,6 @@ describe('batch uploader', () => { ...enableBatchingConfigFlags, }; window.mParticle.config.launcherOptions = { - ...(window.mParticle.config.launcherOptions || {}), noFunctional: false, }; window.mParticle.init(apiKey, window.mParticle.config); From 0b6993091fb22b9acfb2b95a20d7837937ba6dfa Mon Sep 17 00:00:00 2001 From: Jaissica Hora Date: Thu, 11 Sep 2025 20:21:37 -0400 Subject: [PATCH 09/15] refactor: renamed getPrivacyFlagForStorage to getPrivacyFlag --- src/batchUploader.ts | 2 +- src/store.ts | 4 ++-- test/jest/batchUploader.spec.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/batchUploader.ts b/src/batchUploader.ts index 13d41ac90..9c63a0858 100644 --- a/src/batchUploader.ts +++ b/src/batchUploader.ts @@ -74,7 +74,7 @@ export class BatchUploader { // so that we don't have to check it every time this.OfflineStorage = this.isOfflineStorageAvailable() && - !mpInstance._Store.getPrivacyFlagForStorage('Events'); + !mpInstance._Store.getPrivacyFlag('Events'); if (this.OfflineStorage) { this.eventVault = new SessionStorageVault( diff --git a/src/store.ts b/src/store.ts index 7738190b1..dfb089931 100644 --- a/src/store.ts +++ b/src/store.ts @@ -216,7 +216,7 @@ export interface IStore { setNoFunctional?(noFunctional: boolean): void; getNoTargeting?(): boolean; setNoTargeting?(noTargeting: boolean): void; - getPrivacyFlagForStorage?(storageType: typeof StorageTypes[number]): boolean; + getPrivacyFlag?(storageType: typeof StorageTypes[number]): boolean; getDeviceId?(): string; setDeviceId?(deviceId: string): void; @@ -609,7 +609,7 @@ export default function Store( this.noTargeting = noTargeting; }; - this.getPrivacyFlagForStorage = (storageType: typeof StorageTypes[number]): boolean => { + this.getPrivacyFlag = (storageType: typeof StorageTypes[number]): boolean => { const privacyControl = StoragePrivacyMap[storageType]; if (privacyControl === 'functional') { return this.getNoFunctional(); diff --git a/test/jest/batchUploader.spec.ts b/test/jest/batchUploader.spec.ts index f437f9162..246e630df 100644 --- a/test/jest/batchUploader.spec.ts +++ b/test/jest/batchUploader.spec.ts @@ -22,7 +22,7 @@ describe('BatchUploader', () => { storageName: 'mprtcl-v4_abcdef', getNoFunctional: function(this: any) { return this.noFunctional; }, getNoTargeting: function(this: any) { return this.noTargeting; }, - getPrivacyFlagForStorage: function(this: any, storageType: typeof StorageTypes[number]) { + getPrivacyFlag: function(this: any, storageType: typeof StorageTypes[number]) { const privacyControl = StoragePrivacyMap[storageType]; if (privacyControl === 'functional') { return this.getNoFunctional(); From 57823b6c66fc46c771b5561b21e60d45d8b84471 Mon Sep 17 00:00:00 2001 From: Jaissica Date: Mon, 15 Sep 2025 12:58:46 -0400 Subject: [PATCH 10/15] chore: simplify StorageTypes for better type safety --- src/constants.ts | 6 +++--- src/store.ts | 8 ++++---- test/jest/batchUploader.spec.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 2c4aee389..1f3849542 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -233,10 +233,10 @@ export const HTTP_SERVER_ERROR = 500 as const; // Privacy dependency map for storage keys export type PrivacyControl = 'functional' | 'targeting'; -export const StorageTypes = ['SDKState', 'Products', 'Events', 'Batches', 'IdCache', 'TimeOnSite']; +export type StorageTypes = 'SDKState' | 'Products' | 'Events' | 'Batches' | 'IdCache' | 'TimeOnSite'; -export const StoragePrivacyMap: Record = { - SDKState : 'functional', +export const StoragePrivacyMap: Record = { + SDKState: 'functional', Products: 'targeting', Events: 'functional', Batches: 'functional', diff --git a/src/store.ts b/src/store.ts index dfb089931..35a02787c 100644 --- a/src/store.ts +++ b/src/store.ts @@ -8,7 +8,7 @@ import { UserIdentities, } from '@mparticle/web-sdk'; import { IKitConfigs } from './configAPIClient'; -import Constants, { StoragePrivacyMap, StorageTypes } from './constants'; +import Constants, { PrivacyControl, StoragePrivacyMap, StorageTypes } from './constants'; import { DataPlanResult, KitBlockerOptions, @@ -216,7 +216,7 @@ export interface IStore { setNoFunctional?(noFunctional: boolean): void; getNoTargeting?(): boolean; setNoTargeting?(noTargeting: boolean): void; - getPrivacyFlag?(storageType: typeof StorageTypes[number]): boolean; + getPrivacyFlag?(storageType: StorageTypes): boolean; getDeviceId?(): string; setDeviceId?(deviceId: string): void; @@ -609,8 +609,8 @@ export default function Store( this.noTargeting = noTargeting; }; - this.getPrivacyFlag = (storageType: typeof StorageTypes[number]): boolean => { - const privacyControl = StoragePrivacyMap[storageType]; + this.getPrivacyFlag = (storageType: StorageTypes): boolean => { + const privacyControl: PrivacyControl = StoragePrivacyMap[storageType]; if (privacyControl === 'functional') { return this.getNoFunctional(); } diff --git a/test/jest/batchUploader.spec.ts b/test/jest/batchUploader.spec.ts index 246e630df..6d00e08f2 100644 --- a/test/jest/batchUploader.spec.ts +++ b/test/jest/batchUploader.spec.ts @@ -22,7 +22,7 @@ describe('BatchUploader', () => { storageName: 'mprtcl-v4_abcdef', getNoFunctional: function(this: any) { return this.noFunctional; }, getNoTargeting: function(this: any) { return this.noTargeting; }, - getPrivacyFlag: function(this: any, storageType: typeof StorageTypes[number]) { + getPrivacyFlag: function(this: any, storageType: StorageTypes) { const privacyControl = StoragePrivacyMap[storageType]; if (privacyControl === 'functional') { return this.getNoFunctional(); From e7ad62ee8a3f22a2b44939fc909e92aefc61a5c9 Mon Sep 17 00:00:00 2001 From: Jaissica Date: Mon, 15 Sep 2025 21:02:14 -0400 Subject: [PATCH 11/15] chore: revert to offlineStorageEnabled and update StorageTypes --- src/batchUploader.ts | 16 ++++++++-------- src/constants.ts | 7 +++---- test/jest/batchUploader.spec.ts | 4 ++-- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/batchUploader.ts b/src/batchUploader.ts index 9c63a0858..9f456dad0 100644 --- a/src/batchUploader.ts +++ b/src/batchUploader.ts @@ -43,7 +43,7 @@ export class BatchUploader { batchingEnabled: boolean; private eventVault: SessionStorageVault; private batchVault: LocalStorageVault; - private OfflineStorage: boolean = false; + private offlineStorageEnabled: boolean = false; private uploader: AsyncUploader; private lastASTEventTime: number = 0; private readonly AST_DEBOUNCE_MS: number = 1000; // 1 second debounce @@ -72,11 +72,11 @@ export class BatchUploader { // Cache Offline Storage Availability boolean // so that we don't have to check it every time - this.OfflineStorage = + this.offlineStorageEnabled = this.isOfflineStorageAvailable() && - !mpInstance._Store.getPrivacyFlag('Events'); + !mpInstance._Store.getPrivacyFlag('OfflineEvents'); - if (this.OfflineStorage) { + if (this.offlineStorageEnabled) { this.eventVault = new SessionStorageVault( `${mpInstance._Store.storageName}-events`, { @@ -262,7 +262,7 @@ export class BatchUploader { const { verbose } = this.mpInstance.Logger; this.eventsQueuedForProcessing.push(event); - if (this.OfflineStorage && this.eventVault) { + if (this.offlineStorageEnabled && this.eventVault) { this.eventVault.store(this.eventsQueuedForProcessing); } @@ -378,7 +378,7 @@ export class BatchUploader { const currentEvents: SDKEvent[] = this.eventsQueuedForProcessing; this.eventsQueuedForProcessing = []; - if (this.OfflineStorage && this.eventVault) { + if (this.offlineStorageEnabled && this.eventVault) { this.eventVault.store([]); } @@ -392,7 +392,7 @@ export class BatchUploader { } // Top Load any older Batches from Offline Storage so they go out first - if (this.OfflineStorage && this.batchVault) { + if (this.offlineStorageEnabled && this.batchVault) { this.batchesQueuedForProcessing.unshift( ...this.batchVault.retrieve() ); @@ -425,7 +425,7 @@ export class BatchUploader { } // Update Offline Storage with current state of batch queue - if (!useBeacon && this.OfflineStorage && this.batchVault) { + if (!useBeacon && this.offlineStorageEnabled && this.batchVault) { // Note: since beacon is "Fire and forget" it will empty `batchesThatDidNotUplod` // regardless of whether the batches were successfully uploaded or not. We should // therefore NOT overwrite Offline Storage when beacon returns, so that we can retry diff --git a/src/constants.ts b/src/constants.ts index 1f3849542..f1f976998 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -233,13 +233,12 @@ export const HTTP_SERVER_ERROR = 500 as const; // Privacy dependency map for storage keys export type PrivacyControl = 'functional' | 'targeting'; -export type StorageTypes = 'SDKState' | 'Products' | 'Events' | 'Batches' | 'IdCache' | 'TimeOnSite'; +export type StorageTypes = 'SDKState' | 'Products' | 'OfflineEvents' | 'IdentityCache' | 'TimeOnSite'; export const StoragePrivacyMap: Record = { SDKState: 'functional', Products: 'targeting', - Events: 'functional', - Batches: 'functional', - IdCache: 'functional', + OfflineEvents: 'functional', + IdentityCache: 'functional', TimeOnSite: 'targeting', }; diff --git a/test/jest/batchUploader.spec.ts b/test/jest/batchUploader.spec.ts index 6d00e08f2..ed0d33fa3 100644 --- a/test/jest/batchUploader.spec.ts +++ b/test/jest/batchUploader.spec.ts @@ -141,7 +141,7 @@ describe('BatchUploader', () => { mockMPInstance._Store.noFunctional = true; const uploader = new BatchUploader(mockMPInstance, 1000); - expect(uploader['OfflineStorage']).toBe(false); + expect(uploader['offlineStorageEnabled']).toBe(false); uploader.queueEvent({ EventDataType: 4 } as any); expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).toBeNull(); @@ -151,7 +151,7 @@ describe('BatchUploader', () => { it('should enable offline storage when noFunctional is default (false)', async () => { const uploader = new BatchUploader(mockMPInstance, 1000); - expect(uploader['OfflineStorage']).toBe(true); + expect(uploader['offlineStorageEnabled']).toBe(true); uploader.queueEvent({ EventDataType: 4 } as any); expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).not.toBeNull(); From 3b0b0eb6ee645e722a359a21ef72907dc3eb1a7b Mon Sep 17 00:00:00 2001 From: Jaissica Date: Tue, 16 Sep 2025 14:12:56 -0400 Subject: [PATCH 12/15] chore: updated test content and removed comment --- src/constants.ts | 1 - test/src/tests-batchUploader.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index f1f976998..456b10539 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -230,7 +230,6 @@ export const HTTP_FORBIDDEN = 403 as const; export const HTTP_NOT_FOUND = 404 as const; export const HTTP_SERVER_ERROR = 500 as const; -// Privacy dependency map for storage keys export type PrivacyControl = 'functional' | 'targeting'; export type StorageTypes = 'SDKState' | 'Products' | 'OfflineEvents' | 'IdentityCache' | 'TimeOnSite'; diff --git a/test/src/tests-batchUploader.ts b/test/src/tests-batchUploader.ts index c9bd75e1a..f83374289 100644 --- a/test/src/tests-batchUploader.ts +++ b/test/src/tests-batchUploader.ts @@ -1770,7 +1770,7 @@ describe('batch uploader', () => { expect(!!window.sessionStorage.getItem(eventStorageKey)).to.equal(true); }); - it('should store batches in local storage when noFunctional is default (false)', async () => { + it('should store batches in local storage when noFunctional is default false by default', async () => { window.mParticle.init(apiKey, window.mParticle.config); await waitForCondition(hasIdentifyReturned); const mpInstance = window.mParticle.getInstance(); From db8c08eba05bcd0e6c7c6558fb1e9e9105ca1cda Mon Sep 17 00:00:00 2001 From: Jaissica Date: Tue, 16 Sep 2025 17:04:14 -0400 Subject: [PATCH 13/15] chore: updated jest and integration test message --- test/jest/batchUploader.spec.ts | 2 +- test/src/tests-batchUploader.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/jest/batchUploader.spec.ts b/test/jest/batchUploader.spec.ts index ed0d33fa3..67885aba3 100644 --- a/test/jest/batchUploader.spec.ts +++ b/test/jest/batchUploader.spec.ts @@ -148,7 +148,7 @@ describe('BatchUploader', () => { expect(localStorage.getItem('mprtcl-v4_abcdef-batches')).toBeNull(); }); - it('should enable offline storage when noFunctional is default (false)', async () => { + it('should enable offline storage when noFunctional is false by default', async () => { const uploader = new BatchUploader(mockMPInstance, 1000); expect(uploader['offlineStorageEnabled']).toBe(true); diff --git a/test/src/tests-batchUploader.ts b/test/src/tests-batchUploader.ts index f83374289..60f5bbab3 100644 --- a/test/src/tests-batchUploader.ts +++ b/test/src/tests-batchUploader.ts @@ -1724,7 +1724,7 @@ describe('batch uploader', () => { window.mParticle._resetForTests(MPConfig); }); - it('should store events in session storage when noFunctional is default (false)', async () => { + it('should store batches in local storage when noFunctional is false by default', async () => { window.mParticle.config.flags = { offlineStorage: '100', ...enableBatchingConfigFlags, @@ -1770,7 +1770,7 @@ describe('batch uploader', () => { expect(!!window.sessionStorage.getItem(eventStorageKey)).to.equal(true); }); - it('should store batches in local storage when noFunctional is default false by default', async () => { + it('should store batches in local storage when noFunctional is false by default', async () => { window.mParticle.init(apiKey, window.mParticle.config); await waitForCondition(hasIdentifyReturned); const mpInstance = window.mParticle.getInstance(); From 426f03867428e11b59b14f8889fd27b4eec53227 Mon Sep 17 00:00:00 2001 From: Jaissica Date: Thu, 18 Sep 2025 12:44:29 -0400 Subject: [PATCH 14/15] test: updated tests for batch uploader --- test/jest/batchUploader.spec.ts | 14 ++++++++------ test/src/tests-batchUploader.ts | 6 ------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/test/jest/batchUploader.spec.ts b/test/jest/batchUploader.spec.ts index 67885aba3..35f35c3f0 100644 --- a/test/jest/batchUploader.spec.ts +++ b/test/jest/batchUploader.spec.ts @@ -1,6 +1,8 @@ import { BatchUploader } from '../../src/batchUploader'; import { IMParticleWebSDKInstance } from '../../src/mp-instance'; +import { IStore } from '../../src/store'; import { StoragePrivacyMap, StorageTypes } from '../../src/constants'; +import { SDKEvent } from '../../src/sdkRuntimeModels'; describe('BatchUploader', () => { let batchUploader: BatchUploader; @@ -20,9 +22,9 @@ describe('BatchUploader', () => { mockMPInstance = { _Store: { storageName: 'mprtcl-v4_abcdef', - getNoFunctional: function(this: any) { return this.noFunctional; }, - getNoTargeting: function(this: any) { return this.noTargeting; }, - getPrivacyFlag: function(this: any, storageType: StorageTypes) { + getNoFunctional: function(this: IStore) { return this.noFunctional; }, + getNoTargeting: function(this: IStore) { return this.noTargeting; }, + getPrivacyFlag: function(this: IStore, storageType: StorageTypes) { const privacyControl = StoragePrivacyMap[storageType]; if (privacyControl === 'functional') { return this.getNoFunctional(); @@ -143,7 +145,7 @@ describe('BatchUploader', () => { const uploader = new BatchUploader(mockMPInstance, 1000); expect(uploader['offlineStorageEnabled']).toBe(false); - uploader.queueEvent({ EventDataType: 4 } as any); + uploader.queueEvent({ EventDataType: 4 } as SDKEvent); expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).toBeNull(); expect(localStorage.getItem('mprtcl-v4_abcdef-batches')).toBeNull(); }); @@ -153,7 +155,7 @@ describe('BatchUploader', () => { expect(uploader['offlineStorageEnabled']).toBe(true); - uploader.queueEvent({ EventDataType: 4 } as any); + uploader.queueEvent({ EventDataType: 4 } as SDKEvent); expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).not.toBeNull(); jest.advanceTimersByTime(1000); @@ -167,7 +169,7 @@ describe('BatchUploader', () => { const uploader = new BatchUploader(mockMPInstance, 1000); - uploader.queueEvent({ EventDataType: 4 } as any); + uploader.queueEvent({ EventDataType: 4 } as SDKEvent); expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).not.toBeNull(); jest.advanceTimersByTime(1000); diff --git a/test/src/tests-batchUploader.ts b/test/src/tests-batchUploader.ts index 60f5bbab3..0290da757 100644 --- a/test/src/tests-batchUploader.ts +++ b/test/src/tests-batchUploader.ts @@ -1750,7 +1750,6 @@ describe('batch uploader', () => { const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; uploader.queueEvent(event0); - await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); expect(window.sessionStorage.getItem(eventStorageKey) === '').to.equal(true); }); @@ -1775,9 +1774,7 @@ describe('batch uploader', () => { await waitForCondition(hasIdentifyReturned); const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; - fetchMock.post(urls.events, 500, { overwriteRoutes: true }); uploader.queueEvent(event0); - await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); expect(!!window.localStorage.getItem(batchStorageKey)).to.equal(true); }); @@ -1794,7 +1791,6 @@ describe('batch uploader', () => { const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; uploader.queueEvent(event0); - await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); expect(window.localStorage.getItem(batchStorageKey) === '').to.equal(true); }); @@ -1810,9 +1806,7 @@ describe('batch uploader', () => { await waitForCondition(hasIdentifyReturned); const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; - fetchMock.post(urls.events, 500, { overwriteRoutes: true }); uploader.queueEvent(event0); - await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); expect(!!window.localStorage.getItem(batchStorageKey)).to.equal(true); }); From 3b3b9b3ac2b26b180fa4711b72a28a216fc7ffc5 Mon Sep 17 00:00:00 2001 From: Jaissica Date: Thu, 18 Sep 2025 15:39:29 -0400 Subject: [PATCH 15/15] test: updated tests for batch uploader --- test/src/tests-batchUploader.ts | 37 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/test/src/tests-batchUploader.ts b/test/src/tests-batchUploader.ts index 0290da757..f15fd42a5 100644 --- a/test/src/tests-batchUploader.ts +++ b/test/src/tests-batchUploader.ts @@ -1722,9 +1722,11 @@ describe('batch uploader', () => { const batchStorageKey = 'mprtcl-v4_abcdef-batches'; beforeEach(() => { window.mParticle._resetForTests(MPConfig); + window.localStorage.clear(); + window.sessionStorage.clear(); }); - it('should store batches in local storage when noFunctional is false by default', async () => { + it('should store batches in session storage when noFunctional is false by default', async () => { window.mParticle.config.flags = { offlineStorage: '100', ...enableBatchingConfigFlags, @@ -1734,7 +1736,7 @@ describe('batch uploader', () => { const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; uploader.queueEvent(event0); - expect(!!window.sessionStorage.getItem(eventStorageKey)).to.equal(true); + expect(window.sessionStorage.getItem(eventStorageKey)).to.not.equal(null); }); it('should NOT store events in session storage when noFunctional is true', async () => { @@ -1742,15 +1744,13 @@ describe('batch uploader', () => { offlineStorage: '100', ...enableBatchingConfigFlags, }; - window.mParticle.config.launcherOptions = { - noFunctional: true, - }; + window.mParticle.config.noFunctional = true; window.mParticle.init(apiKey, window.mParticle.config); await waitForCondition(hasIdentifyReturned); const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; uploader.queueEvent(event0); - expect(window.sessionStorage.getItem(eventStorageKey) === '').to.equal(true); + expect(window.sessionStorage.getItem(eventStorageKey)).to.equal(null); }); it('should store events in session storage when noFunctional is false', async () => { @@ -1758,15 +1758,13 @@ describe('batch uploader', () => { offlineStorage: '100', ...enableBatchingConfigFlags, }; - window.mParticle.config.launcherOptions = { - noFunctional: false, - }; + window.mParticle.config.noFunctional = false; window.mParticle.init(apiKey, window.mParticle.config); await waitForCondition(hasIdentifyReturned); const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; uploader.queueEvent(event0); - expect(!!window.sessionStorage.getItem(eventStorageKey)).to.equal(true); + expect(window.sessionStorage.getItem(eventStorageKey)).to.not.equal(null); }); it('should store batches in local storage when noFunctional is false by default', async () => { @@ -1774,8 +1772,10 @@ describe('batch uploader', () => { await waitForCondition(hasIdentifyReturned); const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; + fetchMock.post(urls.events, 500, { overwriteRoutes: true }); uploader.queueEvent(event0); - expect(!!window.localStorage.getItem(batchStorageKey)).to.equal(true); + await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); + expect(window.localStorage.getItem(batchStorageKey)).to.not.equal(null); }); it('should NOT store batches in local storage when noFunctional is true', async () => { @@ -1783,15 +1783,14 @@ describe('batch uploader', () => { offlineStorage: '100', ...enableBatchingConfigFlags, }; - window.mParticle.config.launcherOptions = { - noFunctional: true, - }; + window.mParticle.config.noFunctional = true; window.mParticle.init(apiKey, window.mParticle.config); await waitForCondition(hasIdentifyReturned); const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; uploader.queueEvent(event0); - expect(window.localStorage.getItem(batchStorageKey) === '').to.equal(true); + await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); + expect(window.localStorage.getItem(batchStorageKey)).to.equal(null); }); it('should store batches in local storage when noFunctional is false', async () => { @@ -1799,15 +1798,15 @@ describe('batch uploader', () => { offlineStorage: '100', ...enableBatchingConfigFlags, }; - window.mParticle.config.launcherOptions = { - noFunctional: false, - }; + window.mParticle.config.noFunctional = false; window.mParticle.init(apiKey, window.mParticle.config); await waitForCondition(hasIdentifyReturned); const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; + fetchMock.post(urls.events, 500, { overwriteRoutes: true }); uploader.queueEvent(event0); - expect(!!window.localStorage.getItem(batchStorageKey)).to.equal(true); + await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); + expect(window.localStorage.getItem(batchStorageKey)).to.not.equal(null); }); });