diff --git a/packages/experiment-browser/src/experimentClient.ts b/packages/experiment-browser/src/experimentClient.ts index e2e3b9ad..14725269 100644 --- a/packages/experiment-browser/src/experimentClient.ts +++ b/packages/experiment-browser/src/experimentClient.ts @@ -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'; @@ -109,6 +109,7 @@ export class ExperimentClient implements Client { */ public async fetch( user: ExperimentUser = this.user, + options?: FetchOptions, ): Promise { this.setUser(user || {}); try { @@ -116,6 +117,7 @@ export class ExperimentClient implements Client { user, this.config.fetchTimeoutMillis, this.config.retryFetchOnFailure, + options, ); } catch (e) { console.error(e); @@ -327,6 +329,7 @@ export class ExperimentClient implements Client { user: ExperimentUser, timeoutMillis: number, retry: boolean, + options?: FetchOptions, ): Promise { // Don't even try to fetch variants if API key is not set if (!this.apiKey) { @@ -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; } @@ -356,6 +359,7 @@ export class ExperimentClient implements Client { private async doFetch( user: ExperimentUser, timeoutMillis: number, + options?: FetchOptions, ): Promise { const userContext = await this.addContextOrWait(user, 10000); const encodedContext = urlSafeBase64Encode(JSON.stringify(userContext)); @@ -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, @@ -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 { + private async startRetries( + user: ExperimentUser, + options: FetchOptions, + ): Promise { this.debug('[Experiment] Retry fetch'); this.retriesBackoff = new Backoff( fetchBackoffAttempts, @@ -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); }); } diff --git a/packages/experiment-browser/src/storage/localStorage.ts b/packages/experiment-browser/src/storage/localStorage.ts index 859abf52..eb736038 100644 --- a/packages/experiment-browser/src/storage/localStorage.ts +++ b/packages/experiment-browser/src/storage/localStorage.ts @@ -31,6 +31,10 @@ export class LocalStorage implements Storage { this.map = {}; } + remove(key: string): void { + delete this.map[key]; + } + getAll(): Variants { return this.map; } diff --git a/packages/experiment-browser/src/types/client.ts b/packages/experiment-browser/src/types/client.ts index 7a8603ba..2b3f01f5 100644 --- a/packages/experiment-browser/src/types/client.ts +++ b/packages/experiment-browser/src/types/client.ts @@ -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; + fetch(user?: ExperimentUser, options?: FetchOptions): Promise; variant(key: string, fallback?: string | Variant): Variant; all(): Variants; clear(): void; diff --git a/packages/experiment-browser/src/types/storage.ts b/packages/experiment-browser/src/types/storage.ts index 11c9d1c9..5c164b6e 100644 --- a/packages/experiment-browser/src/types/storage.ts +++ b/packages/experiment-browser/src/types/storage.ts @@ -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; diff --git a/packages/experiment-browser/test/client.test.ts b/packages/experiment-browser/test/client.test.ts index ce50ea31..3c098f81 100644 --- a/packages/experiment-browser/test/client.test.ts +++ b/packages/experiment-browser/test/client.test.ts @@ -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'; @@ -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({}); +});