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
7 changes: 7 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module.exports = {
env: {
es6: true,
jest: true,
node: true,
},
extends: [
'eslint:recommended',
Expand Down Expand Up @@ -95,5 +96,11 @@ module.exports = {
'no-restricted-globals': 'off',
},
},
{
files: ['src/tools/**/*.ts'],
rules: {
'no-restricted-globals': 'off',
},
},
],
};
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,78 @@ When publishing releases, the following rules apply:
**Note**: The release will not be published if:
- A pre-release is marked as "latest"
- A pre-release label is used without checking "Set as pre-release"

## Tools

### Bootstrap Configuration

You can generate a bootstrap configuration string from either the command line or programmatically via the
ConfigurationWireHelper class.

The tool allows you to specify the target SDK this configuration will be used on. It is important to correctly specify
the intended SDK, as this determines whether the configuration is obfuscated (for client SDKs) or not (for server SDKs).

#### Command Line Usage

**Install as a project dependency:**
```bash
# Install as a dependency
npm install --save-dev @eppo/js-client-sdk-common

# or, with yarn
yarn add --dev @eppo/js-client-sdk-common
```

Common usage examples:
```bash
# Basic usage
yarn bootstrap-config --key <sdkKey> --output bootstrap-config.json

# With custom SDK name (default is 'js-client-sdk')
yarn bootstrap-config --key <sdkKey> --sdk android

# With custom base URL
yarn bootstrap-config --key <sdkKey> --base-url https://api.custom-domain.com

# Output configuration to stdout
yarn bootstrap-config --key <sdkKey>

# Show help
yarn bootstrap-config --help
```

The tool accepts the following arguments:
- `--key, -k`: SDK key (required, can also be set via EPPO_SDK_KEY environment variable)
- `--sdk`: Target SDK name (default: 'js-client-sdk')
- `--base-url`: Custom base URL for the API
- `--output, -o`: Output file path (if not specified, outputs to console)
- `--help, -h`: Show help

#### Programmatic Usage
```typescript
import { ConfigurationHelper } from '@eppo/js-client-sdk-common';

async function getBootstrapConfig() {
// Initialize the helper
const helper = ConfigurationHelper.build(
'your-sdk-key',
{
sdkName: 'android', // optional: target SDK name (default: 'js-client-sdk')
baseUrl: 'https://api.custom-domain.com', // optional: custom base URL
});

// Fetch the configuration
const config = await helper.fetchConfiguration();
const configString = config.toString();

// You are responsible to transport this string to the client
const clientInitialData = {eppoConfig: eppoConfigString};

// Client-side
const client = getInstance();
const initialConfig = configurationFromString(clientInitialData.eppoConfig);
client.setInitialConfig(configurationFromString(configString));
}
```

The tool will output a JSON string containing the configuration wire format that can be used to bootstrap Eppo SDKs.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"typecheck": "tsc",
"test": "yarn test:unit",
"test:unit": "NODE_ENV=test jest '.*\\.spec\\.ts'",
"obfuscate-mock-ufc": "ts-node test/writeObfuscatedMockUFC"
"obfuscate-mock-ufc": "ts-node test/writeObfuscatedMockUFC",
"bootstrap-config": "ts-node src/tools/get-bootstrap-config"
},
"jsdelivr": "dist/eppo-sdk.js",
"repository": {
Expand Down Expand Up @@ -76,7 +77,8 @@
"pino": "^9.5.0",
"semver": "^7.5.4",
"spark-md5": "^3.0.2",
"uuid": "^11.0.5"
"uuid": "^11.0.5",
"yargs": "^17.7.2"
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
11 changes: 6 additions & 5 deletions src/configuration-wire/configuration-wire-helper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('ConfigurationWireHelper', () => {
baseUrl: TEST_BASE_URL,
});

const wirePacket = await helper.fetchBootstrapConfiguration();
const wirePacket = await helper.fetchConfiguration();

expect(wirePacket.version).toBe(1);
expect(wirePacket.config).toBeDefined();
Expand Down Expand Up @@ -47,9 +47,10 @@ describe('ConfigurationWireHelper', () => {
sdkName: 'node-server',
sdkVersion: '4.0.0',
baseUrl: TEST_BASE_URL,
fetchBandits: true,
});

const wirePacket = await helper.fetchBootstrapConfiguration();
const wirePacket = await helper.fetchConfiguration();

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

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

const wirePacket = await helper.fetchBootstrapConfiguration();
const wirePacket = await helper.fetchConfiguration();

if (!wirePacket.config) {
throw new Error('Flag config not present in ConfigurationWire');
Expand Down
20 changes: 12 additions & 8 deletions src/configuration-wire/configuration-wire-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import SdkTokenDecoder from '../sdk-token-decoder';
import { ConfigurationWireV1, IConfigurationWire } from './configuration-wire-types';

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

/**
Expand All @@ -23,25 +24,28 @@ export class ConfigurationWireHelper {
/**
* Build a new ConfigurationHelper for the target SDK Key.
* @param sdkKey
* @param opts
*/
public static build(
sdkKey: string,
opts: SdkOptions = { sdkName: 'android', sdkVersion: '4.0.0' },
opts: SdkOptions = { sdkName: 'js-client-sdk', sdkVersion: '4.0.0' },
) {
const { sdkName, sdkVersion, baseUrl } = opts;
return new ConfigurationWireHelper(sdkKey, sdkName, sdkVersion, baseUrl);
const { sdkName, sdkVersion, baseUrl, fetchBandits } = opts;
return new ConfigurationWireHelper(sdkKey, sdkName, sdkVersion, baseUrl, fetchBandits);
}

private constructor(
sdkKey: string,
targetSdkName = 'android',
targetSdkName = 'js-client-sdk',
targetSdkVersion = '4.0.0',
baseUrl?: string,
private readonly fetchBandits = false,
) {
const queryParams = {
sdkName: targetSdkName,
sdkVersion: targetSdkVersion,
apiKey: sdkKey,
sdkProxy: 'config-wire-helper',
};
const apiEndpoints = new ApiEndpoints({
baseUrl,
Expand All @@ -56,7 +60,7 @@ export class ConfigurationWireHelper {
* 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> {
public async fetchConfiguration(): Promise<IConfigurationWire> {
// Get the configs
let banditResponse: IBanditParametersResponse | undefined;
const configResponse: IUniversalFlagConfigResponse | undefined =
Expand All @@ -68,7 +72,7 @@ export class ConfigurationWireHelper {
}

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

Expand Down
4 changes: 4 additions & 0 deletions src/configuration-wire/configuration-wire-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ export class ConfigurationWireV1 implements IConfigurationWire {
readonly bandits?: IConfigResponse<IBanditParametersResponse>,
) {}

public toString(): string {
return JSON.stringify(this as IConfigurationWire);
}

public static fromResponses(
flagConfig: IUniversalFlagConfigResponse,
banditConfig?: IBanditParametersResponse,
Expand Down
72 changes: 72 additions & 0 deletions src/tools/commands/bootstrap-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as fs from 'fs';

import type { CommandModule } from 'yargs';

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

export const bootstrapConfigCommand: CommandModule = {
command: 'bootstrap-config',
describe: 'Generate a bootstrap configuration string',
builder: (yargs) => {
return yargs.options({
key: {
type: 'string',
description: 'SDK key',
alias: 'k',
default: process.env.EPPO_SDK_KEY,
},
sdk: {
type: 'string',
description: 'Target SDK name',
default: 'js-client-sdk',
},
'base-url': {
type: 'string',
description: 'Base URL for the API',
},
output: {
type: 'string',
description: 'Output file path',
alias: 'o',
},
});
},
handler: async (argv) => {
if (!argv.key) {
console.error('Error: SDK key is required');
console.error('Provide it either as:');
console.error('- Command line argument: --key <sdkKey> or -k <sdkKey>');
console.error('- Environment variable: EPPO_SDK_KEY');
process.exit(1);
}

try {
const helper = ConfigurationWireHelper.build(argv.key as string, {
sdkName: argv.sdk as string,
baseUrl: argv['base-url'] as string,
fetchBandits: true,
});
const config = await helper.fetchConfiguration();

if (!config) {
console.error('Error: Failed to fetch configuration');
process.exit(1);
}

const jsonConfig = JSON.stringify(config, null, 2);

if (argv.output && typeof argv.output === 'string') {
fs.writeFileSync(argv.output, jsonConfig);
console.log(`Configuration written to ${argv.output}`);
} else {
console.log('Configuration:');
console.log('--------------------------------');
console.log(jsonConfig);
console.log('--------------------------------');
}
} catch (error) {
console.error('Error fetching configuration:', error);
process.exit(1);
}
},
};
27 changes: 27 additions & 0 deletions src/tools/get-bootstrap-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

import { bootstrapConfigCommand } from './commands/bootstrap-config';

/**
* Script to run the bootstrap-config command directly.
*
* For usage, run: `ts-node src/tools/get-bootstrap-config.ts --help`
*/
async function main() {
await yargs(hideBin(process.argv))
.command({
command: '$0',
describe: bootstrapConfigCommand.describe,
builder: bootstrapConfigCommand.builder,
handler: bootstrapConfigCommand.handler,
})
.help()
.alias('help', 'h')
.parse();
}

main().catch((error) => {
console.error('Error in main:', error);
process.exit(1);
});
8 changes: 8 additions & 0 deletions src/tools/node-shim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Required type shape for `process`.
// We don't pin this project to node so eslint complains about the use of `process`. We declare a type shape here to
// appease the linter.
export declare const process: {
exit: (code: number) => void;
env: { [key: string]: string | undefined };
argv: string[];
};
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5081,7 +5081,7 @@ yargs-parser@^21.1.1:
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==

yargs@^17.3.1:
yargs@^17.3.1, yargs@^17.7.2:
version "17.7.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
Expand Down
Loading