From 4391381140ccc5049a6d0e31b88406527f38fd4a Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Tue, 11 Mar 2025 22:27:48 -0600 Subject: [PATCH 1/6] feat: Bootstrap configuration and generation tool --- README.md | 83 ++++++++++++++++++++++++++ package.json | 8 ++- src/configuration-wire-types.ts | 45 +++++++++++++- src/tools/bootstrap.ts | 74 +++++++++++++++++++++++ src/tools/cli.ts | 17 ++++++ src/tools/commands/bootstrap-config.ts | 77 ++++++++++++++++++++++++ src/tools/get-bootstrap-config.ts | 32 ++++++++++ 7 files changed, 333 insertions(+), 3 deletions(-) create mode 100644 src/tools/bootstrap.ts create mode 100644 src/tools/cli.ts create mode 100644 src/tools/commands/bootstrap-config.ts create mode 100644 src/tools/get-bootstrap-config.ts diff --git a/README.md b/README.md index a224c7e..8ee4015 100644 --- a/README.md +++ b/README.md @@ -50,3 +50,86 @@ 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 using either the CLI tool or programmatically via the ConfigurationHelper class. + +#### CLI Usage + +The CLI tool can be used in several ways depending on how you've installed the package: + +**If installed globally:** +```bash +# Install globally +npm install -g @eppo/js-client-sdk-common +# or, with yarn +yarn add -g @eppo/js-client-sdk-common + +# Use the command directly +eppo sdk-tools bootstrap-config --key +``` + +**If installed 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 + +# Use via npx +npx eppo sdk-tools bootstrap-config --key + +# Or via yarn +yarn eppo sdk-tools bootstrap-config --key +``` + +Common usage examples: +```bash +# Basic usage +eppo sdk-tools bootstrap-config --key + +# With custom SDK name (default is 'android') +eppo sdk-tools bootstrap-config --key --sdk js-client + +# With custom base URL +eppo sdk-tools bootstrap-config --key --base-url https://api.custom-domain.com + +# Save configuration to a file +eppo sdk-tools bootstrap-config --key --output bootstrap-config.json + +# Show help +eppo sdk-tools 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: 'android') +- `--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', + 'js-client', // optional: target SDK name (default: 'android') + 'https://api.custom-domain.com' // optional: custom base URL + ); + + // Fetch the configuration + const configBuilder = await helper.getBootstrapConfigurationString(); + const configString = configBuilder.toString(); + + // Use the configuration string to initialize your SDK + console.log(configString); +} +``` + +The tool will output a JSON string containing the configuration wire format that can be used to initialize Eppo SDKs in offline mode. diff --git a/package.json b/package.json index b4f5dd1..86a54e5 100644 --- a/package.json +++ b/package.json @@ -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", + "get-bootstrap-config": "ts-node src/tools/get-bootstrap-config.ts" }, "jsdelivr": "dist/eppo-sdk.js", "repository": { @@ -78,5 +79,8 @@ "spark-md5": "^3.0.2", "uuid": "^11.0.5" }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", + "bin": { + "eppo": "./dist/tools/cli.js" + } } diff --git a/src/configuration-wire-types.ts b/src/configuration-wire-types.ts index 4f2f30c..feb8ee6 100644 --- a/src/configuration-wire-types.ts +++ b/src/configuration-wire-types.ts @@ -1,3 +1,4 @@ +import { IUniversalFlagConfigResponse, IBanditParametersResponse } from './http-client'; import { Environment, FormatEnum, @@ -16,6 +17,8 @@ interface IBasePrecomputedConfigurationResponse { readonly environment?: Environment; } +const t = new EventTarget(); + export interface IPrecomputedConfigurationResponse extends IBasePrecomputedConfigurationResponse { readonly obfuscated: false; // Always false readonly flags: Record; @@ -151,11 +154,51 @@ export interface IConfigurationWire { */ readonly version: number; + readonly config?: IConfigResponse; + readonly bandits?: IConfigResponse; + + /** + * + */ + // TODO: Add flags and bandits for offline/non-precomputed initialization readonly precomputed?: IPrecomputedConfiguration; } +export interface IConfigResponse { + response: string; // JSON-encoded server response + etag?: string; + fetchedAt?: string; // ISO timestamp +} + export class ConfigurationWireV1 implements IConfigurationWire { public readonly version = 1; - constructor(readonly precomputed?: IPrecomputedConfiguration) {} + + constructor( + readonly config?: IConfigResponse, + readonly bandits?: IConfigResponse, + readonly precomputed?: IPrecomputedConfiguration, + ) {} + + public static fromResponses( + flagConfig: IUniversalFlagConfigResponse, + banditConfig?: IBanditParametersResponse, + flagConfigEtag?: string, + banditConfigEtag?: string, + ): ConfigurationWireV1 { + return new ConfigurationWireV1( + { + response: JSON.stringify(flagConfig), + fetchedAt: new Date().toISOString(), + etag: flagConfigEtag, + }, + banditConfig + ? { + response: JSON.stringify(banditConfig), + fetchedAt: new Date().toISOString(), + etag: banditConfigEtag, + } + : undefined, + ); + } } diff --git a/src/tools/bootstrap.ts b/src/tools/bootstrap.ts new file mode 100644 index 0000000..73274b8 --- /dev/null +++ b/src/tools/bootstrap.ts @@ -0,0 +1,74 @@ +import ApiEndpoints from '../api-endpoints'; +import { ConfigurationWireV1, IConfigurationWire } from '../configuration-wire-types'; +import FetchHttpClient, { + IBanditParametersResponse, + IHttpClient, + IUniversalFlagConfigResponse, +} from '../http-client'; + +/** + * Helper class for fetching and converting configuration from the Eppo API(s). + */ +export class ConfigurationHelper { + private httpClient: IHttpClient; + + /** + * Build a new ConfigurationHelper for the target SDK Key. + * @param sdkKey + * @param targetSdkName + * @param baseUrl + */ + public static build(sdkKey: string, targetSdkName = 'android', baseUrl?: string) { + return new ConfigurationHelper(sdkKey, targetSdkName, baseUrl); + } + + private constructor( + private readonly sdkKey: string, + private readonly targetSdkName = 'android', + private readonly targetSdkVersion = '4.0.0', + private readonly baseUrl?: string, + ) { + const queryParams = { + sdkName: this.targetSdkName, + sdkVersion: this.targetSdkVersion, + apiKey: this.sdkKey, + }; + const apiEndpoints = new ApiEndpoints({ + baseUrl, + queryParams, + }); + + this.httpClient = new FetchHttpClient(apiEndpoints, 5000); + } + + /** + * Builds an `IConfigurationWire` object from flag and bandit API responses. + * The IConfigurationWire instance can be used to bootstrap some SDKs. + */ + public async getBootstrapConfigurationFromApi(): Promise { + // 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( + new ConfigurationWireV1({ + response: JSON.stringify({ + flags: {}, + environment: { name: '' }, + fetchedAt: '', + publishedAt: '', + }), + }), + ); + } + + const flagsHaveBandits = Object.keys(configResponse.banditReferences ?? {}).length > 0; + if (flagsHaveBandits) { + banditResponse = await this.httpClient.getBanditParameters(); + } + + return ConfigurationWireV1.fromResponses(configResponse, banditResponse); + } +} diff --git a/src/tools/cli.ts b/src/tools/cli.ts new file mode 100644 index 0000000..3c99503 --- /dev/null +++ b/src/tools/cli.ts @@ -0,0 +1,17 @@ +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +import { bootstrapConfigCommand } from './commands/bootstrap-config'; + +declare const process: { + argv: string[]; +}; + +yargs(hideBin(process.argv)) + .command('sdk-tools', 'SDK tooling commands', (yargs) => { + return yargs.command(bootstrapConfigCommand); + }) + .demandCommand(1) + .help() + .alias('help', 'h') + .parse(); diff --git a/src/tools/commands/bootstrap-config.ts b/src/tools/commands/bootstrap-config.ts new file mode 100644 index 0000000..26410ed --- /dev/null +++ b/src/tools/commands/bootstrap-config.ts @@ -0,0 +1,77 @@ +declare const process: { + exit: (code: number) => void; + env: { [key: string]: string | undefined }; +}; + +import * as fs from 'fs'; + +import type { CommandModule } from 'yargs'; + +import { ConfigurationHelper } from '../bootstrap'; + +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: 'android', + }, + '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 or -k '); + console.error('- Environment variable: EPPO_SDK_KEY'); + process.exit(1); + } + + try { + const helper = ConfigurationHelper.build( + argv.key as string, + argv.sdk as string, + argv['base-url'] as string, + ); + const config = await helper.getBootstrapConfigurationFromApi(); + + if (!config) { + console.error('Error: Failed to fetch configuration'); + process.exit(1); + } + + const jsonConfig = JSON.stringify(config); + + 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); + } + }, +}; diff --git a/src/tools/get-bootstrap-config.ts b/src/tools/get-bootstrap-config.ts new file mode 100644 index 0000000..0e6c6ae --- /dev/null +++ b/src/tools/get-bootstrap-config.ts @@ -0,0 +1,32 @@ +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +import { bootstrapConfigCommand } from './commands/bootstrap-config'; + +// Required type shape for `process`. +declare const process: { + exit: (code: number) => void; + env: { [key: string]: string | undefined }; + argv: string[]; +}; + +/** + * Script to run the bootstrap-config command directly. + */ +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); +}); From 00c9b9b0679d47f7fffc79c6f366b80650783946 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Tue, 11 Mar 2025 22:46:39 -0600 Subject: [PATCH 2/6] wip bootstrapping --- Makefile | 3 +- src/client/eppo-client.ts | 38 +++++++++++++ src/configuration-requestor.ts | 95 ++++++++++++++++++++------------- src/configuration-wire-types.ts | 2 - src/i-configuration.ts | 2 +- 5 files changed, 98 insertions(+), 42 deletions(-) diff --git a/Makefile b/Makefile index 0a914fc..baad711 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,8 @@ test-data: mkdir -p $(tempDir) git clone -b ${branchName} --depth 1 --single-branch ${githubRepoLink} ${gitDataDir} cp -r ${gitDataDir}ufc ${testDataDir} - cp -r ${gitDataDir}configuration-wire ${testDataDir} + mkdir -p ${testDataDir}configuration-wire + cp -r ${gitDataDir}configuration-wire/*.json ${testDataDir}/configuration-wire rm -rf ${tempDir} ## prepare diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index 3b47ece..60dd13d 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -309,6 +309,44 @@ export default class EppoClient { ); } + public bootstrap(configuration: IConfigurationWire) { + if (!this.configurationRequestParameters) { + throw new Error( + 'Eppo SDK unable to fetch flag configurations without configuration request parameters', + ); + } + // if fetchFlagConfigurations() was previously called, stop any polling process from that call + this.requestPoller?.stop(); + const { + apiKey, + sdkName, + sdkVersion, + baseUrl, // Default is set in ApiEndpoints constructor if undefined + requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, + } = this.configurationRequestParameters; + + let { pollingIntervalMs = DEFAULT_POLL_INTERVAL_MS } = this.configurationRequestParameters; + if (pollingIntervalMs <= 0) { + logger.error('pollingIntervalMs must be greater than 0. Using default'); + pollingIntervalMs = DEFAULT_POLL_INTERVAL_MS; + } + + // todo: Inject the chain of dependencies below + const apiEndpoints = new ApiEndpoints({ + baseUrl, + queryParams: { apiKey, sdkName, sdkVersion }, + }); + const httpClient = new FetchHttpClient(apiEndpoints, requestTimeoutMs); + const configurationRequestor = new ConfigurationRequestor( + httpClient, + this.flagConfigurationStore, + this.banditVariationConfigurationStore ?? null, + this.banditModelConfigurationStore ?? null, + ); + + configurationRequestor.setInitialConfiguration(configuration); + } + async fetchFlagConfigurations() { if (!this.configurationRequestParameters) { throw new Error( diff --git a/src/configuration-requestor.ts b/src/configuration-requestor.ts index 1f48e09..c552425 100644 --- a/src/configuration-requestor.ts +++ b/src/configuration-requestor.ts @@ -1,5 +1,10 @@ import { IConfigurationStore } from './configuration-store/configuration-store'; -import { IHttpClient } from './http-client'; +import { IConfigurationWire } from './configuration-wire-types'; +import { + IBanditParametersResponse, + IHttpClient, + IUniversalFlagConfigResponse, +} from './http-client'; import { ConfigStoreHydrationPacket, IConfiguration, @@ -27,6 +32,12 @@ export default class ConfigurationRequestor { ); } + public setInitialConfiguration(configuration: IConfigurationWire): Promise { + const flags = JSON.parse(configuration.config?.response ?? '{}'); + const bandits = JSON.parse(configuration.bandits?.response ?? '{}'); + return this.hydrateConfigurationStores(flags, bandits); + } + public isFlagConfigExpired(): Promise { return this.flagConfigurationStore.isExpired(); } @@ -35,63 +46,71 @@ export default class ConfigurationRequestor { return this.configuration; } + private async hydrateConfigurationStores( + flagConfig: IUniversalFlagConfigResponse, + banditResponse?: IBanditParametersResponse, + ): Promise { + let banditVariationPacket: ConfigStoreHydrationPacket | undefined; + let banditModelPacket: ConfigStoreHydrationPacket | undefined; + const flagResponsePacket: ConfigStoreHydrationPacket = { + entries: flagConfig.flags, + environment: flagConfig.environment, + createdAt: flagConfig.createdAt, + format: flagConfig.format, + }; + + if (banditResponse) { + // Map bandit flag associations by flag key for quick lookup (instead of bandit key as provided by the UFC) + const banditVariations = this.indexBanditVariationsByFlagKey(flagConfig.banditReferences); + + banditVariationPacket = { + entries: banditVariations, + environment: flagConfig.environment, + createdAt: flagConfig.createdAt, + format: flagConfig.format, + }; + + if (banditResponse?.bandits) { + banditModelPacket = { + entries: banditResponse.bandits, + environment: flagConfig.environment, + createdAt: flagConfig.createdAt, + format: flagConfig.format, + }; + } + } + + return await this.configuration.hydrateConfigurationStores( + flagResponsePacket, + banditVariationPacket, + banditModelPacket, + ); + } + async fetchAndStoreConfigurations(): Promise { const configResponse = await this.httpClient.getUniversalFlagConfiguration(); + let banditResponse: IBanditParametersResponse | undefined; if (!configResponse?.flags) { return; } - const flagResponsePacket: ConfigStoreHydrationPacket = { - entries: configResponse.flags, - environment: configResponse.environment, - createdAt: configResponse.createdAt, - format: configResponse.format, - }; - - let banditVariationPacket: ConfigStoreHydrationPacket | undefined; - let banditModelPacket: ConfigStoreHydrationPacket | undefined; const flagsHaveBandits = Object.keys(configResponse.banditReferences ?? {}).length > 0; const banditStoresProvided = Boolean( this.banditVariationConfigurationStore && this.banditModelConfigurationStore, ); if (flagsHaveBandits && banditStoresProvided) { - // Map bandit flag associations by flag key for quick lookup (instead of bandit key as provided by the UFC) - const banditVariations = this.indexBanditVariationsByFlagKey(configResponse.banditReferences); - - banditVariationPacket = { - entries: banditVariations, - environment: configResponse.environment, - createdAt: configResponse.createdAt, - format: configResponse.format, - }; - if ( this.requiresBanditModelConfigurationStoreUpdate( this.banditModelVersions, configResponse.banditReferences, ) ) { - const banditResponse = await this.httpClient.getBanditParameters(); - if (banditResponse?.bandits) { - banditModelPacket = { - entries: banditResponse.bandits, - environment: configResponse.environment, - createdAt: configResponse.createdAt, - format: configResponse.format, - }; - - this.banditModelVersions = this.getLoadedBanditModelVersions(banditResponse.bandits); - } + banditResponse = await this.httpClient.getBanditParameters(); } } - if ( - await this.configuration.hydrateConfigurationStores( - flagResponsePacket, - banditVariationPacket, - banditModelPacket, - ) - ) { + if (await this.hydrateConfigurationStores(configResponse, banditResponse)) { + this.banditModelVersions = this.getLoadedBanditModelVersions(this.configuration.getBandits()); // TODO: Notify that config updated. } } diff --git a/src/configuration-wire-types.ts b/src/configuration-wire-types.ts index feb8ee6..1140446 100644 --- a/src/configuration-wire-types.ts +++ b/src/configuration-wire-types.ts @@ -17,8 +17,6 @@ interface IBasePrecomputedConfigurationResponse { readonly environment?: Environment; } -const t = new EventTarget(); - export interface IPrecomputedConfigurationResponse extends IBasePrecomputedConfigurationResponse { readonly obfuscated: false; // Always false readonly flags: Record; diff --git a/src/i-configuration.ts b/src/i-configuration.ts index f8d3b0d..9bd9eba 100644 --- a/src/i-configuration.ts +++ b/src/i-configuration.ts @@ -49,7 +49,7 @@ export class StoreBackedConfiguration implements IConfiguration { flagConfig: ConfigStoreHydrationPacket, banditVariationConfig?: ConfigStoreHydrationPacket, banditModelConfig?: ConfigStoreHydrationPacket, - ) { + ): Promise { const didUpdateFlags = await hydrateConfigurationStore(this.flagConfigurationStore, flagConfig); const promises: Promise[] = []; if (this.banditVariationConfigurationStore && banditVariationConfig) { From cd454be01e426338deda58dec3fd718df8986307 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Fri, 14 Mar 2025 08:53:28 -0600 Subject: [PATCH 3/6] trim down tool script --- README.md | 35 ++++-------- package.json | 7 +-- src/tools/bootstrap.ts | 74 -------------------------- src/tools/cli.ts | 17 ------ src/tools/commands/bootstrap-config.ts | 14 ++--- 5 files changed, 19 insertions(+), 128 deletions(-) delete mode 100644 src/tools/bootstrap.ts delete mode 100644 src/tools/cli.ts diff --git a/README.md b/README.md index 8ee4015..d614923 100644 --- a/README.md +++ b/README.md @@ -55,53 +55,38 @@ When publishing releases, the following rules apply: ### Bootstrap Configuration -You can generate a bootstrap configuration string using either the CLI tool or programmatically via the ConfigurationHelper class. +You can generate a bootstrap configuration string from either the command line or programmatically via the +ConfigurationWireHelper class. -#### CLI Usage +#### Command Line Usage -The CLI tool can be used in several ways depending on how you've installed the package: - -**If installed globally:** -```bash -# Install globally -npm install -g @eppo/js-client-sdk-common -# or, with yarn -yarn add -g @eppo/js-client-sdk-common - -# Use the command directly -eppo sdk-tools bootstrap-config --key -``` - -**If installed as a project dependency:** +**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 -# Use via npx -npx eppo sdk-tools bootstrap-config --key - # Or via yarn -yarn eppo sdk-tools bootstrap-config --key +yarn bootstrap-config --key ``` Common usage examples: ```bash # Basic usage -eppo sdk-tools bootstrap-config --key +yarn bootstrap-config --key # With custom SDK name (default is 'android') -eppo sdk-tools bootstrap-config --key --sdk js-client +yarn bootstrap-config --key --sdk js-client # With custom base URL -eppo sdk-tools bootstrap-config --key --base-url https://api.custom-domain.com +yarn bootstrap-config --key --base-url https://api.custom-domain.com # Save configuration to a file -eppo sdk-tools bootstrap-config --key --output bootstrap-config.json +yarn bootstrap-config --key --output bootstrap-config.json # Show help -eppo sdk-tools bootstrap-config --help +yarn bootstrap-config --help ``` The tool accepts the following arguments: diff --git a/package.json b/package.json index e13ac93..e4d425c 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "test": "yarn test:unit", "test:unit": "NODE_ENV=test jest '.*\\.spec\\.ts'", "obfuscate-mock-ufc": "ts-node test/writeObfuscatedMockUFC", - "get-bootstrap-config": "ts-node src/tools/get-bootstrap-config.ts" + "bootstrap-config": "ts-node src/tools/get-bootstrap-config.ts" }, "jsdelivr": "dist/eppo-sdk.js", "repository": { @@ -79,8 +79,5 @@ "spark-md5": "^3.0.2", "uuid": "^11.0.5" }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", - "bin": { - "eppo": "./dist/tools/cli.js" - } + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/tools/bootstrap.ts b/src/tools/bootstrap.ts deleted file mode 100644 index 73274b8..0000000 --- a/src/tools/bootstrap.ts +++ /dev/null @@ -1,74 +0,0 @@ -import ApiEndpoints from '../api-endpoints'; -import { ConfigurationWireV1, IConfigurationWire } from '../configuration-wire-types'; -import FetchHttpClient, { - IBanditParametersResponse, - IHttpClient, - IUniversalFlagConfigResponse, -} from '../http-client'; - -/** - * Helper class for fetching and converting configuration from the Eppo API(s). - */ -export class ConfigurationHelper { - private httpClient: IHttpClient; - - /** - * Build a new ConfigurationHelper for the target SDK Key. - * @param sdkKey - * @param targetSdkName - * @param baseUrl - */ - public static build(sdkKey: string, targetSdkName = 'android', baseUrl?: string) { - return new ConfigurationHelper(sdkKey, targetSdkName, baseUrl); - } - - private constructor( - private readonly sdkKey: string, - private readonly targetSdkName = 'android', - private readonly targetSdkVersion = '4.0.0', - private readonly baseUrl?: string, - ) { - const queryParams = { - sdkName: this.targetSdkName, - sdkVersion: this.targetSdkVersion, - apiKey: this.sdkKey, - }; - const apiEndpoints = new ApiEndpoints({ - baseUrl, - queryParams, - }); - - this.httpClient = new FetchHttpClient(apiEndpoints, 5000); - } - - /** - * Builds an `IConfigurationWire` object from flag and bandit API responses. - * The IConfigurationWire instance can be used to bootstrap some SDKs. - */ - public async getBootstrapConfigurationFromApi(): Promise { - // 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( - new ConfigurationWireV1({ - response: JSON.stringify({ - flags: {}, - environment: { name: '' }, - fetchedAt: '', - publishedAt: '', - }), - }), - ); - } - - const flagsHaveBandits = Object.keys(configResponse.banditReferences ?? {}).length > 0; - if (flagsHaveBandits) { - banditResponse = await this.httpClient.getBanditParameters(); - } - - return ConfigurationWireV1.fromResponses(configResponse, banditResponse); - } -} diff --git a/src/tools/cli.ts b/src/tools/cli.ts deleted file mode 100644 index 3c99503..0000000 --- a/src/tools/cli.ts +++ /dev/null @@ -1,17 +0,0 @@ -import yargs from 'yargs'; -import { hideBin } from 'yargs/helpers'; - -import { bootstrapConfigCommand } from './commands/bootstrap-config'; - -declare const process: { - argv: string[]; -}; - -yargs(hideBin(process.argv)) - .command('sdk-tools', 'SDK tooling commands', (yargs) => { - return yargs.command(bootstrapConfigCommand); - }) - .demandCommand(1) - .help() - .alias('help', 'h') - .parse(); diff --git a/src/tools/commands/bootstrap-config.ts b/src/tools/commands/bootstrap-config.ts index 26410ed..738c581 100644 --- a/src/tools/commands/bootstrap-config.ts +++ b/src/tools/commands/bootstrap-config.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import type { CommandModule } from 'yargs'; -import { ConfigurationHelper } from '../bootstrap'; +import { ConfigurationWireHelper } from '../../configuration-wire/configuration-wire-helper'; export const bootstrapConfigCommand: CommandModule = { command: 'bootstrap-config', @@ -46,12 +46,12 @@ export const bootstrapConfigCommand: CommandModule = { } try { - const helper = ConfigurationHelper.build( - argv.key as string, - argv.sdk as string, - argv['base-url'] as string, - ); - const config = await helper.getBootstrapConfigurationFromApi(); + const helper = ConfigurationWireHelper.build(argv.key as string, { + sdkName: argv.sdk as string, + sdkVersion: 'v5.0.0', + baseUrl: argv['base-url'] as string, + }); + const config = await helper.fetchBootstrapConfiguration(); if (!config) { console.error('Error: Failed to fetch configuration'); From 50d31d35c1954a2b369fe42f0a10ccc1304bdd20 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Fri, 14 Mar 2025 09:02:58 -0600 Subject: [PATCH 4/6] refactor node shim process shape --- src/tools/commands/bootstrap-config.ts | 6 +----- src/tools/get-bootstrap-config.ts | 8 +------- src/tools/node-shim.ts | 8 ++++++++ 3 files changed, 10 insertions(+), 12 deletions(-) create mode 100644 src/tools/node-shim.ts diff --git a/src/tools/commands/bootstrap-config.ts b/src/tools/commands/bootstrap-config.ts index 738c581..d694f2f 100644 --- a/src/tools/commands/bootstrap-config.ts +++ b/src/tools/commands/bootstrap-config.ts @@ -1,13 +1,9 @@ -declare const process: { - exit: (code: number) => void; - env: { [key: string]: string | undefined }; -}; - import * as fs from 'fs'; import type { CommandModule } from 'yargs'; import { ConfigurationWireHelper } from '../../configuration-wire/configuration-wire-helper'; +import { process } from '../node-shim'; export const bootstrapConfigCommand: CommandModule = { command: 'bootstrap-config', diff --git a/src/tools/get-bootstrap-config.ts b/src/tools/get-bootstrap-config.ts index 0e6c6ae..272dd61 100644 --- a/src/tools/get-bootstrap-config.ts +++ b/src/tools/get-bootstrap-config.ts @@ -2,13 +2,7 @@ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { bootstrapConfigCommand } from './commands/bootstrap-config'; - -// Required type shape for `process`. -declare const process: { - exit: (code: number) => void; - env: { [key: string]: string | undefined }; - argv: string[]; -}; +import { process } from './node-shim'; /** * Script to run the bootstrap-config command directly. diff --git a/src/tools/node-shim.ts b/src/tools/node-shim.ts new file mode 100644 index 0000000..2c77736 --- /dev/null +++ b/src/tools/node-shim.ts @@ -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[]; +}; From 88ce8bc057d2d1eb3a3d21d969d5b2928281a039 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Fri, 14 Mar 2025 09:25:54 -0600 Subject: [PATCH 5/6] chore: more notes --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d614923..136d9b3 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,9 @@ When publishing releases, the following rules apply: 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:** @@ -117,4 +120,4 @@ async function getBootstrapConfig() { } ``` -The tool will output a JSON string containing the configuration wire format that can be used to initialize Eppo SDKs in offline mode. +The tool will output a JSON string containing the configuration wire format that can be used to bootstrap Eppo SDKs. From 1468e76b7beb64f565ffd4ba094279b8b211192d Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Fri, 14 Mar 2025 09:37:40 -0600 Subject: [PATCH 6/6] fix ref --- src/configuration-requestor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/configuration-requestor.ts b/src/configuration-requestor.ts index c552425..be6cf64 100644 --- a/src/configuration-requestor.ts +++ b/src/configuration-requestor.ts @@ -1,5 +1,5 @@ import { IConfigurationStore } from './configuration-store/configuration-store'; -import { IConfigurationWire } from './configuration-wire-types'; +import { IConfigurationWire } from './configuration-wire/configuration-wire-types'; import { IBanditParametersResponse, IHttpClient,