From 988010e894cb51d41d89b98b337e02edbdce8ec7 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 21 Oct 2025 09:24:30 -0700 Subject: [PATCH 1/4] feat: custom storage --- __mocks__/setup.ts | 2 +- src/experimentClient.ts | 2 +- src/index.ts | 1 + src/storage/cache.ts | 4 +--- src/storage/local-storage.ts | 2 +- src/types/config.ts | 8 ++++++++ src/types/storage.ts | 2 +- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/__mocks__/setup.ts b/__mocks__/setup.ts index 89cc96b..5c15ac8 100644 --- a/__mocks__/setup.ts +++ b/__mocks__/setup.ts @@ -9,7 +9,7 @@ jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage); /* * Mock navigator to avoid undefined access in analytics-connector */ -global['navigator'] = { product: 'ReactNative' }; +global['navigator'] = { product: 'ReactNative' } as unknown as Navigator; /* * Mock Native Module diff --git a/src/experimentClient.ts b/src/experimentClient.ts index 5323456..ee9efe0 100644 --- a/src/experimentClient.ts +++ b/src/experimentClient.ts @@ -132,7 +132,7 @@ export class ExperimentClient implements Client { httpClient, ); // Storage & Caching - const storage = new LocalStorage(); + const storage = this.config.storage || new LocalStorage(); this.variants = getVariantStorage( this.apiKey, this.config.instanceName, diff --git a/src/index.ts b/src/index.ts index c95d1ab..fd4e77f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,3 +7,4 @@ export { Source } from './types/source'; export * from './types/user'; export * from './types/variant'; export * from './types/exposure'; +export * from './types/storage'; diff --git a/src/storage/cache.ts b/src/storage/cache.ts index 1b9960b..5698fef 100644 --- a/src/storage/cache.ts +++ b/src/storage/cache.ts @@ -3,8 +3,6 @@ import { EvaluationFlag } from '@amplitude/experiment-core'; import { Storage } from '../types/storage'; import { Variant } from '../types/variant'; -import { LocalStorage } from './local-storage'; - export const getVariantStorage = ( deploymentKey: string, instanceName: string, @@ -22,7 +20,7 @@ export const getVariantStorage = ( export const getFlagStorage = ( deploymentKey: string, instanceName: string, - storage: Storage = new LocalStorage(), + storage: Storage, ): LoadStoreCache => { const truncatedDeployment = deploymentKey.substring(deploymentKey.length - 6); const namespace = `amp-exp-${instanceName}-${truncatedDeployment}-flags`; diff --git a/src/storage/local-storage.ts b/src/storage/local-storage.ts index ea2ef95..1ed84b1 100644 --- a/src/storage/local-storage.ts +++ b/src/storage/local-storage.ts @@ -3,7 +3,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { Storage } from '../types/storage'; export class LocalStorage implements Storage { - async get(key: string): Promise { + async get(key: string): Promise { return await AsyncStorage.getItem(key); } diff --git a/src/types/config.ts b/src/types/config.ts index b66698f..05bcabd 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -2,6 +2,7 @@ import { FetchHttpClient } from '../transport/http'; import { ExposureTrackingProvider } from './exposure'; import { Source } from './source'; +import { Storage } from './storage'; import { HttpClient } from './transport'; import { ExperimentUserProvider } from './user'; import { Variant, Variants } from './variant'; @@ -131,6 +132,12 @@ export interface ExperimentConfig { * (Advanced) Use your own http client. */ httpClient?: HttpClient; + + /** + * (Advanced) Use your own storage implementation. + * If not provided, the client will use the default local storage implementation, which is AsyncStorage. + */ + storage?: Storage; } /** @@ -179,4 +186,5 @@ export const Defaults: ExperimentConfig = { userProvider: null, exposureTrackingProvider: null, httpClient: FetchHttpClient, + storage: null, }; diff --git a/src/types/storage.ts b/src/types/storage.ts index 36b1cb9..a4933fe 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -1,5 +1,5 @@ export interface Storage { - get(key: string): Promise; + get(key: string): Promise; put(key: string, value: string): Promise; delete(key: string): Promise; } From b5e4e2a784628b5c0b6589f7f5fbe5a46b6f310a Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Fri, 24 Oct 2025 01:32:56 -0700 Subject: [PATCH 2/4] test: added tests --- __tests__/client.test.ts | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/__tests__/client.test.ts b/__tests__/client.test.ts index 17dbcac..e9d1839 100644 --- a/__tests__/client.test.ts +++ b/__tests__/client.test.ts @@ -8,6 +8,7 @@ import { ConnectorExposureTrackingProvider } from '../src/integration/connector' import { FetchOptions } from '../src/types/client'; import { ExposureTrackingProvider } from '../src/types/exposure'; import { Source } from '../src/types/source'; +import { Storage } from '../src/types/storage'; import { HttpClient, SimpleResponse } from '../src/types/transport'; import { ExperimentUser, ExperimentUserProvider } from '../src/types/user'; import { Variant, Variants } from '../src/types/variant'; @@ -1010,6 +1011,66 @@ describe('start', () => { expect(variant.key).toEqual('on'); expect(variant2.key).toEqual('on'); }); + + test('start with custom storage', async () => { + // Create a custom storage object + const storageObject = {}; + const storage = { + get: async (key: string) => storageObject[key], + put: async (key: string, value: string) => { + storageObject[key] = value; + }, + delete: async (key: string) => { + delete storageObject[key]; + }, + } as Storage; + + // Create a client with the custom storage object + const client = new ExperimentClient(API_KEY, { storage }); + await client.start({ device_id: 'test_device' }); + + // Check that the flags are stored in the storage object + const storageKey = `amp-exp-$default_instance-${API_KEY.substring( + API_KEY.length - 6, + )}`; + expect( + JSON.parse(storageObject[storageKey + '-flags'])[serverKey], + ).toMatchObject({ + key: serverKey, + }); + // Check that the variant is stored in the storage object + expect(JSON.parse(storageObject[storageKey])[serverKey]).toMatchObject({ + key: 'on', + value: 'on', + }); + }); +}); + +test('fetch with custom storage', async () => { + // Create a custom storage object + const storageObject = {}; + const storage = { + get: async (key: string) => storageObject[key], + put: async (key: string, value: string) => { + storageObject[key] = value; + }, + delete: async (key: string) => { + delete storageObject[key]; + }, + } as Storage; + + // Create a client with the custom storage object + const client = new ExperimentClient(API_KEY, { storage }); + await client.fetch(testUser); + + // Check that the variant is stored in the storage object + const storageKey = `amp-exp-$default_instance-${API_KEY.substring( + API_KEY.length - 6, + )}`; + expect(JSON.parse(storageObject[storageKey])[serverKey]).toMatchObject({ + key: 'on', + value: 'on', + }); }); describe('fetch retry with different response codes', () => { From 1728cf4e743235cdd36820786a7e16fd0380c0b5 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 28 Oct 2025 13:08:40 -0700 Subject: [PATCH 3/4] test: fix test --- __tests__/client.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/__tests__/client.test.ts b/__tests__/client.test.ts index e9d1839..3834511 100644 --- a/__tests__/client.test.ts +++ b/__tests__/client.test.ts @@ -1040,8 +1040,7 @@ describe('start', () => { }); // Check that the variant is stored in the storage object expect(JSON.parse(storageObject[storageKey])[serverKey]).toMatchObject({ - key: 'on', - value: 'on', + key: 'off', }); }); }); From bd2dc2daad38df6f36429dc99bb934e6ce469b55 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Thu, 13 Nov 2025 15:14:04 -0800 Subject: [PATCH 4/4] fix: rm default argument --- src/storage/cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/cache.ts b/src/storage/cache.ts index b87d889..afe0f9d 100644 --- a/src/storage/cache.ts +++ b/src/storage/cache.ts @@ -30,7 +30,7 @@ export const getFlagStorage = ( export const getVariantsOptionsStorage = ( deploymentKey: string, instanceName: string, - storage: Storage = new LocalStorage(), + storage: Storage, ): SingleValueStoreCache => { const truncatedDeployment = deploymentKey.substring(deploymentKey.length - 6); const namespace = `amp-exp-${instanceName}-${truncatedDeployment}-variants-options`;