diff --git a/flagsmith-core.ts b/flagsmith-core.ts index 73ddec73..f1381c64 100644 --- a/flagsmith-core.ts +++ b/flagsmith-core.ts @@ -41,8 +41,9 @@ type RequestOptions = { } let AsyncStorage: AsyncStorageType = null; -const FLAGSMITH_KEY = "BULLET_TRAIN_DB"; -const FLAGSMITH_EVENT = "BULLET_TRAIN_EVENT"; +const DEFAULT_FLAGSMITH_KEY = "FLAGSMITH_DB"; +const DEFAULT_FLAGSMITH_EVENT = "FLAGSMITH_EVENT"; +let FlagsmithEvent = DEFAULT_FLAGSMITH_EVENT; const defaultAPI = 'https://edge.api.flagsmith.com/api/v1/'; let eventSource: typeof EventSource; const initError = function(caller: string) { @@ -80,7 +81,7 @@ const Flagsmith = class { } getFlags = () => { - let { api, evaluationContext } = this; + const { api, evaluationContext } = this; this.log("Get Flags") this.isLoading = true; @@ -271,7 +272,7 @@ const Flagsmith = class { timer: number|null= null dtrum= null withTraits?: ITraits|null= null - cacheOptions = {ttl:0, skipAPI: false, loadStale: false} + cacheOptions = {ttl:0, skipAPI: false, loadStale: false, storageKey: undefined as string|undefined} async init(config: IInitConfig) { const evaluationContext = toEvaluationContext(config.evaluationContext || {}); try { @@ -290,7 +291,7 @@ const Flagsmith = class { enableDynatrace, enableAnalytics, realtime, - eventSourceUrl= "https://realtime.flagsmith.com/", + eventSourceUrl= "https://realtime.flagsmith.com/", AsyncStorage: _AsyncStorage, identity, traits, @@ -331,7 +332,7 @@ const Flagsmith = class { onError?.(message); }; this.enableLogs = enableLogs || false; - this.cacheOptions = cacheOptions ? { skipAPI: !!cacheOptions.skipAPI, ttl: cacheOptions.ttl || 0, loadStale: !!cacheOptions.loadStale } : this.cacheOptions; + this.cacheOptions = cacheOptions ? { skipAPI: !!cacheOptions.skipAPI, ttl: cacheOptions.ttl || 0, storageKey:cacheOptions.storageKey, loadStale: !!cacheOptions.loadStale } : this.cacheOptions; if (!this.cacheOptions.ttl && this.cacheOptions.skipAPI) { console.warn("Flagsmith: you have set a cache ttl of 0 and are skipping API calls, this means the API will not be hit unless you clear local storage.") } @@ -345,6 +346,9 @@ const Flagsmith = class { this.ticks = 10000; this.timer = this.enableLogs ? new Date().valueOf() : null; this.cacheFlags = typeof AsyncStorage !== 'undefined' && !!cacheFlags; + + FlagsmithEvent = DEFAULT_FLAGSMITH_EVENT + "_" + evaluationContext.environment.apiKey; + if (_AsyncStorage) { AsyncStorage = _AsyncStorage; } @@ -381,7 +385,7 @@ const Flagsmith = class { } if (AsyncStorage && this.canUseStorage) { - AsyncStorage.getItem(FLAGSMITH_EVENT) + AsyncStorage.getItem(FlagsmithEvent) .then((res)=>{ try { this.evaluationEvent = JSON.parse(res!) || {} @@ -398,12 +402,12 @@ const Flagsmith = class { } if (AsyncStorage && this.canUseStorage) { - AsyncStorage.getItem(FLAGSMITH_EVENT, (err, res) => { + AsyncStorage.getItem(FlagsmithEvent, (err, res) => { if (res && this.evaluationContext.environment) { const json = JSON.parse(res); if (json[this.evaluationContext.environment.apiKey]) { - const state = this.getState(); - this.log("Retrieved events from cache", res); + const state = this.getState(); + this.log("Retrieved events from cache", res); this.setState({ ...state, evaluationEvent: json[this.evaluationContext.environment.apiKey], @@ -453,7 +457,7 @@ const Flagsmith = class { ...json, evaluationContext: toEvaluationContext({ ...json.evaluationContext, - identity: !!json.evaluationContext?.identity ? { + identity: json.evaluationContext?.identity ? { ...json.evaluationContext?.identity, traits: { ...json.evaluationContext?.identity?.traits || {}, @@ -510,7 +514,7 @@ const Flagsmith = class { } }; try { - const res = AsyncStorage.getItemSync? AsyncStorage.getItemSync(FLAGSMITH_KEY) : await AsyncStorage.getItem(FLAGSMITH_KEY); + const res = AsyncStorage.getItemSync? AsyncStorage.getItemSync(this.getStorageKey()) : await AsyncStorage.getItem(this.getStorageKey()); await onRetrievedStorage(null, res) } catch (e) {} } @@ -538,15 +542,6 @@ const Flagsmith = class { } } - private _loadedState(error: any = null, source: FlagSource, isFetching = false) { - return { - error, - isFetching, - isLoading: false, - source - } - } - getAllFlags() { return this.flags; } @@ -660,7 +655,7 @@ const Flagsmith = class { } setContext = (clientEvaluationContext: ClientEvaluationContext) => { - let evaluationContext = toEvaluationContext(clientEvaluationContext); + const evaluationContext = toEvaluationContext(clientEvaluationContext); this.evaluationContext = { ...evaluationContext, environment: evaluationContext.environment || this.evaluationContext.environment, @@ -745,6 +740,19 @@ const Flagsmith = class { return res; }; + private _loadedState(error: any = null, source: FlagSource, isFetching = false) { + return { + error, + isFetching, + isLoading: false, + source + } + } + + private getStorageKey = ()=> { + return this.cacheOptions?.storageKey || DEFAULT_FLAGSMITH_KEY + "_" + this.evaluationContext.environment?.apiKey + } + private log(...args: (unknown)[]) { if (this.enableLogs) { console.log.apply(this, ['FLAGSMITH:', new Date().valueOf() - (this.timer || 0), 'ms', ...args]); @@ -756,7 +764,7 @@ const Flagsmith = class { this.ts = new Date().valueOf(); const state = JSON.stringify(this.getState()); this.log('Setting storage', state); - AsyncStorage!.setItem(FLAGSMITH_KEY, state); + AsyncStorage!.setItem(this.getStorageKey(), state); } } @@ -820,7 +828,7 @@ const Flagsmith = class { private updateEventStorage() { if (this.enableAnalytics) { const events = JSON.stringify(this.getState().evaluationEvent); - AsyncStorage!.setItem(FLAGSMITH_EVENT, events); + AsyncStorage!.setItem(FlagsmithEvent, events); } } diff --git a/lib/flagsmith-es/package.json b/lib/flagsmith-es/package.json index fc00c895..d295b304 100644 --- a/lib/flagsmith-es/package.json +++ b/lib/flagsmith-es/package.json @@ -1,6 +1,6 @@ { "name": "flagsmith-es", - "version": "5.0.0", + "version": "6.0.0", "description": "Feature flagging to support continuous development. This is an esm equivalent of the standard flagsmith npm module.", "main": "./index.js", "type": "module", diff --git a/lib/flagsmith/package.json b/lib/flagsmith/package.json index 68988d6e..972d4f5b 100644 --- a/lib/flagsmith/package.json +++ b/lib/flagsmith/package.json @@ -1,6 +1,6 @@ { "name": "flagsmith", - "version": "5.0.0", + "version": "6.0.0", "description": "Feature flagging to support continuous development", "main": "./index.js", "repository": { diff --git a/lib/react-native-flagsmith/package.json b/lib/react-native-flagsmith/package.json index bcbde833..06416c38 100644 --- a/lib/react-native-flagsmith/package.json +++ b/lib/react-native-flagsmith/package.json @@ -1,6 +1,6 @@ { "name": "react-native-flagsmith", - "version": "5.0.0", + "version": "6.0.0", "description": "Feature flagging to support continuous development", "main": "./index.js", "repository": { diff --git a/test/cache.test.ts b/test/cache.test.ts index d88a73bd..cb70e156 100644 --- a/test/cache.test.ts +++ b/test/cache.test.ts @@ -1,7 +1,7 @@ -// Sample test import { defaultState, defaultStateAlt, + FLAGSMITH_KEY, getFlagsmith, getStateToCheck, identityState, @@ -33,7 +33,21 @@ describe('Cache', () => { onChange, }); await flagsmith.init(initConfig); - const cache = await AsyncStorage.getItem('BULLET_TRAIN_DB'); + const cache = await AsyncStorage.getItem(FLAGSMITH_KEY); + expect(getStateToCheck(JSON.parse(`${cache}`))).toEqual(defaultState); + }); + test('should set cache after init with custom key', async () => { + const onChange = jest.fn(); + const customKey = 'custom_key'; + const { flagsmith, initConfig, AsyncStorage, mockFetch } = getFlagsmith({ + cacheFlags: true, + cacheOptions: { + storageKey: customKey, + }, + onChange, + }); + await flagsmith.init(initConfig); + const cache = await AsyncStorage.getItem(customKey); expect(getStateToCheck(JSON.parse(`${cache}`))).toEqual(defaultState); }); test('should call onChange with cache then eventually with an API response', async () => { @@ -53,7 +67,7 @@ describe('Cache', () => { cacheFlags: true, onChange, }); - await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify(defaultStateAlt)); + await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify(defaultStateAlt)); await flagsmith.init(initConfig); // Flags retrieved from cache @@ -86,7 +100,7 @@ describe('Cache', () => { identity: testIdentity, onChange, }); - await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({ + await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({ ...defaultStateAlt, identity: 'bad_identity', })); @@ -102,7 +116,7 @@ describe('Cache', () => { onChange, cacheOptions: { ttl: 1 }, }); - await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({ + await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({ ...defaultStateAlt, ts: new Date().valueOf() - 100, })); @@ -120,7 +134,7 @@ describe('Cache', () => { onChange, cacheOptions: { ttl: 1, loadStale: true }, }); - await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({ + await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({ ...defaultStateAlt, ts: new Date().valueOf() - 100, })); @@ -138,7 +152,7 @@ describe('Cache', () => { onChange, cacheOptions: { ttl: 1000 }, }); - await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({ + await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({ ...defaultStateAlt, ts: new Date().valueOf(), })); @@ -155,7 +169,7 @@ describe('Cache', () => { cacheFlags: false, onChange, }); - await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({ + await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({ ...defaultStateAlt, ts: new Date().valueOf(), })); @@ -173,25 +187,7 @@ describe('Cache', () => { onChange, cacheOptions: { ttl: 1000, skipAPI: true }, }); - await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({ - ...defaultStateAlt, - ts: new Date().valueOf(), - })); - await flagsmith.init(initConfig); - expect(onChange).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledTimes(0); - expect(getStateToCheck(flagsmith.getState())).toEqual({ - ...defaultStateAlt, - }); - }); - test('should not get flags from API when skipAPI is set', async () => { - const onChange = jest.fn(); - const { flagsmith, initConfig, AsyncStorage, mockFetch } = getFlagsmith({ - cacheFlags: true, - onChange, - cacheOptions: { ttl: 1000, skipAPI: true }, - }); - await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({ + await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({ ...defaultStateAlt, ts: new Date().valueOf(), })); @@ -209,7 +205,7 @@ describe('Cache', () => { onChange, cacheOptions: { ttl: 1, skipAPI: true, loadStale: true }, }); - await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({ + await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({ ...defaultStateAlt, ts: new Date().valueOf() - 100, })); @@ -220,6 +216,7 @@ describe('Cache', () => { ...defaultStateAlt, }); }); + test('should validate flags are unchanged when fetched', async () => { const onChange = jest.fn(); const { flagsmith, initConfig, AsyncStorage, mockFetch } = getFlagsmith({ @@ -227,7 +224,7 @@ describe('Cache', () => { cacheFlags: true, preventFetch: true, }); - await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({ + await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({ ...defaultState, })); await flagsmith.init(initConfig); @@ -273,7 +270,7 @@ describe('Cache', () => { preventFetch: true, defaultFlags: defaultState.flags, }); - await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({ + await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({ ...defaultState, })); await flagsmith.init(initConfig); @@ -319,7 +316,7 @@ describe('Cache', () => { preventFetch: true, }); const storage = new SyncStorageMock(); - await storage.setItem('BULLET_TRAIN_DB', JSON.stringify({ + await storage.setItem(FLAGSMITH_KEY, JSON.stringify({ ...defaultState, })); flagsmith.init({ @@ -345,7 +342,7 @@ describe('Cache', () => { preventFetch: true, }); const storage = new SyncStorageMock(); - await storage.setItem('BULLET_TRAIN_DB', JSON.stringify({ + await storage.setItem(FLAGSMITH_KEY, JSON.stringify({ ...identityState, })); const ts = Date.now(); @@ -356,8 +353,8 @@ describe('Cache', () => { }); expect(flagsmith.getAllTraits()).toEqual({ ...identityState.traits, - ts - }) + ts, + }); }); test('should cache transient traits correctly', async () => { const onChange = jest.fn(); @@ -395,4 +392,4 @@ describe('Cache', () => { }, }) }); -}); \ No newline at end of file +}); diff --git a/test/default-flags.test.ts b/test/default-flags.test.ts index 7c21407a..a95548e3 100644 --- a/test/default-flags.test.ts +++ b/test/default-flags.test.ts @@ -1,5 +1,5 @@ // Sample test -import { defaultState, defaultStateAlt, getFlagsmith, getStateToCheck } from './test-constants'; +import { defaultState, defaultStateAlt, FLAGSMITH_KEY, getFlagsmith, getStateToCheck } from './test-constants'; import { IFlags } from '../types'; describe('Default Flags', () => { @@ -51,7 +51,7 @@ describe('Default Flags', () => { cacheFlags: true, defaultFlags: {...defaultFlags, ...itemsToRemove}, }); - await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({ + await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({ ...defaultState, flags: { ...defaultFlags, diff --git a/test/react.test.tsx b/test/react.test.tsx index a2feadcf..a91482c1 100644 --- a/test/react.test.tsx +++ b/test/react.test.tsx @@ -2,8 +2,9 @@ import React, { FC } from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import { FlagsmithProvider, useFlags, useFlagsmithLoading } from '../lib/flagsmith/react'; import { - defaultState, delay, - environmentID, + defaultState, + delay, + FLAGSMITH_KEY, getFlagsmith, getMockFetchWithValue, identityState, @@ -25,6 +26,7 @@ const FlagsmithPage: FC = () => { ); }; + export default FlagsmithPage; describe('FlagsmithProvider', () => { it('renders without crashing', () => { @@ -91,7 +93,34 @@ describe('FlagsmithProvider', () => { preventFetch: true, defaultFlags: defaultState.flags }); - await AsyncStorage.setItem("BULLET_TRAIN_DB", JSON.stringify({ + await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({ + ...defaultState + }) ) + render( + + + + ); + + await waitFor(() => { + expect(JSON.parse(screen.getByTestId("loading-state").innerHTML)).toEqual({"isLoading":false,"isFetching":false,"error":null,"source":"CACHE"}); + expect(JSON.parse(screen.getByTestId("flags").innerHTML)).toEqual(removeIds(defaultState.flags)); + }); + }); + + it('renders cached flags by custom key', async () => { + const customKey = 'custom_key'; + const onChange = jest.fn(); + const { flagsmith, initConfig, AsyncStorage } = getFlagsmith({ + onChange, + cacheFlags: true, + preventFetch: true, + defaultFlags: defaultState.flags, + cacheOptions: { + storageKey: customKey, + }, + }); + await AsyncStorage.setItem(customKey, JSON.stringify({ ...defaultState }) ) render( @@ -105,6 +134,7 @@ describe('FlagsmithProvider', () => { expect(JSON.parse(screen.getByTestId("flags").innerHTML)).toEqual(removeIds(defaultState.flags)); }); }); + it('renders default flags', async () => { const onChange = jest.fn(); diff --git a/test/test-constants.ts b/test/test-constants.ts index 20aebac8..0da6b4cf 100644 --- a/test/test-constants.ts +++ b/test/test-constants.ts @@ -5,7 +5,7 @@ import Mock = jest.Mock; import { promises as fs } from 'fs'; export const environmentID = 'QjgYur4LQTwe5HpvbvhpzK'; // Flagsmith Demo Projects - +export const FLAGSMITH_KEY = 'FLAGSMITH_DB' + "_" + environmentID; export const defaultState = { api: 'https://edge.api.flagsmith.com/api/v1/', evaluationContext: { diff --git a/types.d.ts b/types.d.ts index 4ffbb111..1dae9964 100644 --- a/types.d.ts +++ b/types.d.ts @@ -57,6 +57,7 @@ export interface IState { declare type ICacheOptions = { ttl?: number; skipAPI?: boolean; + storageKey?: string; loadStale?: boolean; };