diff --git a/packages/sdk/browser/__tests__/BrowserClient.test.ts b/packages/sdk/browser/__tests__/BrowserClient.test.ts index f105205f8a..391f8829c4 100644 --- a/packages/sdk/browser/__tests__/BrowserClient.test.ts +++ b/packages/sdk/browser/__tests__/BrowserClient.test.ts @@ -872,4 +872,72 @@ describe('given a mock platform for a BrowserClient', () => { // Verify that no fetch calls were made expect(platform.requests.fetch.mock.calls.length).toBe(0); }); + + it('uses FDv1 endpoints when dataSystem is not set', async () => { + const client = makeClient( + 'client-side-id', + { key: 'user-key', kind: 'user' }, + AutoEnvAttributes.Disabled, + { streaming: false, logger, diagnosticOptOut: true, sendEvents: false, fetchGoals: false }, + platform, + ); + + await client.start(); + + const fetchUrl = platform.requests.fetch.mock.calls[0][0]; + expect(fetchUrl).toContain('/sdk/evalx/'); + expect(fetchUrl).not.toContain('/sdk/poll/eval'); + }); + + it('uses FDv2 endpoints when dataSystem is set', async () => { + const client = makeClient( + 'client-side-id', + { key: 'user-key', kind: 'user' }, + AutoEnvAttributes.Disabled, + { + streaming: false, + logger, + diagnosticOptOut: true, + sendEvents: false, + fetchGoals: false, + // @ts-ignore dataSystem is @internal + dataSystem: {}, + }, + platform, + ); + + await client.start(); + + const fetchUrl = platform.requests.fetch.mock.calls[0][0]; + expect(fetchUrl).toContain('/sdk/poll/eval/'); + }); + + it('validates dataSystem options and applies browser defaults', async () => { + const client = makeClient( + 'client-side-id', + { key: 'user-key', kind: 'user' }, + AutoEnvAttributes.Disabled, + { + streaming: false, + logger, + diagnosticOptOut: true, + sendEvents: false, + fetchGoals: false, + // @ts-ignore dataSystem is @internal + dataSystem: { backgroundConnectionMode: 'invalid-mode' }, + }, + platform, + ); + + // Invalid mode should produce a warning + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('dataSystem.backgroundConnectionMode'), + ); + + await client.start(); + + // Should still use FDv2 — invalid sub-fields fall back to defaults, not disable FDv2 + const fetchUrl = platform.requests.fetch.mock.calls[0][0]; + expect(fetchUrl).toContain('/sdk/poll/eval/'); + }); }); diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index a9de2937c1..5c4b3c55fc 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -2,11 +2,15 @@ import { AutoEnvAttributes, BasicLogger, BROWSER_DATA_SYSTEM_DEFAULTS, + BROWSER_TRANSITION_TABLE, browserFdv1Endpoints, Configuration, + createDefaultSourceFactoryProvider, + createFDv2DataManagerBase, FlagManager, Hook, internal, + LDIdentifyOptions as LDBaseIdentifyOptions, LDClientImpl, LDContext, LDEmitter, @@ -17,8 +21,10 @@ import { LDPluginEnvironmentMetadata, LDWaitForInitializationOptions, LDWaitForInitializationResult, + MODE_TABLE, Platform, readFlagsFromBootstrap, + resolveForegroundMode, safeRegisterDebugOverridePlugins, } from '@launchdarkly/js-client-sdk-common'; @@ -78,57 +84,89 @@ class BrowserClientImpl extends LDClientImpl { const { eventUrlTransformer } = validatedBrowserOptions; const endpoints = browserFdv1Endpoints(clientSideId); - super( - clientSideId, - autoEnvAttributes, - platform, - baseOptionsWithDefaults, - ( - flagManager: FlagManager, - configuration: Configuration, - baseHeaders: LDHeaders, - emitter: LDEmitter, - diagnosticsManager?: internal.DiagnosticsManager, - ) => - new BrowserDataManager( + const dataManagerFactory = ( + flagManager: FlagManager, + configuration: Configuration, + baseHeaders: LDHeaders, + emitter: LDEmitter, + diagnosticsManager?: internal.DiagnosticsManager, + ) => { + if (configuration.dataSystem) { + return createFDv2DataManagerBase({ platform, flagManager, - clientSideId, - configuration, - validatedBrowserOptions, - endpoints.polling, - endpoints.streaming, + credential: clientSideId, + config: configuration, baseHeaders, emitter, - diagnosticsManager, - ), - { - // This logic is derived from https://github.com/launchdarkly/js-sdk-common/blob/main/src/PersistentFlagStore.js - getLegacyStorageKeys: () => - getAllStorageKeys().filter((key) => key.startsWith(`ld:${clientSideId}:`)), - analyticsEventPath: `/events/bulk/${clientSideId}`, - diagnosticEventPath: `/events/diagnostic/${clientSideId}`, - includeAuthorizationHeader: false, - highTimeoutThreshold: 5, - userAgentHeaderName: 'x-launchdarkly-user-agent', - dataSystemDefaults: BROWSER_DATA_SYSTEM_DEFAULTS, - trackEventModifier: (event: internal.InputCustomEvent) => - new internal.InputCustomEvent( - event.context, - event.key, - event.data, - event.metricValue, - event.samplingRatio, - eventUrlTransformer(getHref()), + transitionTable: BROWSER_TRANSITION_TABLE, + foregroundMode: resolveForegroundMode( + configuration.dataSystem, + BROWSER_DATA_SYSTEM_DEFAULTS, ), - getImplementationHooks: (environmentMetadata: LDPluginEnvironmentMetadata) => - internal.safeGetHooks(logger, environmentMetadata, validatedBrowserOptions.plugins), - credentialType: 'clientSideId', - }, - ); + backgroundMode: undefined, + modeTable: MODE_TABLE, + sourceFactoryProvider: createDefaultSourceFactoryProvider(), + fdv1Endpoints: browserFdv1Endpoints(clientSideId), + buildQueryParams: (identifyOptions?: LDBaseIdentifyOptions) => { + const params: { key: string; value: string }[] = [{ key: 'auth', value: clientSideId }]; + const browserOpts = identifyOptions as LDIdentifyOptions | undefined; + if (browserOpts?.hash) { + params.push({ key: 'h', value: browserOpts.hash }); + } + return params; + }, + }); + } + + return new BrowserDataManager( + platform, + flagManager, + clientSideId, + configuration, + validatedBrowserOptions, + endpoints.polling, + endpoints.streaming, + baseHeaders, + emitter, + diagnosticsManager, + ); + }; + + super(clientSideId, autoEnvAttributes, platform, baseOptionsWithDefaults, dataManagerFactory, { + // This logic is derived from https://github.com/launchdarkly/js-sdk-common/blob/main/src/PersistentFlagStore.js + getLegacyStorageKeys: () => + getAllStorageKeys().filter((key) => key.startsWith(`ld:${clientSideId}:`)), + analyticsEventPath: `/events/bulk/${clientSideId}`, + diagnosticEventPath: `/events/diagnostic/${clientSideId}`, + includeAuthorizationHeader: false, + highTimeoutThreshold: 5, + userAgentHeaderName: 'x-launchdarkly-user-agent', + dataSystemDefaults: BROWSER_DATA_SYSTEM_DEFAULTS, + trackEventModifier: (event: internal.InputCustomEvent) => + new internal.InputCustomEvent( + event.context, + event.key, + event.data, + event.metricValue, + event.samplingRatio, + eventUrlTransformer(getHref()), + ), + getImplementationHooks: (environmentMetadata: LDPluginEnvironmentMetadata) => + internal.safeGetHooks(logger, environmentMetadata, validatedBrowserOptions.plugins), + credentialType: 'clientSideId', + }); this.setEventSendingEnabled(true, false); + // Forward the browser streaming option to the FDv2 data manager so that + // an explicit streaming: false prevents auto-promotion to streaming. + if (validatedBrowserOptions.streaming !== undefined) { + this.dataManager.setForcedStreaming?.(validatedBrowserOptions.streaming); + } + + this.dataManager.setFlushCallback?.(() => this.flush()); + this._plugins = validatedBrowserOptions.plugins; if (validatedBrowserOptions.fetchGoals) { @@ -281,18 +319,14 @@ class BrowserClientImpl extends LDClientImpl { } setStreaming(streaming?: boolean): void { - // With FDv2 we may want to consider if we support connection mode directly. - // Maybe with an extension to connection mode for 'automatic'. - const browserDataManager = this.dataManager as BrowserDataManager; - browserDataManager.setForcedStreaming(streaming); + this.dataManager.setForcedStreaming?.(streaming); } private _updateAutomaticStreamingState() { - const browserDataManager = this.dataManager as BrowserDataManager; const hasListeners = this.emitter .eventNames() .some((name) => name.startsWith('change:') || name === 'change'); - browserDataManager.setAutomaticStreamingState(hasListeners); + this.dataManager.setAutomaticStreamingState?.(hasListeners); } override on(eventName: LDEmitterEventName, listener: Function): void { diff --git a/packages/shared/sdk-client/src/datasource/LDClientDataSystemOptions.ts b/packages/shared/sdk-client/src/datasource/LDClientDataSystemOptions.ts index 430b057db7..bfb29b4b36 100644 --- a/packages/shared/sdk-client/src/datasource/LDClientDataSystemOptions.ts +++ b/packages/shared/sdk-client/src/datasource/LDClientDataSystemOptions.ts @@ -1,6 +1,11 @@ import { TypeValidators } from '@launchdarkly/js-sdk-common'; -import type { PlatformDataSystemDefaults } from '../api/datasource'; +import type FDv2ConnectionMode from '../api/datasource/FDv2ConnectionMode'; +import type { + LDClientDataSystemOptions, + ManualModeSwitching, + PlatformDataSystemDefaults, +} from '../api/datasource/LDClientDataSystemOptions'; import { anyOf, validatorOf } from '../configuration/validateOptions'; import { connectionModesValidator, connectionModeValidator } from './ConnectionModeConfig'; @@ -56,8 +61,30 @@ const DESKTOP_DATA_SYSTEM_DEFAULTS: PlatformDataSystemDefaults = { automaticModeSwitching: false, }; +function isManualModeSwitching( + value: LDClientDataSystemOptions['automaticModeSwitching'], +): value is ManualModeSwitching { + return typeof value === 'object' && value !== null && 'type' in value && value.type === 'manual'; +} + +/** + * Resolve the foreground connection mode from a validated data system config + * and platform defaults. Uses the mode from `ManualModeSwitching` when present, + * otherwise falls back to the platform default. + */ +function resolveForegroundMode( + dataSystem: LDClientDataSystemOptions, + defaults: PlatformDataSystemDefaults, +): FDv2ConnectionMode { + if (isManualModeSwitching(dataSystem.automaticModeSwitching)) { + return dataSystem.automaticModeSwitching.initialConnectionMode; + } + return defaults.foregroundConnectionMode; +} + export { dataSystemValidators, + resolveForegroundMode, BROWSER_DATA_SYSTEM_DEFAULTS, MOBILE_DATA_SYSTEM_DEFAULTS, DESKTOP_DATA_SYSTEM_DEFAULTS, diff --git a/packages/shared/sdk-client/src/index.ts b/packages/shared/sdk-client/src/index.ts index ce5289524a..3b53da181d 100644 --- a/packages/shared/sdk-client/src/index.ts +++ b/packages/shared/sdk-client/src/index.ts @@ -104,6 +104,7 @@ export type { DataSourceStatusManager } from './datasource/DataSourceStatusManag // FDv2 data system validators and platform defaults. export { dataSystemValidators, + resolveForegroundMode, BROWSER_DATA_SYSTEM_DEFAULTS, MOBILE_DATA_SYSTEM_DEFAULTS, DESKTOP_DATA_SYSTEM_DEFAULTS,