From 8ad3d819e332c0b3e3ae47fc7e373038ebd9a51b Mon Sep 17 00:00:00 2001 From: Tim Yiu <137842098+tyiuhc@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:58:06 -0700 Subject: [PATCH 01/14] fix: implement consent-aware default user provider --- packages/experiment-browser/src/factory.ts | 6 +- packages/experiment-browser/src/index.ts | 1 + .../src/providers/default.ts | 14 +++- packages/experiment-tag/src/experiment.ts | 6 ++ .../providers/consent-aware-user-provider.ts | 68 +++++++++++++++++++ .../experiment-tag/src/providers/types.ts | 5 ++ .../src/storage/consent-aware-storage.ts | 37 ++++++++-- 7 files changed, 127 insertions(+), 10 deletions(-) create mode 100644 packages/experiment-tag/src/providers/consent-aware-user-provider.ts create mode 100644 packages/experiment-tag/src/providers/types.ts diff --git a/packages/experiment-browser/src/factory.ts b/packages/experiment-browser/src/factory.ts index 25f0aa6e..ac069b1f 100644 --- a/packages/experiment-browser/src/factory.ts +++ b/packages/experiment-browser/src/factory.ts @@ -71,7 +71,11 @@ const newExperimentClient = ( ): ExperimentClient => { return new ExperimentClient(apiKey, { ...config, - userProvider: new DefaultUserProvider(config?.userProvider, apiKey), + userProvider: + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + config.defaultUserProvider ?? + new DefaultUserProvider(config?.userProvider, apiKey), }); }; diff --git a/packages/experiment-browser/src/index.ts b/packages/experiment-browser/src/index.ts index f19e72a8..7b54808d 100644 --- a/packages/experiment-browser/src/index.ts +++ b/packages/experiment-browser/src/index.ts @@ -34,3 +34,4 @@ export { ExperimentPluginType, ExperimentEvent, } from './types/plugin'; +export { DefaultUserProvider } from './providers/default'; diff --git a/packages/experiment-browser/src/providers/default.ts b/packages/experiment-browser/src/providers/default.ts index ca4ae5e1..cbce1d2b 100644 --- a/packages/experiment-browser/src/providers/default.ts +++ b/packages/experiment-browser/src/providers/default.ts @@ -4,6 +4,7 @@ import { UAParser } from '@amplitude/ua-parser-js'; import { LocalStorage } from '../storage/local-storage'; import { SessionStorage } from '../storage/session-storage'; import { ExperimentUserProvider } from '../types/provider'; +import { Storage } from '../types/storage'; import { ExperimentUser } from '../types/user'; export class DefaultUserProvider implements ExperimentUserProvider { @@ -13,17 +14,24 @@ export class DefaultUserProvider implements ExperimentUserProvider { ? this.globalScope?.navigator.userAgent : undefined; private readonly ua = new UAParser(this.userAgent).getResult(); - private readonly localStorage = new LocalStorage(); - private readonly sessionStorage = new SessionStorage(); + private readonly localStorage: Storage; + private readonly sessionStorage: Storage; private readonly storageKey: string; public readonly userProvider: ExperimentUserProvider | undefined; private readonly apiKey?: string; - constructor(userProvider?: ExperimentUserProvider, apiKey?: string) { + constructor( + userProvider?: ExperimentUserProvider, + apiKey?: string, + customLocalStorage?: Storage, + customSessionStorage?: Storage, + ) { this.userProvider = userProvider; this.apiKey = apiKey; this.storageKey = `EXP_${this.apiKey?.slice(0, 10)}_DEFAULT_USER_PROVIDER`; + this.localStorage = customLocalStorage || new LocalStorage(); + this.sessionStorage = customSessionStorage || new SessionStorage(); } getUser(): ExperimentUser { diff --git a/packages/experiment-tag/src/experiment.ts b/packages/experiment-tag/src/experiment.ts index 962031fc..a0671e22 100644 --- a/packages/experiment-tag/src/experiment.ts +++ b/packages/experiment-tag/src/experiment.ts @@ -17,6 +17,7 @@ import mutate, { MutationController } from 'dom-mutator'; import { MessageBus } from './message-bus'; import { showPreviewModeModal } from './preview/preview'; +import { createConsentAwareUserProvider } from './providers/consent-aware-user-provider'; import { ConsentAwareStorage } from './storage/consent-aware-storage'; import { PageChangeEvent, SubscriptionManager } from './subscriptions'; import { @@ -155,6 +156,11 @@ export class DefaultWebExperimentClient implements WebExperimentClient { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore internalInstanceNameSuffix: 'web', + defaultUserProvider: createConsentAwareUserProvider( + this.storage, + config.userProvider, + this.apiKey, + ), initialFlags: initialFlagsString, // timeout for fetching remote flags fetchTimeoutMillis: 1000, diff --git a/packages/experiment-tag/src/providers/consent-aware-user-provider.ts b/packages/experiment-tag/src/providers/consent-aware-user-provider.ts new file mode 100644 index 00000000..4989e43c --- /dev/null +++ b/packages/experiment-tag/src/providers/consent-aware-user-provider.ts @@ -0,0 +1,68 @@ +import { + DefaultUserProvider, + ExperimentUserProvider, +} from '@amplitude/experiment-js-client'; + +import { ConsentAwareStorage } from '../storage/consent-aware-storage'; + +import { Storage } from './types'; + +/** + * Factory function that creates a DefaultUserProvider with consent-aware storage implementations. + * This allows the DefaultUserProvider to respect consent status when storing/retrieving user data. + * + * @param consentAwareStorage - The ConsentAwareStorage instance to use for all storage operations + * @param userProvider - Optional nested user provider for additional user context + * @param apiKey - Optional API key for storage key generation + * @returns A DefaultUserProvider configured with consent-aware storage + */ +export function createConsentAwareUserProvider( + consentAwareStorage: ConsentAwareStorage, + userProvider?: ExperimentUserProvider, + apiKey?: string, +): DefaultUserProvider { + return new DefaultUserProvider( + userProvider, + apiKey, + new ConsentAwareLocalStorage(consentAwareStorage), + new ConsentAwareSessionStorage(consentAwareStorage), + ); +} + +class ConsentAwareLocalStorage implements Storage { + constructor(private consentAwareStorage: ConsentAwareStorage) {} + + get(key: string): string { + return this.consentAwareStorage.getItem('localStorage', key) || ''; + } + + put(key: string, value: string): void { + this.consentAwareStorage.setItem('localStorage', key, value); + } + + delete(key: string): void { + this.consentAwareStorage.removeItem('localStorage', key); + } +} + +/** + * Adapter that implements the experiment-browser Storage interface + * using ConsentAwareStorage for sessionStorage operations + */ +class ConsentAwareSessionStorage implements Storage { + constructor(private consentAwareStorage: ConsentAwareStorage) {} + + get(key: string): string { + return ( + this.consentAwareStorage.getItem('sessionStorage', key) || '' + ); + } + + put(key: string, value: string): void { + this.consentAwareStorage.setItem('sessionStorage', key, value); + } + + delete(key: string): void { + this.consentAwareStorage.removeItem('sessionStorage', key); + } +} diff --git a/packages/experiment-tag/src/providers/types.ts b/packages/experiment-tag/src/providers/types.ts new file mode 100644 index 00000000..943e63bf --- /dev/null +++ b/packages/experiment-tag/src/providers/types.ts @@ -0,0 +1,5 @@ +export interface Storage { + get(key: string): string; + put(key: string, value: string): void; + delete(key: string): void; +} diff --git a/packages/experiment-tag/src/storage/consent-aware-storage.ts b/packages/experiment-tag/src/storage/consent-aware-storage.ts index a716be1a..4ea7e01f 100644 --- a/packages/experiment-tag/src/storage/consent-aware-storage.ts +++ b/packages/experiment-tag/src/storage/consent-aware-storage.ts @@ -16,6 +16,7 @@ import { */ export class ConsentAwareStorage { private inMemoryStorage: Map> = new Map(); + private inMemoryRawStorage: Map> = new Map(); private inMemoryMarketingCookies: Map = new Map(); private consentStatus: ConsentStatus; @@ -30,17 +31,31 @@ export class ConsentAwareStorage { this.consentStatus = consentStatus; if (consentStatus === ConsentStatus.GRANTED) { + // Persist JSON storage for (const [storageType, storageMap] of this.inMemoryStorage.entries()) { for (const [key, value] of storageMap.entries()) { try { const jsonString = JSON.stringify(value); getStorage(storageType)?.setItem(key, jsonString); } catch (error) { - console.warn(`Failed to persist data for key ${key}:`, error); + console.warn(`Failed to persist JSON data for key ${key}:`, error); } } } this.inMemoryStorage.clear(); + + // Persist raw string storage + for (const [storageType, storageMap] of this.inMemoryRawStorage.entries()) { + for (const [key, value] of storageMap.entries()) { + try { + getStorage(storageType)?.setItem(key, value); + } catch (error) { + console.warn(`Failed to persist raw data for key ${key}:`, error); + } + } + } + this.inMemoryRawStorage.clear(); + this.persistMarketingCookies().then(); } } @@ -102,19 +117,29 @@ export class ConsentAwareStorage { * Remove a value from storage with consent awareness */ public removeItem(storageType: StorageType, key: string): void { - const storageMap = this.inMemoryStorage.get(storageType); if (this.consentStatus === ConsentStatus.GRANTED) { removeStorageItem(storageType, key); return; } - if (storageMap) { - storageMap.delete(key); - if (storageMap.size === 0) { + + // Remove from JSON storage + const jsonStorageMap = this.inMemoryStorage.get(storageType); + if (jsonStorageMap) { + jsonStorageMap.delete(key); + if (jsonStorageMap.size === 0) { this.inMemoryStorage.delete(storageType); } } - } + // Remove from raw storage + const rawStorageMap = this.inMemoryRawStorage.get(storageType); + if (rawStorageMap) { + rawStorageMap.delete(key); + if (rawStorageMap.size === 0) { + this.inMemoryRawStorage.delete(storageType); + } + } + } /** * Set marketing cookie with consent awareness * Parses current campaign data from URL and referrer, then stores it in the marketing cookie From a9ad66ad5d266f7c2a22dcdf178013008650d837 Mon Sep 17 00:00:00 2001 From: Tim Yiu <137842098+tyiuhc@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:05:15 -0700 Subject: [PATCH 02/14] remove raw storage --- .../src/storage/consent-aware-storage.ts | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/packages/experiment-tag/src/storage/consent-aware-storage.ts b/packages/experiment-tag/src/storage/consent-aware-storage.ts index 4ea7e01f..a3d7e941 100644 --- a/packages/experiment-tag/src/storage/consent-aware-storage.ts +++ b/packages/experiment-tag/src/storage/consent-aware-storage.ts @@ -16,7 +16,6 @@ import { */ export class ConsentAwareStorage { private inMemoryStorage: Map> = new Map(); - private inMemoryRawStorage: Map> = new Map(); private inMemoryMarketingCookies: Map = new Map(); private consentStatus: ConsentStatus; @@ -43,19 +42,6 @@ export class ConsentAwareStorage { } } this.inMemoryStorage.clear(); - - // Persist raw string storage - for (const [storageType, storageMap] of this.inMemoryRawStorage.entries()) { - for (const [key, value] of storageMap.entries()) { - try { - getStorage(storageType)?.setItem(key, value); - } catch (error) { - console.warn(`Failed to persist raw data for key ${key}:`, error); - } - } - } - this.inMemoryRawStorage.clear(); - this.persistMarketingCookies().then(); } } @@ -130,15 +116,6 @@ export class ConsentAwareStorage { this.inMemoryStorage.delete(storageType); } } - - // Remove from raw storage - const rawStorageMap = this.inMemoryRawStorage.get(storageType); - if (rawStorageMap) { - rawStorageMap.delete(key); - if (rawStorageMap.size === 0) { - this.inMemoryRawStorage.delete(storageType); - } - } } /** * Set marketing cookie with consent awareness From 990d4df9755f66a8c7a03373a1ee1ae25a68e8cb Mon Sep 17 00:00:00 2001 From: Tim Yiu <137842098+tyiuhc@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:36:21 -0700 Subject: [PATCH 03/14] fix factory test --- packages/experiment-browser/src/factory.ts | 2 +- .../experiment-tag/src/storage/consent-aware-storage.ts | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/experiment-browser/src/factory.ts b/packages/experiment-browser/src/factory.ts index ac069b1f..168767f0 100644 --- a/packages/experiment-browser/src/factory.ts +++ b/packages/experiment-browser/src/factory.ts @@ -74,7 +74,7 @@ const newExperimentClient = ( userProvider: // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - config.defaultUserProvider ?? + config?.defaultUserProvider ?? new DefaultUserProvider(config?.userProvider, apiKey), }); }; diff --git a/packages/experiment-tag/src/storage/consent-aware-storage.ts b/packages/experiment-tag/src/storage/consent-aware-storage.ts index a3d7e941..06913e8b 100644 --- a/packages/experiment-tag/src/storage/consent-aware-storage.ts +++ b/packages/experiment-tag/src/storage/consent-aware-storage.ts @@ -107,15 +107,6 @@ export class ConsentAwareStorage { removeStorageItem(storageType, key); return; } - - // Remove from JSON storage - const jsonStorageMap = this.inMemoryStorage.get(storageType); - if (jsonStorageMap) { - jsonStorageMap.delete(key); - if (jsonStorageMap.size === 0) { - this.inMemoryStorage.delete(storageType); - } - } } /** * Set marketing cookie with consent awareness From 953e381ed5dede276fdb351a1036e3df55929040 Mon Sep 17 00:00:00 2001 From: Tim Yiu <137842098+tyiuhc@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:41:29 -0700 Subject: [PATCH 04/14] fix consent aware storage --- .../src/storage/consent-aware-storage.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/experiment-tag/src/storage/consent-aware-storage.ts b/packages/experiment-tag/src/storage/consent-aware-storage.ts index 06913e8b..a716be1a 100644 --- a/packages/experiment-tag/src/storage/consent-aware-storage.ts +++ b/packages/experiment-tag/src/storage/consent-aware-storage.ts @@ -30,14 +30,13 @@ export class ConsentAwareStorage { this.consentStatus = consentStatus; if (consentStatus === ConsentStatus.GRANTED) { - // Persist JSON storage for (const [storageType, storageMap] of this.inMemoryStorage.entries()) { for (const [key, value] of storageMap.entries()) { try { const jsonString = JSON.stringify(value); getStorage(storageType)?.setItem(key, jsonString); } catch (error) { - console.warn(`Failed to persist JSON data for key ${key}:`, error); + console.warn(`Failed to persist data for key ${key}:`, error); } } } @@ -103,11 +102,19 @@ export class ConsentAwareStorage { * Remove a value from storage with consent awareness */ public removeItem(storageType: StorageType, key: string): void { + const storageMap = this.inMemoryStorage.get(storageType); if (this.consentStatus === ConsentStatus.GRANTED) { removeStorageItem(storageType, key); return; } + if (storageMap) { + storageMap.delete(key); + if (storageMap.size === 0) { + this.inMemoryStorage.delete(storageType); + } + } } + /** * Set marketing cookie with consent awareness * Parses current campaign data from URL and referrer, then stores it in the marketing cookie From 045956d50473f35372529fb98e4f129b9d38a931 Mon Sep 17 00:00:00 2001 From: Tim Yiu <137842098+tyiuhc@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:43:45 -0700 Subject: [PATCH 05/14] mandatory api key --- packages/experiment-tag/src/experiment.ts | 2 +- .../src/providers/consent-aware-user-provider.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/experiment-tag/src/experiment.ts b/packages/experiment-tag/src/experiment.ts index a0671e22..f09b8be8 100644 --- a/packages/experiment-tag/src/experiment.ts +++ b/packages/experiment-tag/src/experiment.ts @@ -158,8 +158,8 @@ export class DefaultWebExperimentClient implements WebExperimentClient { internalInstanceNameSuffix: 'web', defaultUserProvider: createConsentAwareUserProvider( this.storage, - config.userProvider, this.apiKey, + config.userProvider, ), initialFlags: initialFlagsString, // timeout for fetching remote flags diff --git a/packages/experiment-tag/src/providers/consent-aware-user-provider.ts b/packages/experiment-tag/src/providers/consent-aware-user-provider.ts index 4989e43c..1c7d13dc 100644 --- a/packages/experiment-tag/src/providers/consent-aware-user-provider.ts +++ b/packages/experiment-tag/src/providers/consent-aware-user-provider.ts @@ -13,13 +13,13 @@ import { Storage } from './types'; * * @param consentAwareStorage - The ConsentAwareStorage instance to use for all storage operations * @param userProvider - Optional nested user provider for additional user context - * @param apiKey - Optional API key for storage key generation + * @param apiKey - API key for storage key generation * @returns A DefaultUserProvider configured with consent-aware storage */ export function createConsentAwareUserProvider( consentAwareStorage: ConsentAwareStorage, + apiKey: string, userProvider?: ExperimentUserProvider, - apiKey?: string, ): DefaultUserProvider { return new DefaultUserProvider( userProvider, From b48c693dcf07e482cfb84294fbec5ca61aca0f07 Mon Sep 17 00:00:00 2001 From: Tim Yiu <137842098+tyiuhc@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:59:27 -0700 Subject: [PATCH 06/14] simplify --- packages/experiment-browser/src/factory.ts | 14 +++- packages/experiment-tag/src/experiment.ts | 16 +++-- .../providers/consent-aware-user-provider.ts | 68 ------------------- .../experiment-tag/src/providers/types.ts | 5 -- .../src/storage/consent-aware-storage.ts | 35 ++++++++++ 5 files changed, 56 insertions(+), 82 deletions(-) delete mode 100644 packages/experiment-tag/src/providers/consent-aware-user-provider.ts delete mode 100644 packages/experiment-tag/src/providers/types.ts diff --git a/packages/experiment-browser/src/factory.ts b/packages/experiment-browser/src/factory.ts index 168767f0..ccf4007d 100644 --- a/packages/experiment-browser/src/factory.ts +++ b/packages/experiment-browser/src/factory.ts @@ -74,8 +74,18 @@ const newExperimentClient = ( userProvider: // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - config?.defaultUserProvider ?? - new DefaultUserProvider(config?.userProvider, apiKey), + config?.consentAwareStorage + ? new DefaultUserProvider( + config?.userProvider, + apiKey, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + config?.consentAwareStorage.localStorage, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + config?.consentAwareStorage.sessionStorage, + ) + : new DefaultUserProvider(config?.userProvider, apiKey), }); }; diff --git a/packages/experiment-tag/src/experiment.ts b/packages/experiment-tag/src/experiment.ts index f09b8be8..5d3ee904 100644 --- a/packages/experiment-tag/src/experiment.ts +++ b/packages/experiment-tag/src/experiment.ts @@ -17,8 +17,11 @@ import mutate, { MutationController } from 'dom-mutator'; import { MessageBus } from './message-bus'; import { showPreviewModeModal } from './preview/preview'; -import { createConsentAwareUserProvider } from './providers/consent-aware-user-provider'; -import { ConsentAwareStorage } from './storage/consent-aware-storage'; +import { + ConsentAwareLocalStorage, + ConsentAwareSessionStorage, + ConsentAwareStorage, +} from './storage/consent-aware-storage'; import { PageChangeEvent, SubscriptionManager } from './subscriptions'; import { ConsentOptions, @@ -156,11 +159,10 @@ export class DefaultWebExperimentClient implements WebExperimentClient { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore internalInstanceNameSuffix: 'web', - defaultUserProvider: createConsentAwareUserProvider( - this.storage, - this.apiKey, - config.userProvider, - ), + consentAwareStorage: { + localStorage: new ConsentAwareLocalStorage(this.storage), + sessionStorage: new ConsentAwareSessionStorage(this.storage), + }, initialFlags: initialFlagsString, // timeout for fetching remote flags fetchTimeoutMillis: 1000, diff --git a/packages/experiment-tag/src/providers/consent-aware-user-provider.ts b/packages/experiment-tag/src/providers/consent-aware-user-provider.ts deleted file mode 100644 index 1c7d13dc..00000000 --- a/packages/experiment-tag/src/providers/consent-aware-user-provider.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - DefaultUserProvider, - ExperimentUserProvider, -} from '@amplitude/experiment-js-client'; - -import { ConsentAwareStorage } from '../storage/consent-aware-storage'; - -import { Storage } from './types'; - -/** - * Factory function that creates a DefaultUserProvider with consent-aware storage implementations. - * This allows the DefaultUserProvider to respect consent status when storing/retrieving user data. - * - * @param consentAwareStorage - The ConsentAwareStorage instance to use for all storage operations - * @param userProvider - Optional nested user provider for additional user context - * @param apiKey - API key for storage key generation - * @returns A DefaultUserProvider configured with consent-aware storage - */ -export function createConsentAwareUserProvider( - consentAwareStorage: ConsentAwareStorage, - apiKey: string, - userProvider?: ExperimentUserProvider, -): DefaultUserProvider { - return new DefaultUserProvider( - userProvider, - apiKey, - new ConsentAwareLocalStorage(consentAwareStorage), - new ConsentAwareSessionStorage(consentAwareStorage), - ); -} - -class ConsentAwareLocalStorage implements Storage { - constructor(private consentAwareStorage: ConsentAwareStorage) {} - - get(key: string): string { - return this.consentAwareStorage.getItem('localStorage', key) || ''; - } - - put(key: string, value: string): void { - this.consentAwareStorage.setItem('localStorage', key, value); - } - - delete(key: string): void { - this.consentAwareStorage.removeItem('localStorage', key); - } -} - -/** - * Adapter that implements the experiment-browser Storage interface - * using ConsentAwareStorage for sessionStorage operations - */ -class ConsentAwareSessionStorage implements Storage { - constructor(private consentAwareStorage: ConsentAwareStorage) {} - - get(key: string): string { - return ( - this.consentAwareStorage.getItem('sessionStorage', key) || '' - ); - } - - put(key: string, value: string): void { - this.consentAwareStorage.setItem('sessionStorage', key, value); - } - - delete(key: string): void { - this.consentAwareStorage.removeItem('sessionStorage', key); - } -} diff --git a/packages/experiment-tag/src/providers/types.ts b/packages/experiment-tag/src/providers/types.ts deleted file mode 100644 index 943e63bf..00000000 --- a/packages/experiment-tag/src/providers/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Storage { - get(key: string): string; - put(key: string, value: string): void; - delete(key: string): void; -} diff --git a/packages/experiment-tag/src/storage/consent-aware-storage.ts b/packages/experiment-tag/src/storage/consent-aware-storage.ts index a716be1a..40fbc1a0 100644 --- a/packages/experiment-tag/src/storage/consent-aware-storage.ts +++ b/packages/experiment-tag/src/storage/consent-aware-storage.ts @@ -138,3 +138,38 @@ export class ConsentAwareStorage { } } } + +export class ConsentAwareLocalStorage { + constructor(private consentAwareStorage: ConsentAwareStorage) {} + + get(key: string): string { + return this.consentAwareStorage.getItem('localStorage', key) || ''; + } + + put(key: string, value: string): void { + this.consentAwareStorage.setItem('localStorage', key, value); + } + + delete(key: string): void { + this.consentAwareStorage.removeItem('localStorage', key); + } +} + +export class ConsentAwareSessionStorage { + constructor(private consentAwareStorage: ConsentAwareStorage) {} + + get(key: string): string { + return ( + this.consentAwareStorage.getItem('sessionStorage', key) || '' + ); + } + + put(key: string, value: string): void { + this.consentAwareStorage.setItem('sessionStorage', key, value); + } + + delete(key: string): void { + this.consentAwareStorage.removeItem('sessionStorage', key); + } +} + From 9a64fc75cb78f09e15ef6050ad05416d410a3c92 Mon Sep 17 00:00:00 2001 From: Tim Yiu <137842098+tyiuhc@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:02:45 -0700 Subject: [PATCH 07/14] remove unused export --- packages/experiment-browser/src/factory.ts | 9 +++------ packages/experiment-browser/src/index.ts | 1 - .../experiment-tag/src/storage/consent-aware-storage.ts | 1 - 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/experiment-browser/src/factory.ts b/packages/experiment-browser/src/factory.ts index ccf4007d..e9de00fa 100644 --- a/packages/experiment-browser/src/factory.ts +++ b/packages/experiment-browser/src/factory.ts @@ -72,17 +72,14 @@ const newExperimentClient = ( return new ExperimentClient(apiKey, { ...config, userProvider: - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + // @ts-expect-error - consentAwareStorage not in type definition config?.consentAwareStorage ? new DefaultUserProvider( config?.userProvider, apiKey, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + // @ts-expect-error - consentAwareStorage not in type definition config?.consentAwareStorage.localStorage, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + // @ts-expect-error - consentAwareStorage not in type definition config?.consentAwareStorage.sessionStorage, ) : new DefaultUserProvider(config?.userProvider, apiKey), diff --git a/packages/experiment-browser/src/index.ts b/packages/experiment-browser/src/index.ts index 7b54808d..f19e72a8 100644 --- a/packages/experiment-browser/src/index.ts +++ b/packages/experiment-browser/src/index.ts @@ -34,4 +34,3 @@ export { ExperimentPluginType, ExperimentEvent, } from './types/plugin'; -export { DefaultUserProvider } from './providers/default'; diff --git a/packages/experiment-tag/src/storage/consent-aware-storage.ts b/packages/experiment-tag/src/storage/consent-aware-storage.ts index 40fbc1a0..8b6ede0e 100644 --- a/packages/experiment-tag/src/storage/consent-aware-storage.ts +++ b/packages/experiment-tag/src/storage/consent-aware-storage.ts @@ -172,4 +172,3 @@ export class ConsentAwareSessionStorage { this.consentAwareStorage.removeItem('sessionStorage', key); } } - From 490041790e7f4e7e09fad70b9870e433415de91b Mon Sep 17 00:00:00 2001 From: Tim Yiu <137842098+tyiuhc@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:05:57 -0700 Subject: [PATCH 08/14] initialize experimentClient with consent-aware sessionstorage --- packages/experiment-browser/src/experimentClient.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/experiment-browser/src/experimentClient.ts b/packages/experiment-browser/src/experimentClient.ts index eb8157cd..2ddff2a7 100644 --- a/packages/experiment-browser/src/experimentClient.ts +++ b/packages/experiment-browser/src/experimentClient.ts @@ -176,7 +176,8 @@ export class ExperimentClient implements Client { ? `${this.config.instanceName}-${internalInstanceName}` : this.config.instanceName; if (this.isWebExperiment) { - storage = new SessionStorage(); + // @ts-expect-error - consentAwareStorage not in type definition + storage = config.consentAwareStorage.sessionStorage; } else { storage = new LocalStorage(); } From 5668549fa74c8e92d2b84e3d69a4511414446ee8 Mon Sep 17 00:00:00 2001 From: Tim Yiu <137842098+tyiuhc@users.noreply.github.com> Date: Wed, 8 Oct 2025 13:46:10 -0700 Subject: [PATCH 09/14] fix tests and storage method --- packages/experiment-tag/src/experiment.ts | 16 +++- .../src/storage/consent-aware-storage.ts | 75 ++++++++++++++----- .../experiment-tag/src/storage/storage.ts | 57 ++++++++------ packages/experiment-tag/src/util/messenger.ts | 7 +- packages/experiment-tag/src/util/url.ts | 13 ++-- .../experiment-tag/test/experiment.test.ts | 38 ++++------ 6 files changed, 127 insertions(+), 79 deletions(-) diff --git a/packages/experiment-tag/src/experiment.ts b/packages/experiment-tag/src/experiment.ts index 5d3ee904..c3969d58 100644 --- a/packages/experiment-tag/src/experiment.ts +++ b/packages/experiment-tag/src/experiment.ts @@ -22,6 +22,10 @@ import { ConsentAwareSessionStorage, ConsentAwareStorage, } from './storage/consent-aware-storage'; +import { + getAndParseStorageItem, + setAndStringifyStorageItem, +} from './storage/storage'; import { PageChangeEvent, SubscriptionManager } from './subscriptions'; import { ConsentOptions, @@ -878,9 +882,13 @@ export class DefaultWebExperimentClient implements WebExperimentClient { } }); - this.storage.setItem('sessionStorage', PREVIEW_MODE_SESSION_KEY, { - previewFlags: this.previewFlags, - }); + setAndStringifyStorageItem( + 'sessionStorage', + PREVIEW_MODE_SESSION_KEY, + { + previewFlags: this.previewFlags, + }, + ); const previewParamsToRemove = [ ...Object.keys(this.previewFlags), PREVIEW_MODE_PARAM, @@ -896,7 +904,7 @@ export class DefaultWebExperimentClient implements WebExperimentClient { // if in preview mode, listen for ForceVariant messages WindowMessenger.setup(); } else { - const previewState: PreviewState | null = this.storage.getItem( + const previewState = getAndParseStorageItem( 'sessionStorage', PREVIEW_MODE_SESSION_KEY, ); diff --git a/packages/experiment-tag/src/storage/consent-aware-storage.ts b/packages/experiment-tag/src/storage/consent-aware-storage.ts index 8b6ede0e..abeef085 100644 --- a/packages/experiment-tag/src/storage/consent-aware-storage.ts +++ b/packages/experiment-tag/src/storage/consent-aware-storage.ts @@ -4,10 +4,11 @@ import type { Campaign } from '@amplitude/analytics-core'; import { ConsentStatus } from '../types'; import { - getStorage, - getStorageItem, + getAndParseStorageItem, + getRawStorageItem, removeStorageItem, - setStorageItem, + setAndStringifyStorageItem, + setRawStorageItem, StorageType, } from './storage'; @@ -16,6 +17,7 @@ import { */ export class ConsentAwareStorage { private inMemoryStorage: Map> = new Map(); + private inMemoryRawStorage: Map> = new Map(); private inMemoryMarketingCookies: Map = new Map(); private consentStatus: ConsentStatus; @@ -32,15 +34,19 @@ export class ConsentAwareStorage { if (consentStatus === ConsentStatus.GRANTED) { for (const [storageType, storageMap] of this.inMemoryStorage.entries()) { for (const [key, value] of storageMap.entries()) { - try { - const jsonString = JSON.stringify(value); - getStorage(storageType)?.setItem(key, jsonString); - } catch (error) { - console.warn(`Failed to persist data for key ${key}:`, error); - } + setAndStringifyStorageItem(storageType, key, value); + } + } + for (const [ + storageType, + storageMap, + ] of this.inMemoryRawStorage.entries()) { + for (const [key, value] of storageMap.entries()) { + setRawStorageItem(storageType, key, value); } } this.inMemoryStorage.clear(); + this.inMemoryRawStorage.clear(); this.persistMarketingCookies().then(); } } @@ -73,7 +79,8 @@ export class ConsentAwareStorage { */ public getItem(storageType: StorageType, key: string): T | null { if (this.consentStatus === ConsentStatus.GRANTED) { - return getStorageItem(storageType, key); + const value = getAndParseStorageItem(storageType, key); + return value as T; } const storageMap = this.inMemoryStorage.get(storageType); @@ -89,7 +96,7 @@ export class ConsentAwareStorage { */ public setItem(storageType: StorageType, key: string, value: unknown): void { if (this.consentStatus === ConsentStatus.GRANTED) { - setStorageItem(storageType, key, value); + setAndStringifyStorageItem(storageType, key, value); } else { if (!this.inMemoryStorage.has(storageType)) { this.inMemoryStorage.set(storageType, new Map()); @@ -115,6 +122,42 @@ export class ConsentAwareStorage { } } + /** + * Get a raw string value from storage with consent awareness + * This is used by Storage interface implementations that expect raw strings + */ + public getRawItem(storageType: StorageType, key: string): string { + if (this.consentStatus === ConsentStatus.GRANTED) { + return getRawStorageItem(storageType, key); + } + + const storageMap = this.inMemoryRawStorage.get(storageType); + if (storageMap && storageMap.has(key)) { + return storageMap.get(key) || ''; + } + + return ''; + } + + /** + * Set a raw string value in storage with consent awareness + * This is used by Storage interface implementations that work with raw strings + */ + public setRawItem( + storageType: StorageType, + key: string, + value: string, + ): void { + if (this.consentStatus === ConsentStatus.GRANTED) { + setRawStorageItem(storageType, key, value); + } else { + if (!this.inMemoryRawStorage.has(storageType)) { + this.inMemoryRawStorage.set(storageType, new Map()); + } + this.inMemoryRawStorage.get(storageType)?.set(key, value); + } + } + /** * Set marketing cookie with consent awareness * Parses current campaign data from URL and referrer, then stores it in the marketing cookie @@ -143,11 +186,11 @@ export class ConsentAwareLocalStorage { constructor(private consentAwareStorage: ConsentAwareStorage) {} get(key: string): string { - return this.consentAwareStorage.getItem('localStorage', key) || ''; + return this.consentAwareStorage.getRawItem('localStorage', key); } put(key: string, value: string): void { - this.consentAwareStorage.setItem('localStorage', key, value); + this.consentAwareStorage.setRawItem('localStorage', key, value); } delete(key: string): void { @@ -159,13 +202,11 @@ export class ConsentAwareSessionStorage { constructor(private consentAwareStorage: ConsentAwareStorage) {} get(key: string): string { - return ( - this.consentAwareStorage.getItem('sessionStorage', key) || '' - ); + return this.consentAwareStorage.getRawItem('sessionStorage', key); } put(key: string, value: string): void { - this.consentAwareStorage.setItem('sessionStorage', key, value); + this.consentAwareStorage.setRawItem('sessionStorage', key, value); } delete(key: string): void { diff --git a/packages/experiment-tag/src/storage/storage.ts b/packages/experiment-tag/src/storage/storage.ts index 29f19644..0c05433f 100644 --- a/packages/experiment-tag/src/storage/storage.ts +++ b/packages/experiment-tag/src/storage/storage.ts @@ -3,43 +3,54 @@ import { getGlobalScope } from '@amplitude/experiment-core'; export type StorageType = 'localStorage' | 'sessionStorage'; /** - * Get a JSON value from storage and parse it + * Get a JSON value from storage * @param storageType - The type of storage to use ('localStorage' or 'sessionStorage') - * @param key - The key to retrieve - * @returns The parsed JSON value or null if not found or invalid JSON + * @param key - The key to retrieve the value for + * @returns The JSON string value, or null if not found */ -export const getStorageItem = ( +export const getRawStorageItem = ( + storageType: StorageType, + key: string, +): string => { + return getStorage(storageType)?.getItem(key) || ''; +}; + +/** + * Set a JSON value in storage + * @param storageType - The type of storage to use ('localStorage' or 'sessionStorage') + * @param key - The key to set the value for + * @param value - The value to set + */ +export const setRawStorageItem = ( + storageType: StorageType, + key: string, + value: string, +): void => { + getStorage(storageType)?.setItem(key, value); +}; + +export const getAndParseStorageItem = ( storageType: StorageType, key: string, ): T | null => { + const value = getRawStorageItem(storageType, key); try { - const value = getStorage(storageType)?.getItem(key); - if (!value) { - return null; - } - return JSON.parse(value) as T; - } catch (error) { - console.warn(`Failed to get and parse JSON from ${storageType}:`, error); + return JSON.parse(value); + } catch { return null; } }; -/** - * Set a JSON value in storage by stringifying it - * @param storageType - The type of storage to use ('localStorage' or 'sessionStorage') - * @param key - The key to store the value under - * @param value - The value to stringify and store - */ -export const setStorageItem = ( +export const setAndStringifyStorageItem = ( storageType: StorageType, key: string, - value: unknown, + value: T, ): void => { try { - const jsonString = JSON.stringify(value); - getStorage(storageType)?.setItem(key, jsonString); + const stringValue = JSON.stringify(value); + setRawStorageItem(storageType, key, stringValue); } catch (error) { - console.warn(`Failed to stringify and set JSON in ${storageType}:`, error); + console.warn(`Failed to persist data for key ${key}:`, error); } }; @@ -59,7 +70,7 @@ export const removeStorageItem = ( } }; -export const getStorage = (storageType: StorageType): Storage | null => { +const getStorage = (storageType: StorageType): Storage | null => { const globalScope = getGlobalScope(); if (!globalScope) { return null; diff --git a/packages/experiment-tag/src/util/messenger.ts b/packages/experiment-tag/src/util/messenger.ts index 1cf0bb19..8e754a51 100644 --- a/packages/experiment-tag/src/util/messenger.ts +++ b/packages/experiment-tag/src/util/messenger.ts @@ -1,6 +1,6 @@ import { getGlobalScope } from '@amplitude/experiment-core'; -import { getStorageItem } from '../storage/storage'; +import { getAndParseStorageItem } from '../storage/storage'; interface VisualEditorSession { injectSrc: string; @@ -73,10 +73,11 @@ export class WindowMessenger { * Retrieve stored session data (read-only) */ private static getStoredSession(): VisualEditorSession | null { - const sessionData = getStorageItem( + const sessionData = getAndParseStorageItem( 'sessionStorage', VISUAL_EDITOR_SESSION_KEY, - ); + ) as VisualEditorSession; + if (!sessionData) { return null; } diff --git a/packages/experiment-tag/src/util/url.ts b/packages/experiment-tag/src/util/url.ts index 12089f27..e03c4789 100644 --- a/packages/experiment-tag/src/util/url.ts +++ b/packages/experiment-tag/src/util/url.ts @@ -1,7 +1,7 @@ import { getGlobalScope } from '@amplitude/experiment-core'; import { PREVIEW_MODE_PARAM, PREVIEW_MODE_SESSION_KEY } from '../experiment'; -import { getStorageItem } from '../storage/storage'; +import { getAndParseStorageItem, getRawStorageItem } from '../storage/storage'; import { PreviewState } from '../types'; export const getUrlParams = (): Record => { @@ -88,15 +88,12 @@ export const isPreviewMode = (): boolean => { if (getUrlParams()[PREVIEW_MODE_PARAM] === 'true') { return true; } - const previewState = getStorageItem( + const previewState = getAndParseStorageItem( 'sessionStorage', PREVIEW_MODE_SESSION_KEY, - ) as PreviewState; - if ( + ); + return !!( previewState?.previewFlags && Object.keys(previewState.previewFlags).length > 0 - ) { - return true; - } - return false; + ); }; diff --git a/packages/experiment-tag/test/experiment.test.ts b/packages/experiment-tag/test/experiment.test.ts index 1cdc7263..b546f44a 100644 --- a/packages/experiment-tag/test/experiment.test.ts +++ b/packages/experiment-tag/test/experiment.test.ts @@ -1507,28 +1507,18 @@ describe('initializeExperiment', () => { }); describe('remote evaluation - flag already stored in session storage', () => { - const sessionStorageMock = () => { - let store = {}; - return { - getItem: jest.fn((key) => store[key] || null), - setItem: jest.fn((key, value) => { - store[key] = value; - }), - removeItem: jest.fn((key) => { - delete store[key]; - }), - clear: jest.fn(() => { - store = {}; - }), - }; - }; + let testMockGlobal: any; + beforeEach(() => { - Object.defineProperty(safeGlobal, 'sessionStorage', { - value: sessionStorageMock(), + // Create fresh mock global for each test in this describe block + testMockGlobal = newMockGlobal({ + experimentConfig: { + consentOptions: { + status: ConsentStatus.GRANTED, + }, + }, }); - }); - afterEach(() => { - safeGlobal.sessionStorage.clear(); + mockGetGlobalScope.mockReturnValue(testMockGlobal); }); test('evaluated, applied, and impression tracked, start updates flag in storage, applied, impression deduped', async () => { const apiKey = 'api1'; @@ -1545,7 +1535,7 @@ describe('initializeExperiment', () => { flagVersion: 2, }, ); - safeGlobal.sessionStorage.setItem( + testMockGlobal.sessionStorage.setItem( storageKey, JSON.stringify({ test: storedFlag }), ); @@ -1592,7 +1582,7 @@ describe('initializeExperiment', () => { expect(mockExposure).toHaveBeenCalledTimes(1); // Check remote flag store in storage const flags = JSON.parse( - safeGlobal.sessionStorage.getItem(storageKey) as string, + testMockGlobal.sessionStorage.getItem(storageKey) as string, ); expect(flags['test'].metadata.flagVersion).toEqual(4); expect(flags['test'].metadata.evaluationMode).toEqual('local'); @@ -1619,7 +1609,7 @@ describe('initializeExperiment', () => { flagVersion: 2, }, ); - safeGlobal.sessionStorage.setItem( + testMockGlobal.sessionStorage.setItem( storageKey, JSON.stringify({ test: storedFlag }), ); @@ -1674,7 +1664,7 @@ describe('initializeExperiment', () => { expect(mockExposure).toHaveBeenCalledTimes(2); // Check remote flag store in storage const flags = JSON.parse( - safeGlobal.sessionStorage.getItem(storageKey) as string, + testMockGlobal.sessionStorage.getItem(storageKey) as string, ); expect(flags['test'].metadata.flagVersion).toEqual(4); expect(flags['test'].metadata.evaluationMode).toEqual('local'); From f923423bbe0eaf50bb00a901016115265813ca1a Mon Sep 17 00:00:00 2001 From: Tim Yiu <137842098+tyiuhc@users.noreply.github.com> Date: Wed, 8 Oct 2025 13:49:42 -0700 Subject: [PATCH 10/14] simplify --- packages/experiment-tag/src/storage/storage.ts | 6 +++--- packages/experiment-tag/test/experiment.test.ts | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/experiment-tag/src/storage/storage.ts b/packages/experiment-tag/src/storage/storage.ts index 0c05433f..e18948a7 100644 --- a/packages/experiment-tag/src/storage/storage.ts +++ b/packages/experiment-tag/src/storage/storage.ts @@ -3,7 +3,7 @@ import { getGlobalScope } from '@amplitude/experiment-core'; export type StorageType = 'localStorage' | 'sessionStorage'; /** - * Get a JSON value from storage + * Get a JSON string value from storage * @param storageType - The type of storage to use ('localStorage' or 'sessionStorage') * @param key - The key to retrieve the value for * @returns The JSON string value, or null if not found @@ -16,10 +16,10 @@ export const getRawStorageItem = ( }; /** - * Set a JSON value in storage + * Set a JSON string value in storage * @param storageType - The type of storage to use ('localStorage' or 'sessionStorage') * @param key - The key to set the value for - * @param value - The value to set + * @param value - The JSON string value to set */ export const setRawStorageItem = ( storageType: StorageType, diff --git a/packages/experiment-tag/test/experiment.test.ts b/packages/experiment-tag/test/experiment.test.ts index b546f44a..35ec7536 100644 --- a/packages/experiment-tag/test/experiment.test.ts +++ b/packages/experiment-tag/test/experiment.test.ts @@ -1510,7 +1510,6 @@ describe('initializeExperiment', () => { let testMockGlobal: any; beforeEach(() => { - // Create fresh mock global for each test in this describe block testMockGlobal = newMockGlobal({ experimentConfig: { consentOptions: { From 36cbe46b7e62fd6ce94336c11347b49a828609a8 Mon Sep 17 00:00:00 2001 From: Tim Yiu <137842098+tyiuhc@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:25:57 -0700 Subject: [PATCH 11/14] simplify factory --- packages/experiment-browser/src/factory.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/experiment-browser/src/factory.ts b/packages/experiment-browser/src/factory.ts index e9de00fa..9edcb308 100644 --- a/packages/experiment-browser/src/factory.ts +++ b/packages/experiment-browser/src/factory.ts @@ -71,18 +71,14 @@ const newExperimentClient = ( ): ExperimentClient => { return new ExperimentClient(apiKey, { ...config, - userProvider: - // @ts-expect-error - consentAwareStorage not in type definition - config?.consentAwareStorage - ? new DefaultUserProvider( - config?.userProvider, - apiKey, - // @ts-expect-error - consentAwareStorage not in type definition - config?.consentAwareStorage.localStorage, - // @ts-expect-error - consentAwareStorage not in type definition - config?.consentAwareStorage.sessionStorage, - ) - : new DefaultUserProvider(config?.userProvider, apiKey), + userProvider: config?.['consentAwareStorage'] + ? new DefaultUserProvider( + config?.userProvider, + apiKey, + config?.['consentAwareStorage']?.['localStorage'], + config?.['consentAwareStorage']?.['sessionStorage'], + ) + : new DefaultUserProvider(config?.userProvider, apiKey), }); }; From b69b86298cb469a9afa5f6ea4662d3c12db5c1be Mon Sep 17 00:00:00 2001 From: Tim Yiu <137842098+tyiuhc@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:41:30 -0700 Subject: [PATCH 12/14] simplify init --- packages/experiment-browser/src/experimentClient.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/experiment-browser/src/experimentClient.ts b/packages/experiment-browser/src/experimentClient.ts index 2ddff2a7..ff079b4c 100644 --- a/packages/experiment-browser/src/experimentClient.ts +++ b/packages/experiment-browser/src/experimentClient.ts @@ -176,8 +176,7 @@ export class ExperimentClient implements Client { ? `${this.config.instanceName}-${internalInstanceName}` : this.config.instanceName; if (this.isWebExperiment) { - // @ts-expect-error - consentAwareStorage not in type definition - storage = config.consentAwareStorage.sessionStorage; + storage = config?.['consentAwareStorage']?.['sessionStorage']; } else { storage = new LocalStorage(); } From 7c31cdc3a3b61f3370a2c172cdb23fd2903d51ac Mon Sep 17 00:00:00 2001 From: Tim Yiu <137842098+tyiuhc@users.noreply.github.com> Date: Wed, 15 Oct 2025 15:03:25 -0700 Subject: [PATCH 13/14] Update packages/experiment-browser/src/factory.ts Co-authored-by: Stephen Choi <106711294+stephen-choi-amplitude@users.noreply.github.com> --- packages/experiment-browser/src/factory.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/experiment-browser/src/factory.ts b/packages/experiment-browser/src/factory.ts index 9edcb308..2cd61f4a 100644 --- a/packages/experiment-browser/src/factory.ts +++ b/packages/experiment-browser/src/factory.ts @@ -71,14 +71,12 @@ const newExperimentClient = ( ): ExperimentClient => { return new ExperimentClient(apiKey, { ...config, - userProvider: config?.['consentAwareStorage'] - ? new DefaultUserProvider( - config?.userProvider, - apiKey, - config?.['consentAwareStorage']?.['localStorage'], - config?.['consentAwareStorage']?.['sessionStorage'], - ) - : new DefaultUserProvider(config?.userProvider, apiKey), + userProvider: new DefaultUserProvider( + config?.userProvider, + apiKey, + config?.['consentAwareStorage']?.localStorage, + config?.['consentAwareStorage']?.sessionStorage, + ), }); }; From 64fa6e6ea3df9de4ed54467deb882202cfb1e88c Mon Sep 17 00:00:00 2001 From: Tim Yiu <137842098+tyiuhc@users.noreply.github.com> Date: Wed, 15 Oct 2025 15:29:55 -0700 Subject: [PATCH 14/14] simplify per comments --- packages/experiment-tag/src/util/url.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/experiment-tag/src/util/url.ts b/packages/experiment-tag/src/util/url.ts index e03c4789..5d5bd66b 100644 --- a/packages/experiment-tag/src/util/url.ts +++ b/packages/experiment-tag/src/util/url.ts @@ -1,7 +1,7 @@ import { getGlobalScope } from '@amplitude/experiment-core'; import { PREVIEW_MODE_PARAM, PREVIEW_MODE_SESSION_KEY } from '../experiment'; -import { getAndParseStorageItem, getRawStorageItem } from '../storage/storage'; +import { getAndParseStorageItem } from '../storage/storage'; import { PreviewState } from '../types'; export const getUrlParams = (): Record => { @@ -92,8 +92,11 @@ export const isPreviewMode = (): boolean => { 'sessionStorage', PREVIEW_MODE_SESSION_KEY, ); - return !!( - previewState?.previewFlags && + if (!previewState) { + return false; + } + return ( + previewState.previewFlags && Object.keys(previewState.previewFlags).length > 0 ); };