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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@eppo/js-client-sdk-common",
"version": "4.13.5",
"version": "4.14.0",
"description": "Common library for Eppo JavaScript SDKs (web, react native, and node)",
"main": "dist/index.js",
"files": [
Expand Down
2 changes: 1 addition & 1 deletion src/client/eppo-client-with-bandits.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
IConfigurationWire,
IPrecomputedConfiguration,
IObfuscatedPrecomputedConfigurationResponse,
} from '../configuration-wire-types';
} from '../configuration-wire/configuration-wire-types';
import { Evaluator, FlagEvaluation } from '../evaluator';
import {
AllocationEvaluationCode,
Expand Down
2 changes: 1 addition & 1 deletion src/client/eppo-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
IConfigurationWire,
IObfuscatedPrecomputedConfigurationResponse,
ObfuscatedPrecomputedConfigurationResponse,
} from '../configuration-wire-types';
} from '../configuration-wire/configuration-wire-types';
import { MAX_EVENT_QUEUE_SIZE, DEFAULT_POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants';
import { decodePrecomputedFlag } from '../decoding';
import { Flag, ObfuscatedFlag, VariationType, FormatEnum, Variation } from '../interfaces';
Expand Down
2 changes: 1 addition & 1 deletion src/client/eppo-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
IConfigurationWire,
IPrecomputedConfiguration,
PrecomputedConfiguration,
} from '../configuration-wire-types';
} from '../configuration-wire/configuration-wire-types';
import {
DEFAULT_INITIAL_CONFIG_REQUEST_RETRIES,
DEFAULT_POLL_CONFIG_REQUEST_RETRIES,
Expand Down
2 changes: 1 addition & 1 deletion src/client/eppo-precomputed-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '../attributes';
import { IConfigurationStore, ISyncStore } from '../configuration-store/configuration-store';
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
import { IPrecomputedConfigurationResponse } from '../configuration-wire-types';
import { IPrecomputedConfigurationResponse } from '../configuration-wire/configuration-wire-types';
import { DEFAULT_POLL_INTERVAL_MS, MAX_EVENT_QUEUE_SIZE, POLL_JITTER_PCT } from '../constants';
import FetchHttpClient from '../http-client';
import {
Expand Down
98 changes: 98 additions & 0 deletions src/configuration-wire/configuration-wire-helper.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { IBanditParametersResponse, IUniversalFlagConfigResponse } from '../http-client';
import { FormatEnum } from '../interfaces';
import { getMD5Hash } from '../obfuscation';

import { ConfigurationWireHelper } from './configuration-wire-helper';

const TEST_BASE_URL = 'https://us-central1-eppo-qa.cloudfunctions.net/serveGitHubRacTestFile';
const DUMMY_SDK_KEY = 'dummy-sdk-key';

// This SDK causes the cloud endpoint below to serve the UFC test file with bandit flags.
const BANDIT_SDK_KEY = 'this-key-serves-bandits';

describe('ConfigurationWireHelper', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌

describe('getBootstrapConfigurationFromApi', () => {
it('should fetch obfuscated flags with android SDK', async () => {
const helper = ConfigurationWireHelper.build(DUMMY_SDK_KEY, {
sdkName: 'android',
sdkVersion: '4.0.0',
baseUrl: TEST_BASE_URL,
});

const wirePacket = await helper.fetchBootstrapConfiguration();

expect(wirePacket.version).toBe(1);
expect(wirePacket.config).toBeDefined();

if (!wirePacket.config) {
throw new Error('Flag config not present in ConfigurationWire');
}

const configResponse = JSON.parse(wirePacket.config.response) as IUniversalFlagConfigResponse;
expect(configResponse.format).toBe(FormatEnum.CLIENT);
expect(configResponse.flags).toBeDefined();
expect(Object.keys(configResponse.flags).length).toBeGreaterThan(1);
expect(Object.keys(configResponse.flags)).toHaveLength(19);

const testFlagKey = getMD5Hash('numeric_flag');
expect(Object.keys(configResponse.flags)).toContain(testFlagKey);

// No bandits.
expect(configResponse.banditReferences).toBeUndefined();
expect(wirePacket.bandits).toBeUndefined();
});

it('should fetch flags and bandits for node-server SDK', async () => {
const helper = ConfigurationWireHelper.build(BANDIT_SDK_KEY, {
sdkName: 'node-server',
sdkVersion: '4.0.0',
baseUrl: TEST_BASE_URL,
});

const wirePacket = await helper.fetchBootstrapConfiguration();

expect(wirePacket.version).toBe(1);
expect(wirePacket.config).toBeDefined();

if (!wirePacket.config) {
throw new Error('Flag config not present in ConfigurationWire');
}

const configResponse = JSON.parse(wirePacket.config.response) as IUniversalFlagConfigResponse;
expect(configResponse.format).toBe(FormatEnum.SERVER);
expect(configResponse.flags).toBeDefined();
expect(configResponse.banditReferences).toBeDefined();
expect(Object.keys(configResponse.flags)).toContain('banner_bandit_flag');
expect(Object.keys(configResponse.flags)).toContain('car_bandit_flag');

expect(wirePacket.bandits).toBeDefined();
const banditResponse = JSON.parse(
wirePacket.bandits?.response ?? '',
) as IBanditParametersResponse;
expect(Object.keys(banditResponse.bandits).length).toBeGreaterThan(1);
expect(Object.keys(banditResponse.bandits)).toContain('banner_bandit');
expect(Object.keys(banditResponse.bandits)).toContain('car_bandit');
});

it('should include fetchedAt timestamps', async () => {
const helper = ConfigurationWireHelper.build(BANDIT_SDK_KEY, {
sdkName: 'android',
sdkVersion: '4.0.0',
baseUrl: TEST_BASE_URL,
});

const wirePacket = await helper.fetchBootstrapConfiguration();

if (!wirePacket.config) {
throw new Error('Flag config not present in ConfigurationWire');
}
if (!wirePacket.bandits) {
throw new Error('Bandit config not present in ConfigurationWire');
}

expect(wirePacket.config.fetchedAt).toBeDefined();
expect(Date.parse(wirePacket.config.fetchedAt ?? '')).not.toBeNaN();
expect(Date.parse(wirePacket.bandits.fetchedAt ?? '')).not.toBeNaN();
});
});
});
75 changes: 75 additions & 0 deletions src/configuration-wire/configuration-wire-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import ApiEndpoints from '../api-endpoints';
import FetchHttpClient, {
IBanditParametersResponse,
IHttpClient,
IUniversalFlagConfigResponse,
} from '../http-client';

import { ConfigurationWireV1, IConfigurationWire } from './configuration-wire-types';

export type SdkOptions = {
sdkName: string;
sdkVersion: string;
baseUrl?: string;
};

/**
* Helper class for fetching and converting configuration from the Eppo API(s).
*/
export class ConfigurationWireHelper {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks extremely similar to configuration fetcher. Is there a significant difference? can this be just a new configuration fetcher?

private httpClient: IHttpClient;

/**
* Build a new ConfigurationHelper for the target SDK Key.
* @param sdkKey
*/
public static build(
sdkKey: string,
opts: SdkOptions = { sdkName: 'android', sdkVersion: '4.0.0' },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any reason we're choosing these values for defaults? They seem extremely off from being default

) {
const { sdkName, sdkVersion, baseUrl } = opts;
return new ConfigurationWireHelper(sdkKey, sdkName, sdkVersion, baseUrl);
}

private constructor(
sdkKey: string,
targetSdkName = 'android',
targetSdkVersion = '4.0.0',
baseUrl?: string,
) {
const queryParams = {
sdkName: targetSdkName,
sdkVersion: targetSdkVersion,
apiKey: sdkKey,
};
const apiEndpoints = new ApiEndpoints({
baseUrl,
queryParams,
});

this.httpClient = new FetchHttpClient(apiEndpoints, 5000);
}

/**
* Fetches configuration data from the API and build a Bootstrap Configuration (aka an `IConfigurationWire` object).
* The IConfigurationWire instance can be used to bootstrap some SDKs.
*/
public async fetchBootstrapConfiguration(): Promise<IConfigurationWire> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we're calling it "bootstrap configuration" vs just "configuration"?

When I head "bootstrap configuration" I imagine: sdk key and set of urls to fetch configuration from

// Get the configs
let banditResponse: IBanditParametersResponse | undefined;
const configResponse: IUniversalFlagConfigResponse | undefined =
await this.httpClient.getUniversalFlagConfiguration();

if (!configResponse?.flags) {
console.warn('Unable to fetch configuration, returning empty configuration');
return Promise.resolve(ConfigurationWireV1.empty());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: given the function is async, I'm pretty sure you can just do this here:

return ConfigurationWireV1.empty();

}

const flagsHaveBandits = Object.keys(configResponse.banditReferences ?? {}).length > 0;
if (flagsHaveBandits) {
banditResponse = await this.httpClient.getBanditParameters();
}

return ConfigurationWireV1.fromResponses(configResponse, banditResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import {
MOCK_BANDIT_MODELS_RESPONSE_FILE,
MOCK_FLAGS_WITH_BANDITS_RESPONSE_FILE,
readMockUFCResponse,
} from '../test/testHelpers';
} from '../../test/testHelpers';
import { IUniversalFlagConfigResponse, IBanditParametersResponse } from '../http-client';
import { FormatEnum } from '../interfaces';

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

describe('Response String Type Safety', () => {
const mockFlagConfig: IUniversalFlagConfigResponse = readMockUFCResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { IUniversalFlagConfigResponse, IBanditParametersResponse } from './http-client';
import { IUniversalFlagConfigResponse, IBanditParametersResponse } from '../http-client';
import {
Environment,
FormatEnum,
IObfuscatedPrecomputedBandit,
IPrecomputedBandit,
PrecomputedFlag,
} from './interfaces';
import { obfuscatePrecomputedBanditMap, obfuscatePrecomputedFlags } from './obfuscation';
import { ContextAttributes, FlagKey, HashedFlagKey } from './types';
} from '../interfaces';
import { obfuscatePrecomputedBanditMap, obfuscatePrecomputedFlags } from '../obfuscation';
import { ContextAttributes, FlagKey, HashedFlagKey } from '../types';

// Base interface for all configuration responses
interface IBasePrecomputedConfigurationResponse {
Expand Down
2 changes: 1 addition & 1 deletion src/http-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ApiEndpoints from './api-endpoints';
import { IObfuscatedPrecomputedConfigurationResponse } from './configuration-wire-types';
import { IObfuscatedPrecomputedConfigurationResponse } from './configuration-wire/configuration-wire-types';
import {
BanditParameters,
BanditReference,
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ import {
} from './configuration-store/configuration-store';
import { HybridConfigurationStore } from './configuration-store/hybrid.store';
import { MemoryStore, MemoryOnlyConfigurationStore } from './configuration-store/memory.store';
import { ConfigurationWireHelper } from './configuration-wire/configuration-wire-helper';
import {
IConfigurationWire,
IObfuscatedPrecomputedConfigurationResponse,
IPrecomputedConfigurationResponse,
} from './configuration-wire-types';
} from './configuration-wire/configuration-wire-types';
import * as constants from './constants';
import { decodePrecomputedFlag } from './decoding';
import { EppoAssignmentLogger } from './eppo-assignment-logger';
Expand Down Expand Up @@ -155,6 +156,7 @@ export {
IPrecomputedConfigurationResponse,
PrecomputedFlag,
FlagKey,
ConfigurationWireHelper,

// Test helpers
decodePrecomputedFlag,
Expand Down
Loading