From 0988d3c0cfba5211f917404491f2909b6a7d9698 Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Thu, 30 Nov 2023 10:43:07 -0800 Subject: [PATCH 1/8] feat: support bootstrapping initial local eval flags --- packages/experiment-browser/src/config.ts | 7 +++++ .../src/experimentClient.ts | 13 ++++++++ .../experiment-browser/test/client.test.ts | 31 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/packages/experiment-browser/src/config.ts b/packages/experiment-browser/src/config.ts index a9e3d2d2..bcdc566e 100644 --- a/packages/experiment-browser/src/config.ts +++ b/packages/experiment-browser/src/config.ts @@ -29,6 +29,11 @@ export interface ExperimentConfig { */ fallbackVariant?: Variant; + /** + * Initial values for flags. + */ + initialFlags?: string; + /** * Initial values for variants. This is useful for bootstrapping the * client with fallbacks and values evaluated from server-side rendering. @@ -144,6 +149,7 @@ export interface ExperimentConfig { | **instanceName** | `$default_instance` | | **fallbackVariant** | `null` | | **initialVariants** | `null` | + | **initialFlags** | `null` | | **source** | `Source.LocalStorage` | | **serverUrl** | `"https://api.lab.amplitude.com"` | | **flagsServerUrl** | `"https://flag.lab.amplitude.com"` | @@ -166,6 +172,7 @@ export const Defaults: ExperimentConfig = { instanceName: '$default_instance', fallbackVariant: {}, initialVariants: {}, + initialFlags: null, 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..9d8fb8be 100644 --- a/packages/experiment-browser/src/experimentClient.ts +++ b/packages/experiment-browser/src/experimentClient.ts @@ -155,6 +155,7 @@ export class ExperimentClient implements Client { } catch (e) { console.warn('Failed to load flags and variants from localStorage', e); } + this.mergeInitialFlagsWithStorage(); } /** @@ -393,6 +394,17 @@ export class ExperimentClient implements Client { return this; } + private mergeInitialFlagsWithStorage(): void { + if (this.config.initialFlags) { + const initialFlags = JSON.parse(this.config.initialFlags); + for (const key in initialFlags) { + if (!this.flags.get(initialFlags[key].key)) { + this.flags.put(initialFlags[key].key, initialFlags[key]); + } + } + } + } + private evaluate(flagKeys?: string[]): Variants { const user = this.addContext(this.user); const flags = topologicalSort(this.flags.getAll(), flagKeys); @@ -677,6 +689,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..b289a574 100644 --- a/packages/experiment-browser/test/client.test.ts +++ b/packages/experiment-browser/test/client.test.ts @@ -1047,4 +1047,35 @@ describe('start', () => { await client.start(); expect(fetchSpy).toBeCalledTimes(0); }); + + test('initial flags', async () => { + 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'); + 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'); + const client2 = new ExperimentClient(API_KEY, { + fetchOnStart: false, + }); + client2.setUser(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'); + }); }); From b24ca9ac759db84f16cc94bf7d0cd88847b81ffb Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Thu, 30 Nov 2023 10:54:12 -0800 Subject: [PATCH 2/8] add comments --- packages/experiment-browser/test/client.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/experiment-browser/test/client.test.ts b/packages/experiment-browser/test/client.test.ts index b289a574..a83d653a 100644 --- a/packages/experiment-browser/test/client.test.ts +++ b/packages/experiment-browser/test/client.test.ts @@ -1049,6 +1049,7 @@ describe('start', () => { }); test('initial flags', async () => { + // Flag, sdk-ci-test-local is modified to always return off const client = new ExperimentClient(API_KEY, { fetchOnStart: false, initialFlags: ` @@ -1064,15 +1065,18 @@ describe('start', () => { 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'); From fe0520054f3b64b71d5c3cb03a93c784b177b10d Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Thu, 30 Nov 2023 13:43:42 -0800 Subject: [PATCH 3/8] update desc --- packages/experiment-browser/src/config.ts | 25 ++++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/experiment-browser/src/config.ts b/packages/experiment-browser/src/config.ts index bcdc566e..4e978889 100644 --- a/packages/experiment-browser/src/config.ts +++ b/packages/experiment-browser/src/config.ts @@ -1,10 +1,10 @@ -import { FetchHttpClient } from './transport/http'; -import { ExperimentAnalyticsProvider } from './types/analytics'; -import { ExposureTrackingProvider } from './types/exposure'; -import { ExperimentUserProvider } from './types/provider'; -import { Source } from './types/source'; -import { HttpClient } from './types/transport'; -import { Variant, Variants } from './types/variant'; +import {FetchHttpClient} from './transport/http'; +import {ExperimentAnalyticsProvider} from './types/analytics'; +import {ExposureTrackingProvider} from './types/exposure'; +import {ExperimentUserProvider} from './types/provider'; +import {Source} from './types/source'; +import {HttpClient} from './types/transport'; +import {Variant, Variants} from './types/variant'; /** * @category Configuration @@ -29,11 +29,6 @@ export interface ExperimentConfig { */ fallbackVariant?: Variant; - /** - * Initial values for flags. - */ - initialFlags?: string; - /** * Initial values for variants. This is useful for bootstrapping the * client with fallbacks and values evaluated from server-side rendering. @@ -41,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 From 8f2aed283c8a45e79d1e8602b3767fb105c29855 Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Thu, 30 Nov 2023 15:33:20 -0800 Subject: [PATCH 4/8] style: fix lint --- packages/experiment-browser/src/config.ts | 14 +++++++------- .../experiment-browser/src/experimentClient.ts | 1 - 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/experiment-browser/src/config.ts b/packages/experiment-browser/src/config.ts index 4e978889..4dd838b9 100644 --- a/packages/experiment-browser/src/config.ts +++ b/packages/experiment-browser/src/config.ts @@ -1,10 +1,10 @@ -import {FetchHttpClient} from './transport/http'; -import {ExperimentAnalyticsProvider} from './types/analytics'; -import {ExposureTrackingProvider} from './types/exposure'; -import {ExperimentUserProvider} from './types/provider'; -import {Source} from './types/source'; -import {HttpClient} from './types/transport'; -import {Variant, Variants} from './types/variant'; +import { FetchHttpClient } from './transport/http'; +import { ExperimentAnalyticsProvider } from './types/analytics'; +import { ExposureTrackingProvider } from './types/exposure'; +import { ExperimentUserProvider } from './types/provider'; +import { Source } from './types/source'; +import { HttpClient } from './types/transport'; +import { Variant, Variants } from './types/variant'; /** * @category Configuration diff --git a/packages/experiment-browser/src/experimentClient.ts b/packages/experiment-browser/src/experimentClient.ts index 9d8fb8be..81b984d0 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 { From 45040543a22c5c3eac0a7d12124c54dcce95ccd3 Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Fri, 1 Dec 2023 15:00:05 -0800 Subject: [PATCH 5/8] Update to comments --- packages/experiment-browser/src/config.ts | 4 ++-- packages/experiment-browser/src/experimentClient.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/experiment-browser/src/config.ts b/packages/experiment-browser/src/config.ts index 4dd838b9..2094693a 100644 --- a/packages/experiment-browser/src/config.ts +++ b/packages/experiment-browser/src/config.ts @@ -150,7 +150,7 @@ export interface ExperimentConfig { | **instanceName** | `$default_instance` | | **fallbackVariant** | `null` | | **initialVariants** | `null` | - | **initialFlags** | `null` | + | **initialFlags** | `undefined` | | **source** | `Source.LocalStorage` | | **serverUrl** | `"https://api.lab.amplitude.com"` | | **flagsServerUrl** | `"https://flag.lab.amplitude.com"` | @@ -173,7 +173,7 @@ export const Defaults: ExperimentConfig = { instanceName: '$default_instance', fallbackVariant: {}, initialVariants: {}, - initialFlags: null, + 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 81b984d0..5dd72f2d 100644 --- a/packages/experiment-browser/src/experimentClient.ts +++ b/packages/experiment-browser/src/experimentClient.ts @@ -396,9 +396,9 @@ export class ExperimentClient implements Client { private mergeInitialFlagsWithStorage(): void { if (this.config.initialFlags) { const initialFlags = JSON.parse(this.config.initialFlags); - for (const key in initialFlags) { - if (!this.flags.get(initialFlags[key].key)) { - this.flags.put(initialFlags[key].key, initialFlags[key]); + for (const flag of initialFlags) { + if (!this.flags.get(flag.key)) { + this.flags.put(flag.key, flag); } } } From f73a02f0687f76b24951f43f3ab20fa53cf6ae7d Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Fri, 1 Dec 2023 15:02:28 -0800 Subject: [PATCH 6/8] nit: simplify code --- packages/experiment-browser/src/experimentClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/experiment-browser/src/experimentClient.ts b/packages/experiment-browser/src/experimentClient.ts index 5dd72f2d..55d9fc81 100644 --- a/packages/experiment-browser/src/experimentClient.ts +++ b/packages/experiment-browser/src/experimentClient.ts @@ -396,11 +396,11 @@ export class ExperimentClient implements Client { private mergeInitialFlagsWithStorage(): void { if (this.config.initialFlags) { const initialFlags = JSON.parse(this.config.initialFlags); - for (const flag of initialFlags) { + initialFlags.forEach((flag: EvaluationFlag) => { if (!this.flags.get(flag.key)) { this.flags.put(flag.key, flag); } - } + }); } } From 3b91094532658fcd3a977a535d0546be912afd4d Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Fri, 1 Dec 2023 15:14:06 -0800 Subject: [PATCH 7/8] remove comment --- packages/experiment-browser/src/experimentClient.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/experiment-browser/src/experimentClient.ts b/packages/experiment-browser/src/experimentClient.ts index 55d9fc81..4fff8137 100644 --- a/packages/experiment-browser/src/experimentClient.ts +++ b/packages/experiment-browser/src/experimentClient.ts @@ -435,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: From bddf1ccae014f7f85e030d3b03269e07e3f7e613 Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Tue, 30 Jan 2024 14:25:32 -0800 Subject: [PATCH 8/8] nit: spelling --- packages/experiment-browser/src/experimentClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/experiment-browser/src/experimentClient.ts b/packages/experiment-browser/src/experimentClient.ts index 4fff8137..ca7cc21c 100644 --- a/packages/experiment-browser/src/experimentClient.ts +++ b/packages/experiment-browser/src/experimentClient.ts @@ -636,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();