Skip to content

Commit

Permalink
feat: fetch options with flag keys (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
bgiori authored Jul 19, 2023
1 parent 38b9b4c commit 59189a1
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 13 deletions.
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

0 comments on commit 59189a1

Please sign in to comment.