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
57 changes: 57 additions & 0 deletions __tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { randomString } from '../src/util/randomstring';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { ExposureTrackingProvider } from '../src/types/exposure';
import { Exposure } from '../lib/typescript';
import { FetchOptions } from '../src/types/client';

const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));

Expand Down Expand Up @@ -392,3 +393,59 @@ test('ExperimentClient.variant experiment key passed from variant to exposure',
client.variant('flagKey');
expect(didTrack).toEqual(true);
});

const flagKeysTestVariantPartial = {
'sdk-ci-test': serverVariant,
};
const flagKeysTestVariants = {
'sdk-ci-test-2': { payload: undefined, value: 'on', expKey: undefined },
'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, {
httpClient: new TestHttpClient(),
});
const option: FetchOptions = { flagKeys: ['sdk-ci-test'] };
await client.fetch(testUser, option);
const variants = client.all();
expect(variants).toEqual(flagKeysTestVariantPartial);
});

test('ExperimentClient.fetch without fetch options, should return all variants', async () => {
const client = new ExperimentClient(API_KEY, {
debug: true,
httpClient: new TestHttpClient({
status: 200,
body: JSON.stringify({
'sdk-ci-test': { key: 'on', payload: 'payload' },
'sdk-ci-test-2': { key: 'on' },
}),
}),
});
await client.fetch(testUser);
const variant = client.all();
console.log(variant);
expect(variant).toEqual(flagKeysTestVariants);
});

test('ExperimentClient.fetch with not exist flagKeys in fetch options', async () => {
const client = new ExperimentClient(API_KEY, {
httpClient: new TestHttpClient({ status: 200, body: '{}' }),
});
const option: FetchOptions = { flagKeys: ['123'] };
await client.fetch(testUser, option);
const variant = client.all();
expect(variant).toEqual({});
});

test('existing storage variant removed when fetch without flag keys response stored', async () => {
const client = new ExperimentClient(API_KEY, {
httpClient: new TestHttpClient(),
});
// @ts-ignore
client.storage.put('not-fetched-variant', { value: 'on' });
await client.fetch(testUser);
const variant = client.variant('not-fetched-variant');
expect(variant).toEqual({});
});
45 changes: 33 additions & 12 deletions src/experimentClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { version as PACKAGE_VERSION } from './gen/version';
import { ExperimentConfig, Defaults } from './types/config';
import { ConnectorUserProvider } from './integration/connector';
import { LocalStorage } from './storage/localStorage';
import { Client } from './types/client';
import { Client, FetchOptions } from './types/client';
import { ExposureTrackingProvider } from './types/exposure';
import { isFallback, Source, VariantSource } from './types/source';
import { Storage } from './types/storage';
Expand Down Expand Up @@ -91,19 +91,22 @@ export class ExperimentClient implements Client {
* from the server, you generally do not need to call `fetch`.
*
* @param user The user to fetch variants for.
* @param options The {@link FetchOptions} for this specific request.
* @returns Promise that resolves when the request for variants completes.
* @see ExperimentUser
* @see ExperimentUserProvider
*/
public async fetch(
user: ExperimentUser = this.user
user: ExperimentUser = this.user,
options?: FetchOptions
): Promise<ExperimentClient> {
this.setUser(user || {});
try {
await this.fetchInternal(
user,
this.config.fetchTimeoutMillis,
this.config.retryFetchOnFailure
this.config.retryFetchOnFailure,
options
);
} catch (e) {
console.error(e);
Expand Down Expand Up @@ -313,7 +316,8 @@ export class ExperimentClient implements Client {
private async fetchInternal(
user: ExperimentUser,
timeoutMillis: number,
retry: boolean
retry: boolean,
options?: FetchOptions
): Promise<Variants> {
// Don't even try to fetch variants if API key is not set
if (!this.apiKey) {
Expand All @@ -329,20 +333,21 @@ 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;
}
}

private async doFetch(
user: ExperimentUser,
timeoutMillis: number
timeoutMillis: number,
options?: FetchOptions
): Promise<Variants> {
const userContext = await this.addContextOrWait(user, 1000);
const encodedContext = urlSafeBase64Encode(JSON.stringify(userContext));
Expand All @@ -355,6 +360,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 @@ -384,16 +394,27 @@ 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 : [];
if (failedFlagKeys.length === 0) {
this.storage.clear();
}
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 @@ -402,7 +423,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 src/storage/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export class LocalStorage implements Storage {
this.map = {};
}

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

getAll(): Variants {
return this.map;
}
Expand Down
12 changes: 11 additions & 1 deletion src/types/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Variant, Variants } from './variant';
* @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 All @@ -23,3 +23,13 @@ export interface Client {
*/
setUserProvider(userProvider: ExperimentUserProvider): Client;
}

/**
* Options to modify the behavior of a remote evaluation fetch request.
*/
export type FetchOptions = {
/**
* Specific flag keys to evaluate and set variants for.
*/
flagKeys?: string[];
};
1 change: 1 addition & 0 deletions 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