Skip to content

Commit 0b855f0

Browse files
authored
feat: wire FDv2 data manager into BrowserClient (#1222)
## Summary - Conditionally use `FDv2DataManagerBase` when `dataSystem` config is present, falling back to `BrowserDataManager` for FDv1 - Derive foreground mode from `ManualModeSwitching.initialConnectionMode` or browser platform default - Use optional chaining for `setForcedStreaming`/`setAutomaticStreamingState`/`setFlushCallback` via the `DataManager` interface methods - Update `combined-browser.yml` bundle size limit for FDv2 code paths Stacked on #1210. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces a conditional switch between FDv1 and FDv2 data paths in `BrowserClient`, which can affect endpoint selection and streaming behavior. Risk is mitigated by fallback to FDv1 when `dataSystem` is unset and added tests covering routing/validation behavior. > > **Overview** > **BrowserClient now conditionally uses FDv2**: when `configuration.dataSystem` is present, it instantiates `createFDv2DataManagerBase` (using `BROWSER_TRANSITION_TABLE`, `MODE_TABLE`, and FDv2 query params), otherwise it continues using the existing `BrowserDataManager`/FDv1 endpoints. > > **Streaming/flush integration is generalized** by calling `setForcedStreaming`, `setAutomaticStreamingState`, and `setFlushCallback` via optional chaining so the same client logic works across FDv1/FDv2 managers; explicit `streaming: false` is forwarded to prevent FDv2 auto-promotion. > > Adds `resolveForegroundMode` to derive FDv2 foreground mode from `ManualModeSwitching.initialConnectionMode` (or platform defaults), exports it from `sdk-client`, and extends browser tests to assert FDv1 vs FDv2 endpoint selection and that invalid `dataSystem` sub-options warn but still enable FDv2. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 04afb4e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 8f8051c commit 0b855f0

4 files changed

Lines changed: 180 additions & 50 deletions

File tree

packages/sdk/browser/__tests__/BrowserClient.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,4 +872,72 @@ describe('given a mock platform for a BrowserClient', () => {
872872
// Verify that no fetch calls were made
873873
expect(platform.requests.fetch.mock.calls.length).toBe(0);
874874
});
875+
876+
it('uses FDv1 endpoints when dataSystem is not set', async () => {
877+
const client = makeClient(
878+
'client-side-id',
879+
{ key: 'user-key', kind: 'user' },
880+
AutoEnvAttributes.Disabled,
881+
{ streaming: false, logger, diagnosticOptOut: true, sendEvents: false, fetchGoals: false },
882+
platform,
883+
);
884+
885+
await client.start();
886+
887+
const fetchUrl = platform.requests.fetch.mock.calls[0][0];
888+
expect(fetchUrl).toContain('/sdk/evalx/');
889+
expect(fetchUrl).not.toContain('/sdk/poll/eval');
890+
});
891+
892+
it('uses FDv2 endpoints when dataSystem is set', async () => {
893+
const client = makeClient(
894+
'client-side-id',
895+
{ key: 'user-key', kind: 'user' },
896+
AutoEnvAttributes.Disabled,
897+
{
898+
streaming: false,
899+
logger,
900+
diagnosticOptOut: true,
901+
sendEvents: false,
902+
fetchGoals: false,
903+
// @ts-ignore dataSystem is @internal
904+
dataSystem: {},
905+
},
906+
platform,
907+
);
908+
909+
await client.start();
910+
911+
const fetchUrl = platform.requests.fetch.mock.calls[0][0];
912+
expect(fetchUrl).toContain('/sdk/poll/eval/');
913+
});
914+
915+
it('validates dataSystem options and applies browser defaults', async () => {
916+
const client = makeClient(
917+
'client-side-id',
918+
{ key: 'user-key', kind: 'user' },
919+
AutoEnvAttributes.Disabled,
920+
{
921+
streaming: false,
922+
logger,
923+
diagnosticOptOut: true,
924+
sendEvents: false,
925+
fetchGoals: false,
926+
// @ts-ignore dataSystem is @internal
927+
dataSystem: { backgroundConnectionMode: 'invalid-mode' },
928+
},
929+
platform,
930+
);
931+
932+
// Invalid mode should produce a warning
933+
expect(logger.warn).toHaveBeenCalledWith(
934+
expect.stringContaining('dataSystem.backgroundConnectionMode'),
935+
);
936+
937+
await client.start();
938+
939+
// Should still use FDv2 — invalid sub-fields fall back to defaults, not disable FDv2
940+
const fetchUrl = platform.requests.fetch.mock.calls[0][0];
941+
expect(fetchUrl).toContain('/sdk/poll/eval/');
942+
});
875943
});

packages/sdk/browser/src/BrowserClient.ts

Lines changed: 83 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import {
22
AutoEnvAttributes,
33
BasicLogger,
44
BROWSER_DATA_SYSTEM_DEFAULTS,
5+
BROWSER_TRANSITION_TABLE,
56
browserFdv1Endpoints,
67
Configuration,
8+
createDefaultSourceFactoryProvider,
9+
createFDv2DataManagerBase,
710
FlagManager,
811
Hook,
912
internal,
13+
LDIdentifyOptions as LDBaseIdentifyOptions,
1014
LDClientImpl,
1115
LDContext,
1216
LDEmitter,
@@ -17,8 +21,10 @@ import {
1721
LDPluginEnvironmentMetadata,
1822
LDWaitForInitializationOptions,
1923
LDWaitForInitializationResult,
24+
MODE_TABLE,
2025
Platform,
2126
readFlagsFromBootstrap,
27+
resolveForegroundMode,
2228
safeRegisterDebugOverridePlugins,
2329
} from '@launchdarkly/js-client-sdk-common';
2430

@@ -78,57 +84,89 @@ class BrowserClientImpl extends LDClientImpl {
7884
const { eventUrlTransformer } = validatedBrowserOptions;
7985
const endpoints = browserFdv1Endpoints(clientSideId);
8086

81-
super(
82-
clientSideId,
83-
autoEnvAttributes,
84-
platform,
85-
baseOptionsWithDefaults,
86-
(
87-
flagManager: FlagManager,
88-
configuration: Configuration,
89-
baseHeaders: LDHeaders,
90-
emitter: LDEmitter,
91-
diagnosticsManager?: internal.DiagnosticsManager,
92-
) =>
93-
new BrowserDataManager(
87+
const dataManagerFactory = (
88+
flagManager: FlagManager,
89+
configuration: Configuration,
90+
baseHeaders: LDHeaders,
91+
emitter: LDEmitter,
92+
diagnosticsManager?: internal.DiagnosticsManager,
93+
) => {
94+
if (configuration.dataSystem) {
95+
return createFDv2DataManagerBase({
9496
platform,
9597
flagManager,
96-
clientSideId,
97-
configuration,
98-
validatedBrowserOptions,
99-
endpoints.polling,
100-
endpoints.streaming,
98+
credential: clientSideId,
99+
config: configuration,
101100
baseHeaders,
102101
emitter,
103-
diagnosticsManager,
104-
),
105-
{
106-
// This logic is derived from https://github.com/launchdarkly/js-sdk-common/blob/main/src/PersistentFlagStore.js
107-
getLegacyStorageKeys: () =>
108-
getAllStorageKeys().filter((key) => key.startsWith(`ld:${clientSideId}:`)),
109-
analyticsEventPath: `/events/bulk/${clientSideId}`,
110-
diagnosticEventPath: `/events/diagnostic/${clientSideId}`,
111-
includeAuthorizationHeader: false,
112-
highTimeoutThreshold: 5,
113-
userAgentHeaderName: 'x-launchdarkly-user-agent',
114-
dataSystemDefaults: BROWSER_DATA_SYSTEM_DEFAULTS,
115-
trackEventModifier: (event: internal.InputCustomEvent) =>
116-
new internal.InputCustomEvent(
117-
event.context,
118-
event.key,
119-
event.data,
120-
event.metricValue,
121-
event.samplingRatio,
122-
eventUrlTransformer(getHref()),
102+
transitionTable: BROWSER_TRANSITION_TABLE,
103+
foregroundMode: resolveForegroundMode(
104+
configuration.dataSystem,
105+
BROWSER_DATA_SYSTEM_DEFAULTS,
123106
),
124-
getImplementationHooks: (environmentMetadata: LDPluginEnvironmentMetadata) =>
125-
internal.safeGetHooks(logger, environmentMetadata, validatedBrowserOptions.plugins),
126-
credentialType: 'clientSideId',
127-
},
128-
);
107+
backgroundMode: undefined,
108+
modeTable: MODE_TABLE,
109+
sourceFactoryProvider: createDefaultSourceFactoryProvider(),
110+
fdv1Endpoints: browserFdv1Endpoints(clientSideId),
111+
buildQueryParams: (identifyOptions?: LDBaseIdentifyOptions) => {
112+
const params: { key: string; value: string }[] = [{ key: 'auth', value: clientSideId }];
113+
const browserOpts = identifyOptions as LDIdentifyOptions | undefined;
114+
if (browserOpts?.hash) {
115+
params.push({ key: 'h', value: browserOpts.hash });
116+
}
117+
return params;
118+
},
119+
});
120+
}
121+
122+
return new BrowserDataManager(
123+
platform,
124+
flagManager,
125+
clientSideId,
126+
configuration,
127+
validatedBrowserOptions,
128+
endpoints.polling,
129+
endpoints.streaming,
130+
baseHeaders,
131+
emitter,
132+
diagnosticsManager,
133+
);
134+
};
135+
136+
super(clientSideId, autoEnvAttributes, platform, baseOptionsWithDefaults, dataManagerFactory, {
137+
// This logic is derived from https://github.com/launchdarkly/js-sdk-common/blob/main/src/PersistentFlagStore.js
138+
getLegacyStorageKeys: () =>
139+
getAllStorageKeys().filter((key) => key.startsWith(`ld:${clientSideId}:`)),
140+
analyticsEventPath: `/events/bulk/${clientSideId}`,
141+
diagnosticEventPath: `/events/diagnostic/${clientSideId}`,
142+
includeAuthorizationHeader: false,
143+
highTimeoutThreshold: 5,
144+
userAgentHeaderName: 'x-launchdarkly-user-agent',
145+
dataSystemDefaults: BROWSER_DATA_SYSTEM_DEFAULTS,
146+
trackEventModifier: (event: internal.InputCustomEvent) =>
147+
new internal.InputCustomEvent(
148+
event.context,
149+
event.key,
150+
event.data,
151+
event.metricValue,
152+
event.samplingRatio,
153+
eventUrlTransformer(getHref()),
154+
),
155+
getImplementationHooks: (environmentMetadata: LDPluginEnvironmentMetadata) =>
156+
internal.safeGetHooks(logger, environmentMetadata, validatedBrowserOptions.plugins),
157+
credentialType: 'clientSideId',
158+
});
129159

130160
this.setEventSendingEnabled(true, false);
131161

162+
// Forward the browser streaming option to the FDv2 data manager so that
163+
// an explicit streaming: false prevents auto-promotion to streaming.
164+
if (validatedBrowserOptions.streaming !== undefined) {
165+
this.dataManager.setForcedStreaming?.(validatedBrowserOptions.streaming);
166+
}
167+
168+
this.dataManager.setFlushCallback?.(() => this.flush());
169+
132170
this._plugins = validatedBrowserOptions.plugins;
133171

134172
if (validatedBrowserOptions.fetchGoals) {
@@ -281,18 +319,14 @@ class BrowserClientImpl extends LDClientImpl {
281319
}
282320

283321
setStreaming(streaming?: boolean): void {
284-
// With FDv2 we may want to consider if we support connection mode directly.
285-
// Maybe with an extension to connection mode for 'automatic'.
286-
const browserDataManager = this.dataManager as BrowserDataManager;
287-
browserDataManager.setForcedStreaming(streaming);
322+
this.dataManager.setForcedStreaming?.(streaming);
288323
}
289324

290325
private _updateAutomaticStreamingState() {
291-
const browserDataManager = this.dataManager as BrowserDataManager;
292326
const hasListeners = this.emitter
293327
.eventNames()
294328
.some((name) => name.startsWith('change:') || name === 'change');
295-
browserDataManager.setAutomaticStreamingState(hasListeners);
329+
this.dataManager.setAutomaticStreamingState?.(hasListeners);
296330
}
297331

298332
override on(eventName: LDEmitterEventName, listener: Function): void {

packages/shared/sdk-client/src/datasource/LDClientDataSystemOptions.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { TypeValidators } from '@launchdarkly/js-sdk-common';
22

3-
import type { PlatformDataSystemDefaults } from '../api/datasource';
3+
import type FDv2ConnectionMode from '../api/datasource/FDv2ConnectionMode';
4+
import type {
5+
LDClientDataSystemOptions,
6+
ManualModeSwitching,
7+
PlatformDataSystemDefaults,
8+
} from '../api/datasource/LDClientDataSystemOptions';
49
import { anyOf, validatorOf } from '../configuration/validateOptions';
510
import { connectionModesValidator, connectionModeValidator } from './ConnectionModeConfig';
611

@@ -56,8 +61,30 @@ const DESKTOP_DATA_SYSTEM_DEFAULTS: PlatformDataSystemDefaults = {
5661
automaticModeSwitching: false,
5762
};
5863

64+
function isManualModeSwitching(
65+
value: LDClientDataSystemOptions['automaticModeSwitching'],
66+
): value is ManualModeSwitching {
67+
return typeof value === 'object' && value !== null && 'type' in value && value.type === 'manual';
68+
}
69+
70+
/**
71+
* Resolve the foreground connection mode from a validated data system config
72+
* and platform defaults. Uses the mode from `ManualModeSwitching` when present,
73+
* otherwise falls back to the platform default.
74+
*/
75+
function resolveForegroundMode(
76+
dataSystem: LDClientDataSystemOptions,
77+
defaults: PlatformDataSystemDefaults,
78+
): FDv2ConnectionMode {
79+
if (isManualModeSwitching(dataSystem.automaticModeSwitching)) {
80+
return dataSystem.automaticModeSwitching.initialConnectionMode;
81+
}
82+
return defaults.foregroundConnectionMode;
83+
}
84+
5985
export {
6086
dataSystemValidators,
87+
resolveForegroundMode,
6188
BROWSER_DATA_SYSTEM_DEFAULTS,
6289
MOBILE_DATA_SYSTEM_DEFAULTS,
6390
DESKTOP_DATA_SYSTEM_DEFAULTS,

packages/shared/sdk-client/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export type { DataSourceStatusManager } from './datasource/DataSourceStatusManag
104104
// FDv2 data system validators and platform defaults.
105105
export {
106106
dataSystemValidators,
107+
resolveForegroundMode,
107108
BROWSER_DATA_SYSTEM_DEFAULTS,
108109
MOBILE_DATA_SYSTEM_DEFAULTS,
109110
DESKTOP_DATA_SYSTEM_DEFAULTS,

0 commit comments

Comments
 (0)