Skip to content
Draft
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
21 changes: 19 additions & 2 deletions packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
SDKConfigDataSynchronizer,
SDKConfigModeDefinition,
SDKConfigParams,
SDKConfigPollingParams,
ClientSideTestHook as TestHook,
ValueType,
} from '@launchdarkly/js-contract-test-utils/client';
Expand Down Expand Up @@ -64,7 +65,10 @@ function translateSynchronizer(sync: SDKConfigDataSynchronizer): SynchronizerEnt
return undefined;
}

function translateModeDefinition(modeDef: SDKConfigModeDefinition): ModeDefinition {
function translateModeDefinition(
modeDef: SDKConfigModeDefinition,
fdv1Fallback?: SDKConfigPollingParams | null,
): ModeDefinition {
Comment on lines +68 to +71

@devin-ai-integration devin-ai-integration Bot Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Fallback polling settings are ignored when tests use custom connection modes

The fallback polling settings sent by the test harness are only passed along in the default single-mode path (translateModeDefinition(modeDef, options.dataSystem.fdv1Fallback) at packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts:167) and are dropped for every custom connection mode, so those runs silently use built-in defaults instead of the values the test asked for.
Impact: Tests that combine custom connection modes with fallback settings exercise the wrong endpoint and interval, producing misleading pass/fail results.

How the fallback config is lost in the custom-connection-mode branch

options.dataSystem.fdv1Fallback is a sibling of options.dataSystem.connectionModeConfig (packages/tooling/contract-test-utils/src/types/ConfigParams.ts:30), but in the connectionModeConfig branch each mode is translated with translateModeDefinition(modeDef) and no fallback argument (packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts:150, packages/sdk/node-client/contract-tests/src/sdkClientEntity.ts:156, packages/sdk/react-native/contract-tests/entity/src/ClientEntity.ts:149). Without fdv1Fallback on the ModeDefinition, the SDK falls back to MODE_TABLE defaults (300s poll interval, default polling endpoint) in packages/shared/sdk-client/src/datasource/FDv2DataManagerBase.ts.

Additionally, the new code only emits the fallback config when fdv1Fallback?.baseUri is truthy, so a harness config supplying only pollIntervalMs is discarded entirely. The server-side entity applies the fallback whenever the object is present (packages/sdk/server-node/contract-tests/src/sdkClientEntity.ts), which is the more faithful translation.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

const initializers: InitializerEntry[] = (modeDef.initializers ?? [])
.map(translateInitializer)
.filter((x): x is InitializerEntry => x !== undefined);
Expand All @@ -73,6 +77,19 @@ function translateModeDefinition(modeDef: SDKConfigModeDefinition): ModeDefiniti
.map(translateSynchronizer)
.filter((x): x is SynchronizerEntry => x !== undefined);

if (fdv1Fallback?.baseUri) {
return {
initializers,
synchronizers,
fdv1Fallback: {
...(fdv1Fallback.pollIntervalMs != null && {
pollInterval: fdv1Fallback.pollIntervalMs / 1000,
}),
endpoints: { pollingBaseUri: fdv1Fallback.baseUri },
},
};
}
Comment on lines +80 to +91

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Fallback polling settings are thrown away when no fallback address is given

The fallback polling settings are only kept when an address is present (if (fdv1Fallback?.baseUri) at packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts:80), so a configuration that supplies only a polling frequency is silently dropped.

Impact: Test runs that configure the fallback with just a poll interval get default behavior instead of the requested one, producing confusing failures.

Condition gating on baseUri discards pollIntervalMs-only fallback config

translateModeDefinition returns the mode without any fdv1Fallback whenever baseUri is unset, even if pollIntervalMs was provided. The underlying SDK type FDv1FallbackConfig (packages/shared/sdk-client/src/api/datasource/DataSourceEntry.ts:78-83) makes both pollInterval and endpoints optional, so a fallback with only pollInterval is valid and should be forwarded. The same pattern is duplicated in packages/sdk/node-client/contract-tests/src/sdkClientEntity.ts:84-95 and packages/sdk/react-native/contract-tests/entity/src/ClientEntity.ts:76-87.

Prompt for agents
In translateModeDefinition (packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts, packages/sdk/node-client/contract-tests/src/sdkClientEntity.ts, packages/sdk/react-native/contract-tests/entity/src/ClientEntity.ts) the fdv1Fallback block is only emitted when fdv1Fallback.baseUri is set. Since FDv1FallbackConfig accepts pollInterval without endpoints, a harness config specifying only pollIntervalMs is silently ignored. Consider emitting fdv1Fallback whenever the object is present, including endpoints only when baseUri exists.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


return { initializers, synchronizers };
}

Expand Down Expand Up @@ -147,7 +164,7 @@ function makeSdkConfig(options: SDKConfigParams, tag: string) {
initialConnectionMode: 'streaming',
};
dataSystem.connectionModes = {
streaming: translateModeDefinition(modeDef),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FDv1 fallback skipped for custom modes

Medium Severity

translateModeDefinition now accepts dataSystem.fdv1Fallback, but the connectionModeConfig.customConnectionModes path still calls it without that argument. Harness configs that combine custom modes with fdv1Fallback keep losing the dedicated FDv1 polling URI to applyEndpointOverrides, the same failure this PR fixes on the top-level initializers/synchronizers path.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f296bbf. Configure here.

streaming: translateModeDefinition(modeDef, options.dataSystem.fdv1Fallback),
};
applyEndpointOverrides(modeDef);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export default class TestHarnessWebSocket {
'client-prereq-cycle-detection',
'client-per-context-summaries',
'track-hooks',
'fdv1-fallback',
];

break;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Browser's native EventSource cannot read response headers/status on the streaming
# connection, so it can never observe the directive this test relies on.
tags/FDv1 fallback directive requests

streaming/requests/method and headers/REPORT/http
streaming/requests/URL path is computed correctly/no environment filter/base URI has no trailing slash/REPORT
streaming/requests/URL path is computed correctly/no environment filter/base URI has a trailing slash/REPORT
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Expand Down
4 changes: 1 addition & 3 deletions packages/sdk/node-client/contract-tests/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,8 @@ app.get('/', (req: Request, res: Response) => {
'tls:skip-verify-peer',
'tls:custom-ca',
'wrapper',
'fdv1-fallback',
'client-event-source-http-errors',
// NOTE: this needs additional fixes to the shared
// SDK code to support
// 'fdv1-fallback',
],
});
});
Expand Down
21 changes: 19 additions & 2 deletions packages/sdk/node-client/contract-tests/src/sdkClientEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
SDKConfigDataSynchronizer,
SDKConfigModeDefinition,
SDKConfigParams,
SDKConfigPollingParams,
ValueType,
} from '@launchdarkly/js-contract-test-utils';
import { ClientSideTestHook as TestHook } from '@launchdarkly/js-contract-test-utils/client';
Expand Down Expand Up @@ -68,7 +69,10 @@ function translateSynchronizer(sync: SDKConfigDataSynchronizer): SynchronizerEnt
return undefined;
}

function translateModeDefinition(modeDef: SDKConfigModeDefinition): ModeDefinition {
function translateModeDefinition(
modeDef: SDKConfigModeDefinition,
fdv1Fallback?: SDKConfigPollingParams | null,
): ModeDefinition {
const initializers: InitializerEntry[] = (modeDef.initializers ?? [])
.map(translateInitializer)
.filter((x): x is InitializerEntry => x !== undefined);
Expand All @@ -77,6 +81,19 @@ function translateModeDefinition(modeDef: SDKConfigModeDefinition): ModeDefiniti
.map(translateSynchronizer)
.filter((x): x is SynchronizerEntry => x !== undefined);

if (fdv1Fallback?.baseUri) {
return {
initializers,
synchronizers,
fdv1Fallback: {
...(fdv1Fallback.pollIntervalMs != null && {
pollInterval: fdv1Fallback.pollIntervalMs / 1000,
}),
endpoints: { pollingBaseUri: fdv1Fallback.baseUri },
},
};
}

return { initializers, synchronizers };
}

Expand Down Expand Up @@ -151,7 +168,7 @@ function makeSdkConfig(options: SDKConfigParams, tag: string): LDOptions {
initialConnectionMode: 'streaming',
};
dataSystem.connectionModes = {
streaming: translateModeDefinition(modeDef),
streaming: translateModeDefinition(modeDef, options.dataSystem.fdv1Fallback),
};
applyEndpointOverrides(modeDef);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SDKConfigDataSynchronizer,
SDKConfigModeDefinition,
SDKConfigParams,
SDKConfigPollingParams,
ClientSideTestHook as TestHook,
ValueType,
} from '@launchdarkly/js-contract-test-utils/client';
Expand Down Expand Up @@ -60,7 +61,10 @@ function translateSynchronizer(sync: SDKConfigDataSynchronizer): any | undefined
return undefined;
}

function translateModeDefinition(modeDef: SDKConfigModeDefinition): any {
function translateModeDefinition(
modeDef: SDKConfigModeDefinition,
fdv1Fallback?: SDKConfigPollingParams | null,
): any {
const initializers = (modeDef.initializers ?? [])
.map(translateInitializer)
.filter((x) => x !== undefined);
Expand All @@ -69,6 +73,19 @@ function translateModeDefinition(modeDef: SDKConfigModeDefinition): any {
.map(translateSynchronizer)
.filter((x) => x !== undefined);

if (fdv1Fallback?.baseUri) {
return {
initializers,
synchronizers,
fdv1Fallback: {
...(fdv1Fallback.pollIntervalMs != null && {
pollInterval: fdv1Fallback.pollIntervalMs / 1000,
}),
endpoints: { pollingBaseUri: fdv1Fallback.baseUri },
},
};
}

return { initializers, synchronizers };
}

Expand Down Expand Up @@ -146,7 +163,7 @@ function makeSdkConfig(options: SDKConfigParams, tag: string) {
initialConnectionMode: 'streaming',
};
dataSystem.connectionModes = {
streaming: translateModeDefinition(modeDef),
streaming: translateModeDefinition(modeDef, options.dataSystem.fdv1Fallback),
};
applyEndpointOverrides(modeDef);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export default class TestHarnessWebSocket {
'client-prereq-cycle-detection',
'client-per-context-summaries',
'track-hooks',
'fdv1-fallback',
'client-event-source-http-errors',
];

break;
Expand Down
12 changes: 12 additions & 0 deletions packages/sdk/react-native/contract-tests/suppressions-fdv2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Tests in this file will be skipped by the LaunchDarkly SDK test harness running
# the FDv2-feature branch against the React Native client-side SDK. Add a path per line.
# Lines beginning with '#' are comments.

# RN's forked EventSource (react-native-sse) fires `open` too late (from
# onreadystatechange at readyState DONE, not from onprogress at LOADING) and never
# populates headers on error events, so FDv1 fallback directives carried on stream
# response headers don't reach the SDK in time (or at all, on the error path).
streaming/fdv2/FDv1 fallback directive/directive on streaming error engages FDv1 fallback
streaming/fdv2/FDv1 fallback directive/directive on streaming success applies payload then engages FDv1
streaming/fdv2/FDv1 fallback directive/directive without FDv1 fallback configured halts the data system
tags/FDv1 fallback directive requests
Comment on lines +5 to +12

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is tracked in a separate ticket

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface SDKConfigDataSystem {
useDefaultDataSystem?: boolean;
initializers?: SDKConfigDataInitializer[];
synchronizers?: SDKConfigDataSynchronizer[];
fdv1Fallback?: SDKConfigPollingParams;
payloadFilter?: string;
connectionModeConfig?: SDKConfigConnectionModeConfig;
}
Expand Down
Loading