Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
cb14960
feat: Add experimental FDv2 configuration (unused)
kinyoklion Mar 10, 2026
9f88376
refactor: Use compound validator for dataSystem with built-in defaults
kinyoklion Mar 10, 2026
a90b962
fix: Lint fixes for dataSystem configuration
kinyoklion Mar 10, 2026
fa111d5
Merge branch 'main' into rlamb/SDK-1935/FDv2-configuration
kinyoklion Mar 10, 2026
73c6f2a
WIP: Example of this working together
keelerm84 Mar 5, 2026
109aeea
Fix interrupted state for non-status codes.
kinyoklion Mar 5, 2026
e7f5049
Tests
kinyoklion Mar 9, 2026
e2821cd
Fix basis.
kinyoklion Mar 9, 2026
a7413c0
rebase data system config
kinyoklion Mar 10, 2026
e40b0d5
Integrate mode switching and caching.
kinyoklion Mar 10, 2026
7bfa7ac
Merge branch 'main' into mk/NOTICKET/testing-fdv2
kinyoklion Mar 11, 2026
e2b7d15
Streaming control interface
kinyoklion Mar 11, 2026
9d242d4
Skip cache when bootstrap is available.
kinyoklion Mar 11, 2026
cee85ee
Add event flush on backgrounding.
kinyoklion Mar 11, 2026
0d92a78
remove flag_eval support; use flag-eval only
keelerm84 Mar 12, 2026
5ee0469
Merge branch 'main' into mk/NOTICKET/testing-fdv2
kinyoklion Mar 12, 2026
cfc1207
Remove debug logging from FDv2DataSource.
kinyoklion Mar 12, 2026
ea3eb1d
Commonize stream control input between browser and RN for FDv2.
kinyoklion Mar 12, 2026
67eff51
Merge branch 'main' into mk/NOTICKET/testing-fdv2
kinyoklion Mar 12, 2026
4f59d39
Increase package size limits
kinyoklion Mar 12, 2026
c603f56
Don't export internals
kinyoklion Mar 12, 2026
2a80fa7
Better type alignment
kinyoklion Mar 12, 2026
4eb5a59
Stricter validation and logging.
kinyoklion Mar 12, 2026
3e287df
Merge branch 'main' into mk/NOTICKET/testing-fdv2
kinyoklion Mar 13, 2026
bd80512
Remove browser specific data manager.
kinyoklion Mar 13, 2026
4824560
Add cache comment
kinyoklion Mar 13, 2026
304e5f3
Ensure streaming is off when setStreaming(false)
kinyoklion Mar 13, 2026
0a03954
Fix mode switching.
kinyoklion Mar 17, 2026
0d1b333
contract test changes - wip
tanderson-ld Mar 17, 2026
88ab6a4
Additional handling.
kinyoklion Mar 17, 2026
70bdf7d
Merge remote-tracking branch 'origin/mk/NOTICKET/testing-fdv2' into m…
kinyoklion Mar 17, 2026
04d2644
Connect withReasons.
kinyoklion Mar 17, 2026
6c1996d
Allow mode customization. Connect withReasons.
kinyoklion Mar 18, 2026
266104a
Trim failing tests.
kinyoklion Mar 18, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/browser.yml
Copy link
Member Author

Choose a reason for hiding this comment

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

For now all the size limits are 10% over the size of the packages in this PR.

After we have everything assembled we can do another pass to decrease size. Most of the size changes probably won't be related to the changes in this PR itself.

We already know that we have some barrel related problems with internal and it is time to fix the server-side exports.

Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
target_file: 'packages/sdk/browser/dist/index.js'
package_name: '@launchdarkly/js-client-sdk'
pr_number: ${{ github.event.number }}
size_limit: 25000
size_limit: 34000

# Contract Tests
- name: Install contract test dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/combined-browser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@ jobs:
target_file: 'packages/sdk/combined-browser/dist/index.js'
package_name: '@launchdarkly/browser'
pr_number: ${{ github.event.number }}
size_limit: 200000
size_limit: 194000
2 changes: 1 addition & 1 deletion .github/workflows/common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ jobs:
target_file: 'packages/shared/common/dist/esm/index.mjs'
package_name: '@launchdarkly/js-sdk-common'
pr_number: ${{ github.event.number }}
size_limit: 26000
size_limit: 29000
2 changes: 1 addition & 1 deletion .github/workflows/sdk-client.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ jobs:
target_file: 'packages/shared/sdk-client/dist/esm/index.mjs'
package_name: '@launchdarkly/js-client-sdk-common'
pr_number: ${{ github.event.number }}
size_limit: 24000
size_limit: 38000
68 changes: 68 additions & 0 deletions packages/sdk/browser/__tests__/BrowserClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: { initialConnectionMode: 'invalid-mode' },
},
platform,
);

// Invalid mode should produce a warning
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('dataSystem.initialConnectionMode'),
);

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/');
});
});
127 changes: 109 additions & 18 deletions packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import {
CommandType,
CreateInstanceParams,
makeLogger,
SDKConfigDataInitializer,
SDKConfigDataSynchronizer,
SDKConfigModeDefinition,
SDKConfigParams,
ClientSideTestHook as TestHook,
ValueType,
Expand All @@ -12,6 +15,59 @@ import {
export const badCommandError = new Error('unsupported command');
export const malformedCommand = new Error('command was malformed');

function translateInitializer(init: SDKConfigDataInitializer): any | undefined {
if (init.polling) {
return {
type: 'polling',
...(init.polling.pollIntervalMs !== undefined && {
pollInterval: init.polling.pollIntervalMs / 1000,
}),
...(init.polling.baseUri && {
endpoints: { pollingBaseUri: init.polling.baseUri },
}),
};
}
return undefined;
}

function translateSynchronizer(sync: SDKConfigDataSynchronizer): any | undefined {
if (sync.streaming) {
return {
type: 'streaming',
...(sync.streaming.initialRetryDelayMs !== undefined && {
initialReconnectDelay: sync.streaming.initialRetryDelayMs / 1000,
}),
...(sync.streaming.baseUri && {
endpoints: { streamingBaseUri: sync.streaming.baseUri },
}),
};
}
if (sync.polling) {
return {
type: 'polling',
...(sync.polling.pollIntervalMs !== undefined && {
pollInterval: sync.polling.pollIntervalMs / 1000,
}),
...(sync.polling.baseUri && {
endpoints: { pollingBaseUri: sync.polling.baseUri },
}),
};
}
return undefined;
}

function translateModeDefinition(modeDef: SDKConfigModeDefinition): any {
const initializers = (modeDef.initializers ?? [])
.map(translateInitializer)
.filter((x: any) => x !== undefined);

const synchronizers = (modeDef.synchronizers ?? [])
.map(translateSynchronizer)
.filter((x: any) => x !== undefined);

return { initializers, synchronizers };
}

function makeSdkConfig(options: SDKConfigParams, tag: string) {
if (!options.clientSide) {
throw new Error('configuration did not include clientSide options');
Expand All @@ -23,30 +79,65 @@ function makeSdkConfig(options: SDKConfigParams, tag: string) {
const cf: LDOptions = {
withReasons: options.clientSide.evaluationReasons,
logger: makeLogger(`${tag}.sdk`),
useReport: options.clientSide.useReport,
useReport: options.clientSide.useReport ?? undefined,
};

if (options.serviceEndpoints) {
cf.streamUri = options.serviceEndpoints.streaming;
cf.baseUri = options.serviceEndpoints.polling;
cf.eventsUri = options.serviceEndpoints.events;
}
if (options.dataSystem?.connectionModeConfig) {
const connMode = options.dataSystem.connectionModeConfig;
const dataSystem: any = {
initialConnectionMode: connMode.initialConnectionMode,
automaticModeSwitching: false,
};

if (connMode.customConnectionModes) {
const connectionModes: Record<string, any> = {};
Object.entries(connMode.customConnectionModes).forEach(([modeName, modeDef]) => {
connectionModes[modeName] = translateModeDefinition(modeDef);

if (options.polling) {
if (options.polling.baseUri) {
cf.baseUri = options.polling.baseUri;
// Also set global endpoint URIs for compatibility with ServiceEndpoints.
(modeDef.synchronizers ?? []).forEach((sync) => {
if (sync.streaming?.baseUri) {
cf.streamUri = sync.streaming.baseUri;
cf.streamInitialReconnectDelay = maybeTime(sync.streaming.initialRetryDelayMs);
}
if (sync.polling?.baseUri) {
cf.baseUri = sync.polling.baseUri;
}
});
(modeDef.initializers ?? []).forEach((init) => {
if (init.polling?.baseUri) {
cf.baseUri = init.polling.baseUri;
}
});
});
dataSystem.connectionModes = connectionModes;
}
}

// Can contain streaming and polling, if streaming is set override the initial connection
// mode. This can be removed when we add JS specific initialization that uses polling
// and then streaming.
if (options.streaming) {
if (options.streaming.baseUri) {
cf.streamUri = options.streaming.baseUri;
(cf as any).dataSystem = dataSystem;

if (options.dataSystem.payloadFilter) {
cf.payloadFilterKey = options.dataSystem.payloadFilter;
}
} else {
if (options.serviceEndpoints) {
cf.streamUri = options.serviceEndpoints.streaming;
cf.baseUri = options.serviceEndpoints.polling;
cf.eventsUri = options.serviceEndpoints.events;
}

if (options.polling) {
if (options.polling.baseUri) {
cf.baseUri = options.polling.baseUri;
}
}

if (options.streaming) {
if (options.streaming.baseUri) {
cf.streamUri = options.streaming.baseUri;
}
cf.streaming = true;
cf.streamInitialReconnectDelay = maybeTime(options.streaming.initialRetryDelayMs);
}
cf.streaming = true;
cf.streamInitialReconnectDelay = maybeTime(options.streaming.initialRetryDelayMs);
}

if (options.events) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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
streaming/requests/query parameters/evaluationReasons set to [none]/REPORT
streaming/requests/query parameters/evaluationReasons set to false/REPORT
streaming/requests/query parameters/evaluationReasons set to true/REPORT
streaming/requests/context properties/single kind minimal/REPORT
streaming/requests/context properties/single kind with all attributes/REPORT
streaming/requests/context properties/multi-kind/REPORT
polling/requests/method and headers/REPORT/http
polling/requests/URL path is computed correctly/no environment filter/base URI has no trailing slash/REPORT
polling/requests/URL path is computed correctly/no environment filter/base URI has a trailing slash/REPORT
polling/requests/query parameters/evaluationReasons set to [none]/REPORT
polling/requests/query parameters/evaluationReasons set to false/REPORT
polling/requests/query parameters/evaluationReasons set to true/REPORT
polling/requests/context properties/single kind minimal/REPORT
polling/requests/context properties/single kind with all attributes/REPORT
polling/requests/context properties/multi-kind/REPORT
tags/stream requests/{"applicationId":null,"applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678"}
tags/stream requests/{"applicationId":null,"applicationVersion":"________________________________________________________________"}
tags/stream requests/{"applicationId":"","applicationVersion":null}
tags/stream requests/{"applicationId":"","applicationVersion":""}
tags/stream requests/{"applicationId":"","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678"}
tags/stream requests/{"applicationId":"","applicationVersion":"________________________________________________________________"}
tags/stream requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678","applicationVersion":null}
tags/stream requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678","applicationVersion":""}
tags/stream requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678"}
tags/stream requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678","applicationVersion":"________________________________________________________________"}
tags/stream requests/{"applicationId":"________________________________________________________________","applicationVersion":null}
tags/stream requests/{"applicationId":"________________________________________________________________","applicationVersion":""}
tags/stream requests/{"applicationId":"________________________________________________________________","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678"}
tags/stream requests/{"applicationId":"________________________________________________________________","applicationVersion":"________________________________________________________________"}
tags/poll requests/{"applicationId":null,"applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678"}
tags/poll requests/{"applicationId":null,"applicationVersion":"________________________________________________________________"}
tags/poll requests/{"applicationId":"","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678"}
tags/poll requests/{"applicationId":"","applicationVersion":"________________________________________________________________"}
tags/poll requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678","applicationVersion":null}
tags/poll requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678","applicationVersion":""}
tags/poll requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678"}
tags/poll requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678","applicationVersion":"________________________________________________________________"}
tags/poll requests/{"applicationId":"________________________________________________________________","applicationVersion":null}
tags/poll requests/{"applicationId":"________________________________________________________________","applicationVersion":""}
tags/poll requests/{"applicationId":"________________________________________________________________","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678"}
tags/poll requests/{"applicationId":"________________________________________________________________","applicationVersion":"________________________________________________________________"}
tags/disallowed characters
60 changes: 59 additions & 1 deletion packages/sdk/browser/example/index.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,69 @@
body {
margin: 0;
padding: 20px;
background: #373841;
color: white;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
}

#status {
padding: 10px;
margin-bottom: 10px;
background: rgba(255,255,255,0.1);
border-radius: 4px;
}

#flag {
font-size: 1.4em;
padding: 15px;
margin-bottom: 20px;
background: rgba(255,255,255,0.05);
border-radius: 4px;
}

#controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}

#controls > div {
padding: 10px;
background: rgba(255,255,255,0.05);
border-radius: 4px;
}

#controls h3 {
margin: 0 0 8px 0;
font-size: 0.9em;
text-transform: uppercase;
opacity: 0.7;
}

button {
padding: 6px 14px;
margin: 3px 2px;
border: 1px solid rgba(255,255,255,0.3);
border-radius: 4px;
background: rgba(255,255,255,0.1);
color: white;
cursor: pointer;
font-size: 0.9em;
}

button:hover {
background: rgba(255,255,255,0.2);
}

#log {
max-height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 0.8em;
line-height: 1.5;
opacity: 0.8;
}
Loading