Skip to content

Commit c2c7937

Browse files
authored
feat: Utility class to produce IConfigurationWire instances (#241)
* chore: gather configuration-wire code * lint * feat: Configuration Helper to fetch and encode config * feat: export config helper
1 parent 0b3efbe commit c2c7937

11 files changed

+189
-14
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eppo/js-client-sdk-common",
3-
"version": "4.13.5",
3+
"version": "4.14.0",
44
"description": "Common library for Eppo JavaScript SDKs (web, react native, and node)",
55
"main": "dist/index.js",
66
"files": [

src/client/eppo-client-with-bandits.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
IConfigurationWire,
1919
IPrecomputedConfiguration,
2020
IObfuscatedPrecomputedConfigurationResponse,
21-
} from '../configuration-wire-types';
21+
} from '../configuration-wire/configuration-wire-types';
2222
import { Evaluator, FlagEvaluation } from '../evaluator';
2323
import {
2424
AllocationEvaluationCode,

src/client/eppo-client.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
IConfigurationWire,
2222
IObfuscatedPrecomputedConfigurationResponse,
2323
ObfuscatedPrecomputedConfigurationResponse,
24-
} from '../configuration-wire-types';
24+
} from '../configuration-wire/configuration-wire-types';
2525
import { MAX_EVENT_QUEUE_SIZE, DEFAULT_POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants';
2626
import { decodePrecomputedFlag } from '../decoding';
2727
import { Flag, ObfuscatedFlag, VariationType, FormatEnum, Variation } from '../interfaces';

src/client/eppo-client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
IConfigurationWire,
2323
IPrecomputedConfiguration,
2424
PrecomputedConfiguration,
25-
} from '../configuration-wire-types';
25+
} from '../configuration-wire/configuration-wire-types';
2626
import {
2727
DEFAULT_INITIAL_CONFIG_REQUEST_RETRIES,
2828
DEFAULT_POLL_CONFIG_REQUEST_RETRIES,

src/client/eppo-precomputed-client.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
} from '../attributes';
1414
import { IConfigurationStore, ISyncStore } from '../configuration-store/configuration-store';
1515
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
16-
import { IPrecomputedConfigurationResponse } from '../configuration-wire-types';
16+
import { IPrecomputedConfigurationResponse } from '../configuration-wire/configuration-wire-types';
1717
import { DEFAULT_POLL_INTERVAL_MS, MAX_EVENT_QUEUE_SIZE, POLL_JITTER_PCT } from '../constants';
1818
import FetchHttpClient from '../http-client';
1919
import {
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { IBanditParametersResponse, IUniversalFlagConfigResponse } from '../http-client';
2+
import { FormatEnum } from '../interfaces';
3+
import { getMD5Hash } from '../obfuscation';
4+
5+
import { ConfigurationWireHelper } from './configuration-wire-helper';
6+
7+
const TEST_BASE_URL = 'https://us-central1-eppo-qa.cloudfunctions.net/serveGitHubRacTestFile';
8+
const DUMMY_SDK_KEY = 'dummy-sdk-key';
9+
10+
// This SDK causes the cloud endpoint below to serve the UFC test file with bandit flags.
11+
const BANDIT_SDK_KEY = 'this-key-serves-bandits';
12+
13+
describe('ConfigurationWireHelper', () => {
14+
describe('getBootstrapConfigurationFromApi', () => {
15+
it('should fetch obfuscated flags with android SDK', async () => {
16+
const helper = ConfigurationWireHelper.build(DUMMY_SDK_KEY, {
17+
sdkName: 'android',
18+
sdkVersion: '4.0.0',
19+
baseUrl: TEST_BASE_URL,
20+
});
21+
22+
const wirePacket = await helper.fetchBootstrapConfiguration();
23+
24+
expect(wirePacket.version).toBe(1);
25+
expect(wirePacket.config).toBeDefined();
26+
27+
if (!wirePacket.config) {
28+
throw new Error('Flag config not present in ConfigurationWire');
29+
}
30+
31+
const configResponse = JSON.parse(wirePacket.config.response) as IUniversalFlagConfigResponse;
32+
expect(configResponse.format).toBe(FormatEnum.CLIENT);
33+
expect(configResponse.flags).toBeDefined();
34+
expect(Object.keys(configResponse.flags).length).toBeGreaterThan(1);
35+
expect(Object.keys(configResponse.flags)).toHaveLength(19);
36+
37+
const testFlagKey = getMD5Hash('numeric_flag');
38+
expect(Object.keys(configResponse.flags)).toContain(testFlagKey);
39+
40+
// No bandits.
41+
expect(configResponse.banditReferences).toBeUndefined();
42+
expect(wirePacket.bandits).toBeUndefined();
43+
});
44+
45+
it('should fetch flags and bandits for node-server SDK', async () => {
46+
const helper = ConfigurationWireHelper.build(BANDIT_SDK_KEY, {
47+
sdkName: 'node-server',
48+
sdkVersion: '4.0.0',
49+
baseUrl: TEST_BASE_URL,
50+
});
51+
52+
const wirePacket = await helper.fetchBootstrapConfiguration();
53+
54+
expect(wirePacket.version).toBe(1);
55+
expect(wirePacket.config).toBeDefined();
56+
57+
if (!wirePacket.config) {
58+
throw new Error('Flag config not present in ConfigurationWire');
59+
}
60+
61+
const configResponse = JSON.parse(wirePacket.config.response) as IUniversalFlagConfigResponse;
62+
expect(configResponse.format).toBe(FormatEnum.SERVER);
63+
expect(configResponse.flags).toBeDefined();
64+
expect(configResponse.banditReferences).toBeDefined();
65+
expect(Object.keys(configResponse.flags)).toContain('banner_bandit_flag');
66+
expect(Object.keys(configResponse.flags)).toContain('car_bandit_flag');
67+
68+
expect(wirePacket.bandits).toBeDefined();
69+
const banditResponse = JSON.parse(
70+
wirePacket.bandits?.response ?? '',
71+
) as IBanditParametersResponse;
72+
expect(Object.keys(banditResponse.bandits).length).toBeGreaterThan(1);
73+
expect(Object.keys(banditResponse.bandits)).toContain('banner_bandit');
74+
expect(Object.keys(banditResponse.bandits)).toContain('car_bandit');
75+
});
76+
77+
it('should include fetchedAt timestamps', async () => {
78+
const helper = ConfigurationWireHelper.build(BANDIT_SDK_KEY, {
79+
sdkName: 'android',
80+
sdkVersion: '4.0.0',
81+
baseUrl: TEST_BASE_URL,
82+
});
83+
84+
const wirePacket = await helper.fetchBootstrapConfiguration();
85+
86+
if (!wirePacket.config) {
87+
throw new Error('Flag config not present in ConfigurationWire');
88+
}
89+
if (!wirePacket.bandits) {
90+
throw new Error('Bandit config not present in ConfigurationWire');
91+
}
92+
93+
expect(wirePacket.config.fetchedAt).toBeDefined();
94+
expect(Date.parse(wirePacket.config.fetchedAt ?? '')).not.toBeNaN();
95+
expect(Date.parse(wirePacket.bandits.fetchedAt ?? '')).not.toBeNaN();
96+
});
97+
});
98+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import ApiEndpoints from '../api-endpoints';
2+
import FetchHttpClient, {
3+
IBanditParametersResponse,
4+
IHttpClient,
5+
IUniversalFlagConfigResponse,
6+
} from '../http-client';
7+
8+
import { ConfigurationWireV1, IConfigurationWire } from './configuration-wire-types';
9+
10+
export type SdkOptions = {
11+
sdkName: string;
12+
sdkVersion: string;
13+
baseUrl?: string;
14+
};
15+
16+
/**
17+
* Helper class for fetching and converting configuration from the Eppo API(s).
18+
*/
19+
export class ConfigurationWireHelper {
20+
private httpClient: IHttpClient;
21+
22+
/**
23+
* Build a new ConfigurationHelper for the target SDK Key.
24+
* @param sdkKey
25+
*/
26+
public static build(
27+
sdkKey: string,
28+
opts: SdkOptions = { sdkName: 'android', sdkVersion: '4.0.0' },
29+
) {
30+
const { sdkName, sdkVersion, baseUrl } = opts;
31+
return new ConfigurationWireHelper(sdkKey, sdkName, sdkVersion, baseUrl);
32+
}
33+
34+
private constructor(
35+
sdkKey: string,
36+
targetSdkName = 'android',
37+
targetSdkVersion = '4.0.0',
38+
baseUrl?: string,
39+
) {
40+
const queryParams = {
41+
sdkName: targetSdkName,
42+
sdkVersion: targetSdkVersion,
43+
apiKey: sdkKey,
44+
};
45+
const apiEndpoints = new ApiEndpoints({
46+
baseUrl,
47+
queryParams,
48+
});
49+
50+
this.httpClient = new FetchHttpClient(apiEndpoints, 5000);
51+
}
52+
53+
/**
54+
* Fetches configuration data from the API and build a Bootstrap Configuration (aka an `IConfigurationWire` object).
55+
* The IConfigurationWire instance can be used to bootstrap some SDKs.
56+
*/
57+
public async fetchBootstrapConfiguration(): Promise<IConfigurationWire> {
58+
// Get the configs
59+
let banditResponse: IBanditParametersResponse | undefined;
60+
const configResponse: IUniversalFlagConfigResponse | undefined =
61+
await this.httpClient.getUniversalFlagConfiguration();
62+
63+
if (!configResponse?.flags) {
64+
console.warn('Unable to fetch configuration, returning empty configuration');
65+
return Promise.resolve(ConfigurationWireV1.empty());
66+
}
67+
68+
const flagsHaveBandits = Object.keys(configResponse.banditReferences ?? {}).length > 0;
69+
if (flagsHaveBandits) {
70+
banditResponse = await this.httpClient.getBanditParameters();
71+
}
72+
73+
return ConfigurationWireV1.fromResponses(configResponse, banditResponse);
74+
}
75+
}

src/configuration-wire-types.spec.ts renamed to src/configuration-wire/configuration-wire-types.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import {
22
MOCK_BANDIT_MODELS_RESPONSE_FILE,
33
MOCK_FLAGS_WITH_BANDITS_RESPONSE_FILE,
44
readMockUFCResponse,
5-
} from '../test/testHelpers';
5+
} from '../../test/testHelpers';
6+
import { IUniversalFlagConfigResponse, IBanditParametersResponse } from '../http-client';
7+
import { FormatEnum } from '../interfaces';
68

79
import { ConfigurationWireV1, deflateResponse, inflateResponse } from './configuration-wire-types';
8-
import { IUniversalFlagConfigResponse, IBanditParametersResponse } from './http-client';
9-
import { FormatEnum } from './interfaces';
1010

1111
describe('Response String Type Safety', () => {
1212
const mockFlagConfig: IUniversalFlagConfigResponse = readMockUFCResponse(

src/configuration-wire-types.ts renamed to src/configuration-wire/configuration-wire-types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { IUniversalFlagConfigResponse, IBanditParametersResponse } from './http-client';
1+
import { IUniversalFlagConfigResponse, IBanditParametersResponse } from '../http-client';
22
import {
33
Environment,
44
FormatEnum,
55
IObfuscatedPrecomputedBandit,
66
IPrecomputedBandit,
77
PrecomputedFlag,
8-
} from './interfaces';
9-
import { obfuscatePrecomputedBanditMap, obfuscatePrecomputedFlags } from './obfuscation';
10-
import { ContextAttributes, FlagKey, HashedFlagKey } from './types';
8+
} from '../interfaces';
9+
import { obfuscatePrecomputedBanditMap, obfuscatePrecomputedFlags } from '../obfuscation';
10+
import { ContextAttributes, FlagKey, HashedFlagKey } from '../types';
1111

1212
// Base interface for all configuration responses
1313
interface IBasePrecomputedConfigurationResponse {

src/http-client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import ApiEndpoints from './api-endpoints';
2-
import { IObfuscatedPrecomputedConfigurationResponse } from './configuration-wire-types';
2+
import { IObfuscatedPrecomputedConfigurationResponse } from './configuration-wire/configuration-wire-types';
33
import {
44
BanditParameters,
55
BanditReference,

0 commit comments

Comments
 (0)