diff --git a/src/batchUploader.ts b/src/batchUploader.ts index 54acfb5ac..9f456dad0 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(); + this.offlineStorageEnabled = + this.isOfflineStorageAvailable() && + !mpInstance._Store.getPrivacyFlag('OfflineEvents'); if (this.offlineStorageEnabled) { this.eventVault = new SessionStorageVault( diff --git a/src/constants.ts b/src/constants.ts index ee8d4b721..456b10539 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -229,3 +229,15 @@ 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; + +export type PrivacyControl = 'functional' | 'targeting'; + +export type StorageTypes = 'SDKState' | 'Products' | 'OfflineEvents' | 'IdentityCache' | 'TimeOnSite'; + +export const StoragePrivacyMap: Record = { + SDKState: 'functional', + Products: 'targeting', + OfflineEvents: 'functional', + IdentityCache: 'functional', + TimeOnSite: 'targeting', +}; diff --git a/src/store.ts b/src/store.ts index 637d57d3c..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 from './constants'; +import Constants, { PrivacyControl, StoragePrivacyMap, StorageTypes } from './constants'; import { DataPlanResult, KitBlockerOptions, @@ -216,6 +216,7 @@ export interface IStore { setNoFunctional?(noFunctional: boolean): void; getNoTargeting?(): boolean; setNoTargeting?(noTargeting: boolean): void; + getPrivacyFlag?(storageType: StorageTypes): boolean; getDeviceId?(): string; setDeviceId?(deviceId: string): void; @@ -608,6 +609,17 @@ export default function Store( this.noTargeting = noTargeting; }; + this.getPrivacyFlag = (storageType: StorageTypes): boolean => { + const privacyControl: 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 944d2a7a0..35f35c3f0 100644 --- a/test/jest/batchUploader.spec.ts +++ b/test/jest/batchUploader.spec.ts @@ -1,9 +1,13 @@ 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; let mockMPInstance: IMParticleWebSDKInstance; + let originalFetch: typeof global.fetch; beforeEach(() => { const now = Date.now(); @@ -11,22 +15,45 @@ 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 mockMPInstance = { _Store: { + storageName: 'mprtcl-v4_abcdef', + 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(); + } + if (privacyControl === 'targeting') { + return this.getNoTargeting(); + } + return false; + }, + deviceId: 'device-1', SDKConfig: { flags: {} } }, _Helpers: { - getFeatureFlag: jest.fn().mockReturnValue(false), - createServiceUrl: jest.fn().mockReturnValue('https://mock-url.com') + getFeatureFlag: jest.fn().mockReturnValue('100'), + 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: { + verbose: jest.fn(), + error: jest.fn(), + warning: jest.fn(), } } as unknown as IMParticleWebSDKInstance; @@ -35,6 +62,7 @@ describe('BatchUploader', () => { afterEach(() => { jest.useRealTimers(); + global.fetch = originalFetch; }); describe('shouldDebounceAST', () => { @@ -104,4 +132,50 @@ 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); + expect(uploader['offlineStorageEnabled']).toBe(false); + + uploader.queueEvent({ EventDataType: 4 } as SDKEvent); + expect(sessionStorage.getItem('mprtcl-v4_abcdef-events')).toBeNull(); + expect(localStorage.getItem('mprtcl-v4_abcdef-batches')).toBeNull(); + }); + + it('should enable offline storage when noFunctional is false by default', async () => { + const uploader = new BatchUploader(mockMPInstance, 1000); + + expect(uploader['offlineStorageEnabled']).toBe(true); + + uploader.queueEvent({ EventDataType: 4 } as SDKEvent); + 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', async () => { + mockMPInstance._Store.noFunctional = false; + + const uploader = new BatchUploader(mockMPInstance, 1000); + + uploader.queueEvent({ EventDataType: 4 } as SDKEvent); + 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 5b76bb840..f15fd42a5 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,99 @@ describe('batch uploader', () => { .catch((e) => { }) }); + + describe('noFunctional', () => { + const eventStorageKey = 'mprtcl-v4_abcdef-events'; + const batchStorageKey = 'mprtcl-v4_abcdef-batches'; + beforeEach(() => { + window.mParticle._resetForTests(MPConfig); + window.localStorage.clear(); + window.sessionStorage.clear(); + }); + + it('should store batches in session storage when noFunctional is false by default', async () => { + window.mParticle.config.flags = { + offlineStorage: '100', + ...enableBatchingConfigFlags, + }; + 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.not.equal(null); + }); + + it('should NOT store events in session storage when noFunctional is true', async () => { + window.mParticle.config.flags = { + offlineStorage: '100', + ...enableBatchingConfigFlags, + }; + 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(null); + }); + + it('should store events in session storage when noFunctional is false', async () => { + window.mParticle.config.flags = { + offlineStorage: '100', + ...enableBatchingConfigFlags, + }; + 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.not.equal(null); + }); + + 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(); + 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.not.equal(null); + }); + + it('should NOT store batches in local storage when noFunctional is true', async () => { + window.mParticle.config.flags = { + offlineStorage: '100', + ...enableBatchingConfigFlags, + }; + 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); + 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 () => { + window.mParticle.config.flags = { + offlineStorage: '100', + ...enableBatchingConfigFlags, + }; + 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); + await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); + expect(window.localStorage.getItem(batchStorageKey)).to.not.equal(null); + }); + + }); }); }); \ No newline at end of file