diff --git a/packages/experiment-browser/src/config.ts b/packages/experiment-browser/src/config.ts index a9e3d2d2..2094693a 100644 --- a/packages/experiment-browser/src/config.ts +++ b/packages/experiment-browser/src/config.ts @@ -36,6 +36,12 @@ export interface ExperimentConfig { */ initialVariants?: Variants; + /** + * Initial values for flags. This is useful for bootstrapping the + * client with fallbacks for flag configs. + */ + initialFlags?: string; + /** * Determines the primary source of variants and variants before falling back. * @see Source @@ -144,6 +150,7 @@ export interface ExperimentConfig { | **instanceName** | `$default_instance` | | **fallbackVariant** | `null` | | **initialVariants** | `null` | + | **initialFlags** | `undefined` | | **source** | `Source.LocalStorage` | | **serverUrl** | `"https://api.lab.amplitude.com"` | | **flagsServerUrl** | `"https://flag.lab.amplitude.com"` | @@ -166,6 +173,7 @@ export const Defaults: ExperimentConfig = { instanceName: '$default_instance', fallbackVariant: {}, initialVariants: {}, + initialFlags: undefined, source: Source.LocalStorage, serverUrl: 'https://api.lab.amplitude.com', flagsServerUrl: 'https://flag.lab.amplitude.com', diff --git a/packages/experiment-browser/src/experimentClient.ts b/packages/experiment-browser/src/experimentClient.ts index 9fe45390..ca7cc21c 100644 --- a/packages/experiment-browser/src/experimentClient.ts +++ b/packages/experiment-browser/src/experimentClient.ts @@ -37,7 +37,6 @@ import { isLocalEvaluationMode, isNullOrUndefined, isNullUndefinedOrEmpty, - isRemoteEvaluationMode, } from './util'; import { Backoff } from './util/backoff'; import { @@ -155,6 +154,7 @@ export class ExperimentClient implements Client { } catch (e) { console.warn('Failed to load flags and variants from localStorage', e); } + this.mergeInitialFlagsWithStorage(); } /** @@ -393,6 +393,17 @@ export class ExperimentClient implements Client { return this; } + private mergeInitialFlagsWithStorage(): void { + if (this.config.initialFlags) { + const initialFlags = JSON.parse(this.config.initialFlags); + initialFlags.forEach((flag: EvaluationFlag) => { + if (!this.flags.get(flag.key)) { + this.flags.put(flag.key, flag); + } + }); + } + } + private evaluate(flagKeys?: string[]): Variants { const user = this.addContext(this.user); const flags = topologicalSort(this.flags.getAll(), flagKeys); @@ -424,8 +435,6 @@ export class ExperimentClient implements Client { return sourceVariant; } - // TODO variant and source for both local and remote needs to be cleaned up. - /** * This function assumes the flag exists and is local evaluation mode. For * local evaluation, fallback order goes: @@ -627,7 +636,7 @@ export class ExperimentClient implements Client { this.debug(`[Experiment] Fetch all: retry=${retry}`); - // Proactively cancel retries if active in order to avoid unecessary API + // Proactively cancel retries if active in order to avoid unnecessary API // requests. A new failure will restart the retries. if (retry) { this.stopRetries(); @@ -677,6 +686,7 @@ export class ExperimentClient implements Client { } catch (e) { console.warn('Failed to store flags in localStorage', e); } + this.mergeInitialFlagsWithStorage(); } private async storeVariants( diff --git a/packages/experiment-browser/test/client.test.ts b/packages/experiment-browser/test/client.test.ts index 6f9a63c5..a83d653a 100644 --- a/packages/experiment-browser/test/client.test.ts +++ b/packages/experiment-browser/test/client.test.ts @@ -1047,4 +1047,39 @@ describe('start', () => { await client.start(); expect(fetchSpy).toBeCalledTimes(0); }); + + test('initial flags', async () => { + // Flag, sdk-ci-test-local is modified to always return off + const client = new ExperimentClient(API_KEY, { + fetchOnStart: false, + initialFlags: ` + [ + {"key":"sdk-ci-test-local","metadata":{"deployed":true,"evaluationMode":"local","flagType":"release","flagVersion":1},"segments":[{"metadata":{"segmentName":"All Other Users"},"variant":"off"}],"variants":{"off":{"key":"off","metadata":{"default":true}},"on":{"key":"on","value":"on"}}}, + {"key":"sdk-ci-test-local-2","metadata":{"deployed":true,"evaluationMode":"local","flagType":"release","flagVersion":1},"segments":[{"metadata":{"segmentName":"All Other Users"},"variant":"on"}],"variants":{"off":{"key":"off","metadata":{"default":true}},"on":{"key":"on","value":"on"}}} + ] + `.trim(), + }); + const user: ExperimentUser = { user_id: 'user_id', device_id: 'device_id' }; + client.setUser(user); + let variant = client.variant('sdk-ci-test-local'); + let variant2 = client.variant('sdk-ci-test-local-2'); + expect(variant.key).toEqual('off'); + expect(variant2.key).toEqual('on'); + // Call start to update the flag, overwrites the initial flag to return on + await client.start(user); + variant = client.variant('sdk-ci-test-local'); + variant2 = client.variant('sdk-ci-test-local-2'); + expect(variant.key).toEqual('on'); + expect(variant2.key).toEqual('on'); + // Initialize a second client with the same storage to simulate an app restart + const client2 = new ExperimentClient(API_KEY, { + fetchOnStart: false, + }); + client2.setUser(user); + // Storage flag should take precedent over initial flag + variant = client.variant('sdk-ci-test-local'); + variant2 = client.variant('sdk-ci-test-local-2'); + expect(variant.key).toEqual('on'); + expect(variant2.key).toEqual('on'); + }); });