Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bootstrap initial local evaluation flags #93

Merged
merged 8 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/experiment-browser/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"` |
Expand All @@ -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',
Expand Down
18 changes: 14 additions & 4 deletions packages/experiment-browser/src/experimentClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import {
isLocalEvaluationMode,
isNullOrUndefined,
isNullUndefinedOrEmpty,
isRemoteEvaluationMode,
} from './util';
import { Backoff } from './util/backoff';
import {
Expand Down Expand Up @@ -155,6 +154,7 @@ export class ExperimentClient implements Client {
} catch (e) {
console.warn('Failed to load flags and variants from localStorage', e);
}
this.mergeInitialFlagsWithStorage();
}

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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(
Expand Down
35 changes: 35 additions & 0 deletions packages/experiment-browser/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
Loading