Skip to content

Commit db26eb1

Browse files
committed
feat: Configuration Helper to fetch and encode config
1 parent 4520fd2 commit db26eb1

File tree

2 files changed

+173
-0
lines changed

2 files changed

+173
-0
lines changed
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.getBootstrapConfigurationFromApi();
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.getBootstrapConfigurationFromApi();
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.getBootstrapConfigurationFromApi();
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+
* Builds an `IConfigurationWire` object from flag and bandit API responses.
55+
* The IConfigurationWire instance can be used to bootstrap some SDKs.
56+
*/
57+
public async getBootstrapConfigurationFromApi(): 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+
}

0 commit comments

Comments
 (0)