Skip to content

Commit 9d35ea9

Browse files
committed
merge main
2 parents 785da98 + 226120e commit 9d35ea9

File tree

11 files changed

+300
-27
lines changed

11 files changed

+300
-27
lines changed

.eslintrc.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module.exports = {
33
env: {
44
es6: true,
55
jest: true,
6+
node: true,
67
},
78
extends: [
89
'eslint:recommended',
@@ -95,5 +96,11 @@ module.exports = {
9596
'no-restricted-globals': 'off',
9697
},
9798
},
99+
{
100+
files: ['src/tools/**/*.ts'],
101+
rules: {
102+
'no-restricted-globals': 'off',
103+
},
104+
},
98105
],
99106
};

README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,78 @@ When publishing releases, the following rules apply:
5050
**Note**: The release will not be published if:
5151
- A pre-release is marked as "latest"
5252
- A pre-release label is used without checking "Set as pre-release"
53+
54+
## Tools
55+
56+
### Bootstrap Configuration
57+
58+
You can generate a bootstrap configuration string from either the command line or programmatically via the
59+
ConfigurationWireHelper class.
60+
61+
The tool allows you to specify the target SDK this configuration will be used on. It is important to correctly specify
62+
the intended SDK, as this determines whether the configuration is obfuscated (for client SDKs) or not (for server SDKs).
63+
64+
#### Command Line Usage
65+
66+
**Install as a project dependency:**
67+
```bash
68+
# Install as a dependency
69+
npm install --save-dev @eppo/js-client-sdk-common
70+
71+
# or, with yarn
72+
yarn add --dev @eppo/js-client-sdk-common
73+
```
74+
75+
Common usage examples:
76+
```bash
77+
# Basic usage
78+
yarn bootstrap-config --key <sdkKey> --output bootstrap-config.json
79+
80+
# With custom SDK name (default is 'js-client-sdk')
81+
yarn bootstrap-config --key <sdkKey> --sdk android
82+
83+
# With custom base URL
84+
yarn bootstrap-config --key <sdkKey> --base-url https://api.custom-domain.com
85+
86+
# Output configuration to stdout
87+
yarn bootstrap-config --key <sdkKey>
88+
89+
# Show help
90+
yarn bootstrap-config --help
91+
```
92+
93+
The tool accepts the following arguments:
94+
- `--key, -k`: SDK key (required, can also be set via EPPO_SDK_KEY environment variable)
95+
- `--sdk`: Target SDK name (default: 'js-client-sdk')
96+
- `--base-url`: Custom base URL for the API
97+
- `--output, -o`: Output file path (if not specified, outputs to console)
98+
- `--help, -h`: Show help
99+
100+
#### Programmatic Usage
101+
```typescript
102+
import { ConfigurationHelper } from '@eppo/js-client-sdk-common';
103+
104+
async function getBootstrapConfig() {
105+
// Initialize the helper
106+
const helper = ConfigurationHelper.build(
107+
'your-sdk-key',
108+
{
109+
sdkName: 'android', // optional: target SDK name (default: 'js-client-sdk')
110+
baseUrl: 'https://api.custom-domain.com', // optional: custom base URL
111+
});
112+
113+
// Fetch the configuration
114+
const config = await helper.fetchConfiguration();
115+
const configString = config.toString();
116+
117+
// You are responsible to transport this string to the client
118+
const clientInitialData = {eppoConfig: eppoConfigString};
119+
120+
// Client-side
121+
const client = getInstance();
122+
const initialConfig = configurationFromString(clientInitialData.eppoConfig);
123+
client.setInitialConfig(configurationFromString(configString));
124+
}
125+
```
126+
127+
The tool will output a JSON string containing the configuration wire format that can be used to bootstrap Eppo SDKs.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"typecheck": "tsc",
2828
"test": "yarn test:unit",
2929
"test:unit": "NODE_ENV=test jest '.*\\.spec\\.ts'",
30-
"obfuscate-mock-ufc": "ts-node test/writeObfuscatedMockUFC"
30+
"obfuscate-mock-ufc": "ts-node test/writeObfuscatedMockUFC",
31+
"bootstrap-config": "ts-node src/tools/get-bootstrap-config"
3132
},
3233
"jsdelivr": "dist/eppo-sdk.js",
3334
"repository": {
@@ -76,7 +77,8 @@
7677
"pino": "^9.5.0",
7778
"semver": "^7.5.4",
7879
"spark-md5": "^3.0.2",
79-
"uuid": "^11.0.5"
80+
"uuid": "^11.0.5",
81+
"yargs": "^17.7.2"
8082
},
8183
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
8284
}

src/configuration-requestor.spec.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,5 +655,72 @@ describe('ConfigurationRequestor', () => {
655655
expect(Object.keys(config.getBandits())).toEqual(['test-bandit']);
656656
});
657657
});
658+
659+
describe('IConfigurationStore updates', () => {
660+
it('should update configuration when stores are changed', async () => {
661+
// Create new stores
662+
const newFlagStore = new MemoryOnlyConfigurationStore<Flag>();
663+
const newBanditVariationStore = new MemoryOnlyConfigurationStore<BanditVariation[]>();
664+
const newBanditModelStore = new MemoryOnlyConfigurationStore<BanditParameters>();
665+
666+
// Add a test flag to the new flag store
667+
await newFlagStore.setEntries({
668+
'test-flag': {
669+
key: 'test-flag',
670+
enabled: true,
671+
variationType: VariationType.STRING,
672+
variations: {
673+
control: { key: 'control', value: 'control-value' },
674+
treatment: { key: 'treatment', value: 'treatment-value' },
675+
},
676+
allocations: [
677+
{
678+
key: 'allocation-1',
679+
rules: [],
680+
splits: [
681+
{
682+
shards: [{ salt: '', ranges: [{ start: 0, end: 10000 }] }],
683+
variationKey: 'treatment',
684+
},
685+
],
686+
doLog: true,
687+
},
688+
],
689+
totalShards: 10000,
690+
},
691+
});
692+
693+
await newBanditModelStore.setEntries({
694+
'test-bandit': {
695+
banditKey: 'test-bandt',
696+
modelVersion: 'v123',
697+
modelName: 'falcon',
698+
modelData: {
699+
coefficients: {},
700+
gamma: 0,
701+
defaultActionScore: 0,
702+
actionProbabilityFloor: 0,
703+
},
704+
},
705+
});
706+
707+
// Get the configuration and verify it has the test flag
708+
const initialConfig = configurationRequestor.getConfiguration();
709+
expect(initialConfig.getFlagKeys()).toEqual([]);
710+
expect(Object.keys(initialConfig.getBandits())).toEqual([]);
711+
712+
// Update the stores
713+
configurationRequestor.setConfigurationStores(
714+
newFlagStore,
715+
newBanditVariationStore,
716+
newBanditModelStore,
717+
);
718+
719+
// Get the configuration and verify it has the test flag
720+
const config = configurationRequestor.getConfiguration();
721+
expect(config.getFlagKeys()).toEqual(['test-flag']);
722+
expect(Object.keys(config.getBandits())).toEqual(['test-bandit']);
723+
});
724+
});
658725
});
659726
});

src/configuration-wire/configuration-wire-helper.spec.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ describe('ConfigurationWireHelper', () => {
1919
baseUrl: TEST_BASE_URL,
2020
});
2121

22-
const wirePacket = await helper.fetchBootstrapConfiguration();
22+
const wirePacket = await helper.fetchConfiguration();
2323

2424
expect(wirePacket.version).toBe(1);
2525
expect(wirePacket.config).toBeDefined();
@@ -47,9 +47,10 @@ describe('ConfigurationWireHelper', () => {
4747
sdkName: 'node-server',
4848
sdkVersion: '4.0.0',
4949
baseUrl: TEST_BASE_URL,
50+
fetchBandits: true,
5051
});
5152

52-
const wirePacket = await helper.fetchBootstrapConfiguration();
53+
const wirePacket = await helper.fetchConfiguration();
5354

5455
expect(wirePacket.version).toBe(1);
5556
expect(wirePacket.config).toBeDefined();
@@ -76,12 +77,12 @@ describe('ConfigurationWireHelper', () => {
7677

7778
it('should include fetchedAt timestamps', async () => {
7879
const helper = ConfigurationWireHelper.build(BANDIT_SDK_KEY, {
79-
sdkName: 'android',
80-
sdkVersion: '4.0.0',
80+
sdkName: 'node-server',
8181
baseUrl: TEST_BASE_URL,
82+
fetchBandits: true,
8283
});
8384

84-
const wirePacket = await helper.fetchBootstrapConfiguration();
85+
const wirePacket = await helper.fetchConfiguration();
8586

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

src/configuration-wire/configuration-wire-helper.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import SdkTokenDecoder from '../sdk-token-decoder';
99
import { ConfigurationWireV1, IConfigurationWire } from './configuration-wire-types';
1010

1111
export type SdkOptions = {
12-
sdkName: string;
13-
sdkVersion: string;
12+
sdkName?: string;
13+
sdkVersion?: string;
1414
baseUrl?: string;
15+
fetchBandits?: boolean;
1516
};
1617

1718
/**
@@ -23,25 +24,28 @@ export class ConfigurationWireHelper {
2324
/**
2425
* Build a new ConfigurationHelper for the target SDK Key.
2526
* @param sdkKey
27+
* @param opts
2628
*/
2729
public static build(
2830
sdkKey: string,
29-
opts: SdkOptions = { sdkName: 'android', sdkVersion: '4.0.0' },
31+
opts: SdkOptions = { sdkName: 'js-client-sdk', sdkVersion: '4.0.0' },
3032
) {
31-
const { sdkName, sdkVersion, baseUrl } = opts;
32-
return new ConfigurationWireHelper(sdkKey, sdkName, sdkVersion, baseUrl);
33+
const { sdkName, sdkVersion, baseUrl, fetchBandits } = opts;
34+
return new ConfigurationWireHelper(sdkKey, sdkName, sdkVersion, baseUrl, fetchBandits);
3335
}
3436

3537
private constructor(
3638
sdkKey: string,
37-
targetSdkName = 'android',
39+
targetSdkName = 'js-client-sdk',
3840
targetSdkVersion = '4.0.0',
3941
baseUrl?: string,
42+
private readonly fetchBandits = false,
4043
) {
4144
const queryParams = {
4245
sdkName: targetSdkName,
4346
sdkVersion: targetSdkVersion,
4447
apiKey: sdkKey,
48+
sdkProxy: 'config-wire-helper',
4549
};
4650
const apiEndpoints = new ApiEndpoints({
4751
baseUrl,
@@ -56,7 +60,7 @@ export class ConfigurationWireHelper {
5660
* Fetches configuration data from the API and build a Bootstrap Configuration (aka an `IConfigurationWire` object).
5761
* The IConfigurationWire instance can be used to bootstrap some SDKs.
5862
*/
59-
public async fetchBootstrapConfiguration(): Promise<IConfigurationWire> {
63+
public async fetchConfiguration(): Promise<IConfigurationWire> {
6064
// Get the configs
6165
let banditResponse: IBanditParametersResponse | undefined;
6266
const configResponse: IUniversalFlagConfigResponse | undefined =
@@ -68,7 +72,7 @@ export class ConfigurationWireHelper {
6872
}
6973

7074
const flagsHaveBandits = Object.keys(configResponse.banditReferences ?? {}).length > 0;
71-
if (flagsHaveBandits) {
75+
if (this.fetchBandits && flagsHaveBandits) {
7276
banditResponse = await this.httpClient.getBanditParameters();
7377
}
7478

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ export class ConfigurationWireV1 implements IConfigurationWire {
203203
return inflateJsonObject(payloadString as JsonString<IConfigurationWire>);
204204
}
205205

206+
public toString(): string {
207+
return JSON.stringify(this as IConfigurationWire);
208+
}
209+
206210
public static fromResponses(
207211
flagConfig: IUniversalFlagConfigResponse,
208212
banditConfig?: IBanditParametersResponse,
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as fs from 'fs';
2+
3+
import type { CommandModule } from 'yargs';
4+
5+
import { ConfigurationWireHelper } from '../../configuration-wire/configuration-wire-helper';
6+
7+
export const bootstrapConfigCommand: CommandModule = {
8+
command: 'bootstrap-config',
9+
describe: 'Generate a bootstrap configuration string',
10+
builder: (yargs) => {
11+
return yargs.options({
12+
key: {
13+
type: 'string',
14+
description: 'SDK key',
15+
alias: 'k',
16+
default: process.env.EPPO_SDK_KEY,
17+
},
18+
sdk: {
19+
type: 'string',
20+
description: 'Target SDK name',
21+
default: 'js-client-sdk',
22+
},
23+
'base-url': {
24+
type: 'string',
25+
description: 'Base URL for the API',
26+
},
27+
output: {
28+
type: 'string',
29+
description: 'Output file path',
30+
alias: 'o',
31+
},
32+
});
33+
},
34+
handler: async (argv) => {
35+
if (!argv.key) {
36+
console.error('Error: SDK key is required');
37+
console.error('Provide it either as:');
38+
console.error('- Command line argument: --key <sdkKey> or -k <sdkKey>');
39+
console.error('- Environment variable: EPPO_SDK_KEY');
40+
process.exit(1);
41+
}
42+
43+
try {
44+
const helper = ConfigurationWireHelper.build(argv.key as string, {
45+
sdkName: argv.sdk as string,
46+
baseUrl: argv['base-url'] as string,
47+
fetchBandits: true,
48+
});
49+
const config = await helper.fetchConfiguration();
50+
51+
if (!config) {
52+
console.error('Error: Failed to fetch configuration');
53+
process.exit(1);
54+
}
55+
56+
const jsonConfig = JSON.stringify(config, null, 2);
57+
58+
if (argv.output && typeof argv.output === 'string') {
59+
fs.writeFileSync(argv.output, jsonConfig);
60+
console.log(`Configuration written to ${argv.output}`);
61+
} else {
62+
console.log('Configuration:');
63+
console.log('--------------------------------');
64+
console.log(jsonConfig);
65+
console.log('--------------------------------');
66+
}
67+
} catch (error) {
68+
console.error('Error fetching configuration:', error);
69+
process.exit(1);
70+
}
71+
},
72+
};

0 commit comments

Comments
 (0)