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 4 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** | `null` |
| **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: null,
source: Source.LocalStorage,
serverUrl: 'https://api.lab.amplitude.com',
flagsServerUrl: 'https://flag.lab.amplitude.com',
Expand Down
14 changes: 13 additions & 1 deletion 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);
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);
Expand Down Expand Up @@ -677,6 +688,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');
});
});