Skip to content
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
34 changes: 26 additions & 8 deletions packages/experiment-browser/src/experimentClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ExperimentConfig, Defaults } from './config';
import { ConnectorUserProvider } from './integration/connector';
import { LocalStorage } from './storage/localStorage';
import { exposureEvent } from './types/analytics';
import { Client } from './types/client';
import { Client, FetchOptions } from './types/client';
import { ExposureTrackingProvider } from './types/exposure';
import { ExperimentUserProvider } from './types/provider';
import { isFallback, Source, VariantSource } from './types/source';
Expand Down Expand Up @@ -109,13 +109,15 @@ export class ExperimentClient implements Client {
*/
public async fetch(
user: ExperimentUser = this.user,
options?: FetchOptions,
): Promise<ExperimentClient> {
this.setUser(user || {});
try {
await this.fetchInternal(
user,
this.config.fetchTimeoutMillis,
this.config.retryFetchOnFailure,
options,
);
} catch (e) {
console.error(e);
Expand Down Expand Up @@ -327,6 +329,7 @@ export class ExperimentClient implements Client {
user: ExperimentUser,
timeoutMillis: number,
retry: boolean,
options?: FetchOptions,
): Promise<Variants> {
// Don't even try to fetch variants if API key is not set
if (!this.apiKey) {
Expand All @@ -342,12 +345,12 @@ export class ExperimentClient implements Client {
}

try {
const variants = await this.doFetch(user, timeoutMillis);
this.storeVariants(variants);
const variants = await this.doFetch(user, timeoutMillis, options);
this.storeVariants(variants, options);
return variants;
} catch (e) {
if (retry) {
this.startRetries(user);
this.startRetries(user, options);
}
throw e;
}
Expand All @@ -356,6 +359,7 @@ export class ExperimentClient implements Client {
private async doFetch(
user: ExperimentUser,
timeoutMillis: number,
options?: FetchOptions,
): Promise<Variants> {
const userContext = await this.addContextOrWait(user, 10000);
const encodedContext = urlSafeBase64Encode(JSON.stringify(userContext));
Expand All @@ -368,6 +372,11 @@ export class ExperimentClient implements Client {
Authorization: `Api-Key ${this.apiKey}`,
'X-Amp-Exp-User': encodedContext,
};
if (options && options.flagKeys) {
headers['X-Amp-Exp-Flag-Keys'] = urlSafeBase64Encode(
JSON.stringify(options.flagKeys),
);
}
this.debug('[Experiment] Fetch variants for user: ', userContext);
const response = await this.httpClient.request(
endpoint,
Expand Down Expand Up @@ -396,16 +405,25 @@ export class ExperimentClient implements Client {
return variants;
}

private storeVariants(variants: Variants): void {
this.storage.clear();
private storeVariants(variants: Variants, options?: FetchOptions): void {
let failedFlagKeys = options && options.flagKeys ? options.flagKeys : [];

for (const key in variants) {
failedFlagKeys = failedFlagKeys.filter((flagKey) => flagKey !== key);
this.storage.put(key, variants[key]);
}

for (const key in failedFlagKeys) {
this.storage.remove(key);
}
this.storage.save();
this.debug('[Experiment] Stored variants: ', variants);
}

private async startRetries(user: ExperimentUser): Promise<void> {
private async startRetries(
user: ExperimentUser,
options: FetchOptions,
): Promise<void> {
this.debug('[Experiment] Retry fetch');
this.retriesBackoff = new Backoff(
fetchBackoffAttempts,
Expand All @@ -414,7 +432,7 @@ export class ExperimentClient implements Client {
fetchBackoffScalar,
);
this.retriesBackoff.start(async () => {
await this.fetchInternal(user, fetchBackoffTimeout, false);
await this.fetchInternal(user, fetchBackoffTimeout, false, options);
});
}

Expand Down
4 changes: 4 additions & 0 deletions packages/experiment-browser/src/storage/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export class LocalStorage implements Storage {
this.map = {};
}

remove(key: string): void {
delete this.map[key];
}

getAll(): Variants {
return this.map;
}
Expand Down
6 changes: 5 additions & 1 deletion packages/experiment-browser/src/types/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import { ExperimentUserProvider } from './provider';
import { ExperimentUser } from './user';
import { Variant, Variants } from './variant';

export type FetchOptions = {
flagKeys: string[];
};

/**
* Interface for the main client.
* @category Core Usage
*/
export interface Client {
fetch(user?: ExperimentUser): Promise<Client>;
fetch(user?: ExperimentUser, options?: FetchOptions): Promise<Client>;
variant(key: string, fallback?: string | Variant): Variant;
all(): Variants;
clear(): void;
Expand Down
1 change: 1 addition & 0 deletions packages/experiment-browser/src/types/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface Storage {
put(key: string, value: Variant): void;
get(key: string): Variant;
clear(): void;
remove(key: string): void;
getAll(): Variants;
save(): void;
load(): void;
Expand Down
36 changes: 36 additions & 0 deletions packages/experiment-browser/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { HttpClient, SimpleResponse } from 'src/types/transport';

import { ExperimentClient } from '../src/experimentClient';
import { ExperimentAnalyticsProvider } from '../src/types/analytics';
import { FetchOptions } from '../src/types/client';
import { ExposureTrackingProvider } from '../src/types/exposure';
import { ExperimentUserProvider } from '../src/types/provider';
import { Source } from '../src/types/source';
Expand Down Expand Up @@ -383,3 +384,38 @@ test('configure httpClient, success', async () => {
const v = client.variant('flag');
expect(v).toEqual({ value: 'key' });
});

// Testing with local api server, need to updated to use the production data.
const LOCAL_TEST_API = API_KEY; //'server-qz35UwzJ5akieoAdIgzM4m9MIiOLXLoz'; //'server-VY0FufBsdITI1Gv9y7RyUopLzk9m8t0n';
const local_test_user = testUser; //{ user_id: 'brian.giori@amplitude.com' };

const flagKeysTestVariantPartial = {
'sdk-ci-test': serverVariant,
};
const flagKeysTestVariants = {
'sdk-ci-test-2': { payload: undefined, value: 'on' },
'sdk-ci-test': serverVariant,
};

test('ExperimentClient.fetch with partial flag keys in fetch options, should return the fetched variant', async () => {
const client = new ExperimentClient(API_KEY, {});
const option: FetchOptions = { flagKeys: ['sdk-ci-test'] };
await client.fetch(local_test_user, option);
const variant = client.all();
expect(variant).toEqual(flagKeysTestVariantPartial);
});

test('ExperimentClient.fetch without fetch options, should return all variants', async () => {
const client = new ExperimentClient(API_KEY, {});
await client.fetch(local_test_user);
const variant = client.all();
expect(variant).toEqual(flagKeysTestVariants);
});

test('ExperimentClient.fetch with not exist flagKeys in fetch options', async () => {
const client = new ExperimentClient(LOCAL_TEST_API, {});
const option: FetchOptions = { flagKeys: ['123'] };
await client.fetch(local_test_user, option);
const variant = client.all();
expect(variant).toEqual({});
});