diff --git a/packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts b/packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts index 3f437855a..e0ccf7502 100644 --- a/packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts +++ b/packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import { createSafeLogger } from '@launchdarkly/js-sdk-common'; -import ConfigurationImpl from '../../src/configuration/Configuration'; +import { createConfiguration } from '../../src/configuration/Configuration'; describe('Configuration', () => { beforeEach(() => { @@ -10,17 +10,15 @@ describe('Configuration', () => { }); it('has valid default values', () => { - const config = new ConfigurationImpl(); + const config = createConfiguration(); expect(config).toMatchObject({ allAttributesPrivate: false, - baseUri: 'https://clientsdk.launchdarkly.com', capacity: 100, debug: false, diagnosticOptOut: false, diagnosticRecordingInterval: 900, withReasons: false, - eventsUri: 'https://events.launchdarkly.com', flushInterval: 30, logger: expect.anything(), maxCachedContexts: 5, @@ -28,28 +26,32 @@ describe('Configuration', () => { sendEvents: true, sendLDHeaders: true, streamInitialReconnectDelay: 1, - streamUri: 'https://clientstream.launchdarkly.com', useReport: false, }); + // Verify service endpoints have correct default values + expect(config.serviceEndpoints.polling).toBe('https://clientsdk.launchdarkly.com'); + expect(config.serviceEndpoints.streaming).toBe('https://clientstream.launchdarkly.com'); + expect(config.serviceEndpoints.events).toBe('https://events.launchdarkly.com'); expect(console.error).not.toHaveBeenCalled(); }); it('allows specifying valid wrapperName', () => { - const config = new ConfigurationImpl({ wrapperName: 'test' }); + const config = createConfiguration({ wrapperName: 'test' }); expect(config).toMatchObject({ wrapperName: 'test' }); }); it('warns and ignored invalid keys', () => { // @ts-ignore - const config = new ConfigurationImpl({ baseballUri: 1 }); + const config = createConfiguration({ baseballUri: 1 }); + // @ts-ignore expect(config.baseballUri).toBeUndefined(); expect(console.error).toHaveBeenCalledWith(expect.stringContaining('unknown config option')); }); it('converts boolean types', () => { // @ts-ignore - const config = new ConfigurationImpl({ sendEvents: 0 }); + const config = createConfiguration({ sendEvents: 0 }); expect(config.sendEvents).toBeFalsy(); expect(console.error).toHaveBeenCalledWith( @@ -59,7 +61,7 @@ describe('Configuration', () => { it('ignores wrong type for number and logs appropriately', () => { // @ts-ignore - const config = new ConfigurationImpl({ capacity: true }); + const config = createConfiguration({ capacity: true }); expect(config.capacity).toEqual(100); expect(console.error).toHaveBeenCalledWith( @@ -68,7 +70,7 @@ describe('Configuration', () => { }); it('enforces minimum flushInterval', () => { - const config = new ConfigurationImpl({ flushInterval: 1 }); + const config = createConfiguration({ flushInterval: 1 }); expect(config.flushInterval).toEqual(2); expect(console.error).toHaveBeenNthCalledWith( @@ -78,14 +80,14 @@ describe('Configuration', () => { }); it('allows setting a valid maxCachedContexts', () => { - const config = new ConfigurationImpl({ maxCachedContexts: 3 }); + const config = createConfiguration({ maxCachedContexts: 3 }); expect(config.maxCachedContexts).toBeDefined(); expect(console.error).not.toHaveBeenCalled(); }); it('enforces minimum maxCachedContext', () => { - const config = new ConfigurationImpl({ maxCachedContexts: -1 }); + const config = createConfiguration({ maxCachedContexts: -1 }); expect(config.maxCachedContexts).toBeDefined(); expect(console.error).toHaveBeenNthCalledWith( @@ -101,16 +103,16 @@ describe('Configuration', () => { ['kebab-case-works'], ['snake_case_works'], ])('allow setting valid payload filter keys', (filter) => { - const config = new ConfigurationImpl({ payloadFilterKey: filter }); - expect(config.payloadFilterKey).toEqual(filter); + const config = createConfiguration({ payloadFilterKey: filter }); + expect(config.serviceEndpoints.payloadFilterKey).toEqual(filter); expect(console.error).toHaveBeenCalledTimes(0); }); it.each([['invalid-@-filter'], ['_invalid-filter'], ['-invalid-filter']])( 'ignores invalid filters and logs a warning', (filter) => { - const config = new ConfigurationImpl({ payloadFilterKey: filter }); - expect(config.payloadFilterKey).toBeUndefined(); + const config = createConfiguration({ payloadFilterKey: filter }); + expect(config.serviceEndpoints.payloadFilterKey).toBeUndefined(); expect(console.error).toHaveBeenNthCalledWith( 1, expect.stringMatching(/should be of type string matching/i), @@ -134,7 +136,7 @@ it('makes a safe logger', () => { throw new Error('bad'); }, }; - const config = new ConfigurationImpl({ + const config = createConfiguration({ logger: badLogger, }); @@ -160,6 +162,6 @@ it('does not wrap already safe loggers', () => { throw new Error('bad'); }, }); - const config = new ConfigurationImpl({ logger }); + const config = createConfiguration({ logger }); expect(config.logger).toBe(logger); }); diff --git a/packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts b/packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts index cc0759d86..3d65d1fee 100644 --- a/packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts +++ b/packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts @@ -7,7 +7,7 @@ import { LDUser, } from '@launchdarkly/js-sdk-common'; -import { Configuration, ConfigurationImpl } from '../../src/configuration'; +import { Configuration, createConfiguration } from '../../src/configuration'; import { addApplicationInfo, addAutoEnv, @@ -37,7 +37,7 @@ describe('automatic environment attributes', () => { beforeEach(() => { ({ crypto, info } = mockPlatform); (crypto.randomUUID as jest.Mock).mockResolvedValue('test-device-key-1'); - config = new ConfigurationImpl({ logger }); + config = createConfiguration({ logger }); }); afterEach(() => { @@ -342,7 +342,7 @@ describe('automatic environment attributes', () => { describe('addApplicationInfo', () => { test('add id, version, name, versionName', async () => { - config = new ConfigurationImpl({ + config = createConfiguration({ applicationInfo: { id: 'com.from-config.ld', version: '2.2.2', @@ -435,7 +435,7 @@ describe('automatic environment attributes', () => { info.platformData = jest .fn() .mockReturnValueOnce({ ld_application: { version: null, locale: '' } }); - config = new ConfigurationImpl({ applicationInfo: { version: '1.2.3' } }); + config = createConfiguration({ applicationInfo: { version: '1.2.3' } }); const ldApplication = await addApplicationInfo(mockPlatform, config); expect(ldApplication).toBeUndefined(); diff --git a/packages/shared/sdk-client/__tests__/diagnostics/createDiagnosticsInitConfig.test.ts b/packages/shared/sdk-client/__tests__/diagnostics/createDiagnosticsInitConfig.test.ts index 004dd0f7b..35f6bd218 100644 --- a/packages/shared/sdk-client/__tests__/diagnostics/createDiagnosticsInitConfig.test.ts +++ b/packages/shared/sdk-client/__tests__/diagnostics/createDiagnosticsInitConfig.test.ts @@ -1,6 +1,6 @@ import { secondsToMillis } from '@launchdarkly/js-sdk-common'; -import { ConfigurationImpl } from '../../src/configuration'; +import { createConfiguration } from '../../src/configuration'; import createDiagnosticsInitConfig, { type DiagnosticsInitConfig, } from '../../src/diagnostics/createDiagnosticsInitConfig'; @@ -9,7 +9,7 @@ describe('createDiagnosticsInitConfig', () => { let initConfig: DiagnosticsInitConfig; beforeEach(() => { - initConfig = createDiagnosticsInitConfig(new ConfigurationImpl()); + initConfig = createDiagnosticsInitConfig(createConfiguration()); }); test('defaults', () => { @@ -29,7 +29,7 @@ describe('createDiagnosticsInitConfig', () => { test('non-default config', () => { const custom = createDiagnosticsInitConfig( - new ConfigurationImpl({ + createConfiguration({ baseUri: 'https://dev.ld.com', streamUri: 'https://stream.ld.com', eventsUri: 'https://events.ld.com', diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 9bec7272b..3cfb944dd 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -37,7 +37,7 @@ import { import { LDEvaluationDetail, LDEvaluationDetailTyped } from './api/LDEvaluationDetail'; import { LDIdentifyOptions } from './api/LDIdentifyOptions'; import { createAsyncTaskQueue } from './async/AsyncTaskQueue'; -import { Configuration, ConfigurationImpl, LDClientInternalOptions } from './configuration'; +import { Configuration, createConfiguration, LDClientInternalOptions } from './configuration'; import { addAutoEnv } from './context/addAutoEnv'; import { ActiveContextTracker, @@ -114,7 +114,7 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult { throw new Error('Platform must implement Encoding because btoa is required.'); } - this._config = new ConfigurationImpl(options, internalOptions); + this._config = createConfiguration(options, internalOptions); this.logger = this._config.logger; this._baseHeaders = defaultHeaders( diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index ead620ac3..a137b91c2 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -76,141 +76,128 @@ function ensureSafeLogger(logger?: LDLogger): LDLogger { return createSafeLogger(logger); } -export default class ConfigurationImpl implements Configuration { - public readonly logger: LDLogger = createSafeLogger(); - - // Naming conventions is not followed for these lines because the config validation - // accesses members based on the keys of the options. (sdk-763) - // eslint-disable-next-line @typescript-eslint/naming-convention - private readonly baseUri = DEFAULT_POLLING; - // eslint-disable-next-line @typescript-eslint/naming-convention - private readonly eventsUri = ServiceEndpoints.DEFAULT_EVENTS; - // eslint-disable-next-line @typescript-eslint/naming-convention - private readonly streamUri = DEFAULT_STREAM; - - public readonly maxCachedContexts = 5; - - public readonly capacity = 100; - public readonly diagnosticRecordingInterval = 900; - public readonly flushInterval = 30; - public readonly streamInitialReconnectDelay = 1; - - public readonly allAttributesPrivate: boolean = false; - public readonly debug: boolean = false; - public readonly diagnosticOptOut: boolean = false; - public readonly sendEvents: boolean = true; - public readonly sendLDHeaders: boolean = true; - - public readonly useReport: boolean = false; - public readonly withReasons: boolean = false; - - public readonly privateAttributes: string[] = []; - - public readonly tags: ApplicationTags; - public readonly applicationInfo?: { +interface ConfigurationValues { + logger: LDLogger; + baseUri: string; + eventsUri: string; + streamUri: string; + maxCachedContexts: number; + capacity: number; + diagnosticRecordingInterval: number; + flushInterval: number; + streamInitialReconnectDelay: number; + allAttributesPrivate: boolean; + debug: boolean; + diagnosticOptOut: boolean; + sendEvents: boolean; + sendLDHeaders: boolean; + useReport: boolean; + withReasons: boolean; + privateAttributes: string[]; + applicationInfo?: { id?: string; version?: string; name?: string; versionName?: string; }; - public readonly bootstrap?: LDFlagSet; - - // TODO: implement requestHeaderTransform - public readonly requestHeaderTransform?: (headers: Map) => Map; - public readonly stream?: boolean; - public readonly hash?: string; - public readonly wrapperName?: string; - public readonly wrapperVersion?: string; - - public readonly serviceEndpoints: ServiceEndpoints; - - public readonly pollInterval: number = DEFAULT_POLLING_INTERVAL; - - public readonly userAgentHeaderName: 'user-agent' | 'x-launchdarkly-user-agent'; - - public readonly hooks: Hook[] = []; - - public readonly inspectors: LDInspection[] = []; - - public readonly trackEventModifier: ( - event: internal.InputCustomEvent, - ) => internal.InputCustomEvent; - - public readonly credentialType: 'clientSideId' | 'mobileKey'; - public readonly getImplementationHooks: ( - environmentMetadata: LDPluginEnvironmentMetadata, - ) => Hook[]; - - // Allow indexing Configuration by a string + bootstrap?: LDFlagSet; + requestHeaderTransform?: (headers: Map) => Map; + stream?: boolean; + hash?: string; + wrapperName?: string; + wrapperVersion?: string; + pollInterval: number; + hooks: Hook[]; + inspectors: LDInspection[]; + payloadFilterKey?: string; [index: string]: any; +} - constructor( - pristineOptions: LDOptions = {}, - internalOptions: LDClientInternalOptions = { - getImplementationHooks: () => [], - credentialType: 'mobileKey', - }, - ) { - this.logger = ensureSafeLogger(pristineOptions.logger); - const errors = this._validateTypesAndNames(pristineOptions); - errors.forEach((e: string) => this.logger.warn(e)); - - this.serviceEndpoints = new ServiceEndpoints( - this.streamUri, - this.baseUri, - this.eventsUri, - internalOptions.analyticsEventPath, - internalOptions.diagnosticEventPath, - internalOptions.includeAuthorizationHeader, - pristineOptions.payloadFilterKey, - ); - this.useReport = pristineOptions.useReport ?? false; +export function createConfiguration( + pristineOptions: LDOptions = {}, + internalOptions: LDClientInternalOptions = { + getImplementationHooks: () => [], + credentialType: 'mobileKey', + }, +): Configuration { + const logger = ensureSafeLogger(pristineOptions.logger); + + const values: ConfigurationValues = { + logger, + baseUri: DEFAULT_POLLING, + eventsUri: ServiceEndpoints.DEFAULT_EVENTS, + streamUri: DEFAULT_STREAM, + maxCachedContexts: 5, + capacity: 100, + diagnosticRecordingInterval: 900, + flushInterval: 30, + streamInitialReconnectDelay: 1, + allAttributesPrivate: false, + debug: false, + diagnosticOptOut: false, + sendEvents: true, + sendLDHeaders: true, + useReport: false, + withReasons: false, + privateAttributes: [], + pollInterval: DEFAULT_POLLING_INTERVAL, + hooks: [], + inspectors: [], + }; - this.tags = new ApplicationTags({ application: this.applicationInfo, logger: this.logger }); - this.userAgentHeaderName = internalOptions.userAgentHeaderName ?? 'user-agent'; - this.trackEventModifier = internalOptions.trackEventModifier ?? ((event) => event); + // Validate options and update values + Object.entries(pristineOptions).forEach(([k, v]) => { + const validator = validators[k as keyof LDOptions]; - this.credentialType = internalOptions.credentialType; - this.getImplementationHooks = internalOptions.getImplementationHooks; - } + if (validator) { + if (!validator.is(v)) { + const validatorType = validator.getType(); + + if (validatorType === 'boolean') { + logger.warn(OptionMessages.wrongOptionTypeBoolean(k, typeof v)); + values[k] = !!v; + } else if (validatorType === 'boolean | undefined | null') { + logger.warn(OptionMessages.wrongOptionTypeBoolean(k, typeof v)); - private _validateTypesAndNames(pristineOptions: LDOptions): string[] { - const errors: string[] = []; - - Object.entries(pristineOptions).forEach(([k, v]) => { - const validator = validators[k as keyof LDOptions]; - - if (validator) { - if (!validator.is(v)) { - const validatorType = validator.getType(); - - if (validatorType === 'boolean') { - errors.push(OptionMessages.wrongOptionTypeBoolean(k, typeof v)); - this[k] = !!v; - } else if (validatorType === 'boolean | undefined | null') { - errors.push(OptionMessages.wrongOptionTypeBoolean(k, typeof v)); - - if (typeof v !== 'boolean' && typeof v !== 'undefined' && v !== null) { - this[k] = !!v; - } - } else if (validator instanceof NumberWithMinimum && TypeValidators.Number.is(v)) { - const { min } = validator as NumberWithMinimum; - errors.push(OptionMessages.optionBelowMinimum(k, v, min)); - this[k] = min; - } else { - errors.push(OptionMessages.wrongOptionType(k, validator.getType(), typeof v)); + if (typeof v !== 'boolean' && typeof v !== 'undefined' && v !== null) { + values[k] = !!v; } - } else if (k === 'logger') { - // Logger already assigned. + } else if (validator instanceof NumberWithMinimum && TypeValidators.Number.is(v)) { + const { min } = validator as NumberWithMinimum; + logger.warn(OptionMessages.optionBelowMinimum(k, v, min)); + values[k] = min; } else { - // if an option is explicitly null, coerce to undefined - this[k] = v ?? undefined; + logger.warn(OptionMessages.wrongOptionType(k, validator.getType(), typeof v)); } + } else if (k === 'logger') { + // Logger already assigned. } else { - errors.push(OptionMessages.unknownOption(k)); + // if an option is explicitly null, coerce to undefined + values[k] = v ?? undefined; } - }); - - return errors; - } + } else { + logger.warn(OptionMessages.unknownOption(k)); + } + }); + + // Remove internal URI properties that shouldn't be on the final Configuration + const { baseUri, eventsUri, streamUri, payloadFilterKey, ...configValues } = values; + + return { + ...configValues, + tags: new ApplicationTags({ application: values.applicationInfo, logger }), + serviceEndpoints: new ServiceEndpoints( + streamUri, + baseUri, + eventsUri, + internalOptions.analyticsEventPath, + internalOptions.diagnosticEventPath, + internalOptions.includeAuthorizationHeader, + payloadFilterKey, + ), + userAgentHeaderName: internalOptions.userAgentHeaderName ?? 'user-agent', + trackEventModifier: internalOptions.trackEventModifier ?? ((event) => event), + credentialType: internalOptions.credentialType, + getImplementationHooks: internalOptions.getImplementationHooks, + }; } diff --git a/packages/shared/sdk-client/src/configuration/index.ts b/packages/shared/sdk-client/src/configuration/index.ts index cdb5c1344..58d7716ad 100644 --- a/packages/shared/sdk-client/src/configuration/index.ts +++ b/packages/shared/sdk-client/src/configuration/index.ts @@ -1,5 +1,6 @@ -import ConfigurationImpl, { +import { Configuration, + createConfiguration, DEFAULT_POLLING, DEFAULT_STREAM, LDClientInternalOptions, @@ -7,7 +8,7 @@ import ConfigurationImpl, { export { Configuration, - ConfigurationImpl, + createConfiguration, LDClientInternalOptions, DEFAULT_POLLING, DEFAULT_STREAM,