From 1d9f1d6b208bda110cb70af1ab04e717a8cb8e28 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:38:00 +0000 Subject: [PATCH 1/7] feat: move bootstrap capability to js-client-common (SDK-1874) Move bootstrap handling from platform-specific start() methods to the shared LDClientImpl.identifyResult() method so all client-side SDKs can support flag bootstrapping. - Add bootstrap and bootstrapParsed fields to base LDIdentifyOptions - Add bootstrap parsing/presetFlags logic to identifyResult() execute phase - Remove duplicate bootstrap fields from BrowserIdentifyOptions - Remove duplicate bootstrap fields from ElectronIdentifyOptions - Remove bootstrap logic from BrowserClient.start() - Remove bootstrap logic from ElectronClient.start() - Clean up unused readFlagsFromBootstrap imports from platform SDKs Co-Authored-By: Steven Zhang --- packages/sdk/browser/src/BrowserClient.ts | 15 ---------- .../sdk/browser/src/BrowserIdentifyOptions.ts | 25 +---------------- packages/sdk/electron/src/ElectronClient.ts | 15 ---------- .../electron/src/ElectronIdentifyOptions.ts | 28 ++----------------- .../shared/sdk-client/src/LDClientImpl.ts | 13 +++++++++ .../sdk-client/src/api/LDIdentifyOptions.ts | 19 +++++++++++++ 6 files changed, 36 insertions(+), 79 deletions(-) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 4498a08924..e2c69a87e1 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -18,7 +18,6 @@ import { LDWaitForInitializationOptions, LDWaitForInitializationResult, Platform, - readFlagsFromBootstrap, safeRegisterDebugOverridePlugins, } from '@launchdarkly/js-client-sdk-common'; @@ -274,20 +273,6 @@ class BrowserClientImpl extends LDClientImpl { identifyOptions.bootstrap = options.bootstrap; } - if (identifyOptions?.bootstrap) { - try { - if (!identifyOptions.bootstrapParsed) { - identifyOptions.bootstrapParsed = readFlagsFromBootstrap( - this.logger, - identifyOptions.bootstrap, - ); - } - this.presetFlags(identifyOptions.bootstrapParsed!); - } catch (error) { - this.logger.error('Failed to bootstrap data', error); - } - } - if (!this.initializedPromise) { this.initializedPromise = new Promise((resolve) => { this.initResolve = resolve; diff --git a/packages/sdk/browser/src/BrowserIdentifyOptions.ts b/packages/sdk/browser/src/BrowserIdentifyOptions.ts index 13dabe168f..e1cc74e93d 100644 --- a/packages/sdk/browser/src/BrowserIdentifyOptions.ts +++ b/packages/sdk/browser/src/BrowserIdentifyOptions.ts @@ -1,4 +1,4 @@ -import { ItemDescriptor, LDIdentifyOptions } from '@launchdarkly/js-client-sdk-common'; +import { LDIdentifyOptions } from '@launchdarkly/js-client-sdk-common'; /** * @property sheddable - If true, the identify operation will be sheddable. This means that if multiple identify operations are done, without @@ -13,27 +13,4 @@ export interface BrowserIdentifyOptions extends Omit { this.initResolve = resolve; diff --git a/packages/sdk/electron/src/ElectronIdentifyOptions.ts b/packages/sdk/electron/src/ElectronIdentifyOptions.ts index 75f4a9d3ed..968626579f 100644 --- a/packages/sdk/electron/src/ElectronIdentifyOptions.ts +++ b/packages/sdk/electron/src/ElectronIdentifyOptions.ts @@ -1,29 +1,7 @@ -import { ItemDescriptor, LDIdentifyOptions } from '@launchdarkly/js-client-sdk-common'; +import { LDIdentifyOptions } from '@launchdarkly/js-client-sdk-common'; /** * Options for the identify method when using the Electron SDK. - * Extends the base identify options with bootstrap support. + * Extends the base identify options. */ -export interface ElectronIdentifyOptions extends LDIdentifyOptions { - /** - * The initial set of flags to use until the remote set is retrieved. - * - * Bootstrap data can be generated by server SDKs. When bootstrap data is provided the SDK - * identification operation will complete without waiting for any values from LaunchDarkly and - * the variation calls can be used immediately. - * - * If streaming or polling is configured, a connection will subsequently be established - * to receive live updates. - * - * For more information, see the [SDK Reference Guide](https://docs.launchdarkly.com/sdk/features/bootstrapping#javascript). - */ - bootstrap?: unknown; - - /** - * Parsed bootstrap data used to ensure that the bootstrap data is only parsed once during - * the initialization process. - * - * @hidden - */ - bootstrapParsed?: { [key: string]: ItemDescriptor }; -} +export type ElectronIdentifyOptions = LDIdentifyOptions; diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 9bec7272ba..fe4fcfe740 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -52,6 +52,7 @@ import { } from './evaluation/evaluationDetail'; import createEventProcessor from './events/createEventProcessor'; import EventFactory from './events/EventFactory'; +import { readFlagsFromBootstrap } from './flag-manager/bootstrap'; import DefaultFlagManager, { FlagManager, LDDebugOverride } from './flag-manager/FlagManager'; import { FlagChangeType } from './flag-manager/FlagUpdater'; import { ItemDescriptor } from './flag-manager/ItemDescriptor'; @@ -338,6 +339,18 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult { }, execute: async (beforeResult) => { const { context, checkedContext } = beforeResult!; + + if (identifyOptions?.bootstrap) { + try { + const bootstrapParsed = + identifyOptions.bootstrapParsed ?? + readFlagsFromBootstrap(this.logger, identifyOptions.bootstrap); + this.presetFlags(bootstrapParsed); + } catch (error) { + this.logger.error('Failed to bootstrap data', error); + } + } + if (!checkedContext.valid) { const error = new Error('Context was unspecified or had no key'); this.emitter.emit('error', context, error); diff --git a/packages/shared/sdk-client/src/api/LDIdentifyOptions.ts b/packages/shared/sdk-client/src/api/LDIdentifyOptions.ts index 3faec6820c..20369b8fe7 100644 --- a/packages/shared/sdk-client/src/api/LDIdentifyOptions.ts +++ b/packages/shared/sdk-client/src/api/LDIdentifyOptions.ts @@ -1,3 +1,5 @@ +import { ItemDescriptor } from '../flag-manager/ItemDescriptor'; + export interface LDIdentifyOptions { /** * In seconds. Determines when the identify promise resolves if no flags have been @@ -44,4 +46,21 @@ export interface LDIdentifyOptions { * Defaults to false. */ sheddable?: boolean; + + /** + * The initial set of flags to use until the remote set is retrieved. + * + * Bootstrap data can be generated by server SDKs. When bootstrap data is provided the SDK the + * identification operation will complete without waiting for any values from LaunchDarkly and + * the variation calls can be used immediately. + */ + bootstrap?: unknown; + + /** + * Parsed bootstrap data used to ensure that the bootstrap data is only parsed once during + * the initialization process. + * + * @hidden + */ + bootstrapParsed?: { [key: string]: ItemDescriptor }; } From 5d24f34498f1f0b55ba5055a6f0d83ecfad1035d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:44:32 +0000 Subject: [PATCH 2/7] fix: restore synchronous presetFlags in start() for immediate bootstrap availability The start() methods need to call presetFlags() synchronously so that bootstrap flags are available immediately after start() returns, before the async identify queue executes. The common identifyResult() code also calls presetFlags() for SDKs that use identify() directly without start(). presetFlags() is idempotent so both paths running is safe. Co-Authored-By: Steven Zhang --- packages/sdk/browser/src/BrowserClient.ts | 15 +++++++++++++++ packages/sdk/electron/src/ElectronClient.ts | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index e2c69a87e1..4498a08924 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -18,6 +18,7 @@ import { LDWaitForInitializationOptions, LDWaitForInitializationResult, Platform, + readFlagsFromBootstrap, safeRegisterDebugOverridePlugins, } from '@launchdarkly/js-client-sdk-common'; @@ -273,6 +274,20 @@ class BrowserClientImpl extends LDClientImpl { identifyOptions.bootstrap = options.bootstrap; } + if (identifyOptions?.bootstrap) { + try { + if (!identifyOptions.bootstrapParsed) { + identifyOptions.bootstrapParsed = readFlagsFromBootstrap( + this.logger, + identifyOptions.bootstrap, + ); + } + this.presetFlags(identifyOptions.bootstrapParsed!); + } catch (error) { + this.logger.error('Failed to bootstrap data', error); + } + } + if (!this.initializedPromise) { this.initializedPromise = new Promise((resolve) => { this.initResolve = resolve; diff --git a/packages/sdk/electron/src/ElectronClient.ts b/packages/sdk/electron/src/ElectronClient.ts index 03708e87c7..6098dbb93a 100644 --- a/packages/sdk/electron/src/ElectronClient.ts +++ b/packages/sdk/electron/src/ElectronClient.ts @@ -17,6 +17,7 @@ import { LDIdentifyResult, LDPluginEnvironmentMetadata, LDWaitForInitializationResult, + readFlagsFromBootstrap, } from '@launchdarkly/js-client-sdk-common'; import ElectronDataManager from './ElectronDataManager'; @@ -168,6 +169,20 @@ export class ElectronClient extends LDClientImpl { identifyOptions.bootstrap = options.bootstrap; } + if (identifyOptions.bootstrap) { + try { + if (!identifyOptions.bootstrapParsed) { + identifyOptions.bootstrapParsed = readFlagsFromBootstrap( + this.logger, + identifyOptions.bootstrap, + ); + } + this.presetFlags(identifyOptions.bootstrapParsed!); + } catch (error) { + this.logger.error('Failed to bootstrap data', error); + } + } + if (!this.initializedPromise) { this.initializedPromise = new Promise((resolve) => { this.initResolve = resolve; From 2f622edae3a5f2085e3f8ec1a4e26592494b004a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:28:04 +0000 Subject: [PATCH 3/7] refactor: remove ElectronIdentifyOptions file, use LDIdentifyOptions directly ElectronIdentifyOptions was just a type alias for LDIdentifyOptions after bootstrap fields moved to the base interface. Replace all usages with LDIdentifyOptions from js-client-sdk-common. Co-Authored-By: Steven Zhang --- .../__tests__/ElectronDataManager.test.ts | 3 +-- packages/sdk/electron/src/ElectronClient.ts | 8 ++++---- packages/sdk/electron/src/ElectronDataManager.ts | 15 ++++++--------- .../sdk/electron/src/ElectronIdentifyOptions.ts | 7 ------- packages/sdk/electron/src/LDClient.ts | 11 +++++------ packages/sdk/electron/src/LDCommon.ts | 5 +---- 6 files changed, 17 insertions(+), 32 deletions(-) delete mode 100644 packages/sdk/electron/src/ElectronIdentifyOptions.ts diff --git a/packages/sdk/electron/__tests__/ElectronDataManager.test.ts b/packages/sdk/electron/__tests__/ElectronDataManager.test.ts index f2f69e1a2a..531bea0596 100644 --- a/packages/sdk/electron/__tests__/ElectronDataManager.test.ts +++ b/packages/sdk/electron/__tests__/ElectronDataManager.test.ts @@ -16,7 +16,6 @@ import { } from '@launchdarkly/js-client-sdk-common'; import ElectronDataManager from '../src/ElectronDataManager'; -import type { ElectronIdentifyOptions } from '../src/ElectronIdentifyOptions'; import { ValidatedOptions } from '../src/options'; import ElectronCrypto from '../src/platform/ElectronCrypto'; import ElectronEncoding from '../src/platform/ElectronEncoding'; @@ -200,7 +199,7 @@ describe('given an ElectronDataManager with mocked dependencies', () => { await dataManager.identify(identifyResolve, identifyReject, context, { bootstrap: goodBootstrapData, - } as ElectronIdentifyOptions); + } as LDIdentifyOptions); expect(flagManager.setBootstrap).toHaveBeenCalledWith( context, diff --git a/packages/sdk/electron/src/ElectronClient.ts b/packages/sdk/electron/src/ElectronClient.ts index 6098dbb93a..d8a0e6bfb3 100644 --- a/packages/sdk/electron/src/ElectronClient.ts +++ b/packages/sdk/electron/src/ElectronClient.ts @@ -14,6 +14,7 @@ import { LDEmitterEventName, LDFlagValue, LDHeaders, + LDIdentifyOptions, LDIdentifyResult, LDPluginEnvironmentMetadata, LDWaitForInitializationResult, @@ -21,7 +22,6 @@ import { } from '@launchdarkly/js-client-sdk-common'; import ElectronDataManager from './ElectronDataManager'; -import type { ElectronIdentifyOptions } from './ElectronIdentifyOptions'; import type { ElectronOptions, ElectronOptions as LDOptions } from './ElectronOptions'; import type { LDClient, LDStartOptions } from './LDClient'; import type { LDPlugin } from './LDPlugin'; @@ -156,7 +156,7 @@ export class ElectronClient extends LDClientImpl { return Promise.resolve({ status: 'failed', error: new Error('Initial context not set') }); } - const identifyOptions: ElectronIdentifyOptions = { + const identifyOptions: LDIdentifyOptions = { ...(options?.identifyOptions ?? {}), sheddable: false, }; @@ -197,7 +197,7 @@ export class ElectronClient extends LDClientImpl { override async identifyResult( pristineContext: LDContext, - identifyOptions?: ElectronIdentifyOptions, + identifyOptions?: LDIdentifyOptions, ): Promise { if (!this._startPromise) { this.logger.error( @@ -276,7 +276,7 @@ export function makeClient( off: (key: string, callback: (...args: unknown[]) => void) => impl.off(key as LDEmitterEventName, callback as (...args: unknown[]) => void), flush: () => impl.flush(), - identify: (ctx: LDContext, identifyOptions?: ElectronIdentifyOptions) => + identify: (ctx: LDContext, identifyOptions?: LDIdentifyOptions) => impl.identifyResult(ctx, identifyOptions), getContext: () => impl.getContext(), close: () => impl.close(), diff --git a/packages/sdk/electron/src/ElectronDataManager.ts b/packages/sdk/electron/src/ElectronDataManager.ts index e140c4b736..20abbb452d 100644 --- a/packages/sdk/electron/src/ElectronDataManager.ts +++ b/packages/sdk/electron/src/ElectronDataManager.ts @@ -13,8 +13,6 @@ import { Platform, readFlagsFromBootstrap, } from '@launchdarkly/js-client-sdk-common'; - -import type { ElectronIdentifyOptions } from './ElectronIdentifyOptions'; import type { ValidatedOptions } from './options'; const logTag = '[ElectronDataManager]'; @@ -68,12 +66,11 @@ export default class ElectronDataManager extends BaseDataManager { // When bootstrap is provided, we will resolve the identify immediately. Then we will fallthrough to connect // to the configured connection mode. - const electronIdentifyOptions = identifyOptions as ElectronIdentifyOptions | undefined; - if (electronIdentifyOptions?.bootstrap) { - this._finishIdentifyFromBootstrap(context, electronIdentifyOptions, identifyResolve); + if (identifyOptions?.bootstrap) { + this._finishIdentifyFromBootstrap(context, identifyOptions, identifyResolve); } // Bootstrap path already called resolve so we use this to prevent duplicate resolve calls. - const resolvedFromBootstrap = !!electronIdentifyOptions?.bootstrap; + const resolvedFromBootstrap = !!identifyOptions?.bootstrap; const offline = this.connectionMode === 'offline'; // In offline mode we do not support waiting for results. @@ -109,12 +106,12 @@ export default class ElectronDataManager extends BaseDataManager { private _finishIdentifyFromBootstrap( context: Context, - electronIdentifyOptions: ElectronIdentifyOptions, + identifyOpts: LDIdentifyOptions, identifyResolve: () => void, ): void { - let { bootstrapParsed } = electronIdentifyOptions; + let { bootstrapParsed } = identifyOpts; if (!bootstrapParsed) { - bootstrapParsed = readFlagsFromBootstrap(this.logger, electronIdentifyOptions.bootstrap); + bootstrapParsed = readFlagsFromBootstrap(this.logger, identifyOpts.bootstrap); } this.flagManager.setBootstrap(context, bootstrapParsed); this._debugLog('Identify - Initialization completed from bootstrap'); diff --git a/packages/sdk/electron/src/ElectronIdentifyOptions.ts b/packages/sdk/electron/src/ElectronIdentifyOptions.ts deleted file mode 100644 index 968626579f..0000000000 --- a/packages/sdk/electron/src/ElectronIdentifyOptions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { LDIdentifyOptions } from '@launchdarkly/js-client-sdk-common'; - -/** - * Options for the identify method when using the Electron SDK. - * Extends the base identify options. - */ -export type ElectronIdentifyOptions = LDIdentifyOptions; diff --git a/packages/sdk/electron/src/LDClient.ts b/packages/sdk/electron/src/LDClient.ts index 85303f10f7..986c5653e9 100644 --- a/packages/sdk/electron/src/LDClient.ts +++ b/packages/sdk/electron/src/LDClient.ts @@ -2,17 +2,16 @@ import type { ConnectionMode, LDClient as LDClientBase, LDContext, + LDIdentifyOptions, LDIdentifyResult, LDWaitForInitializationOptions, LDWaitForInitializationResult, } from '@launchdarkly/js-client-sdk-common'; -import type { ElectronIdentifyOptions } from './ElectronIdentifyOptions'; - export interface LDStartOptions extends LDWaitForInitializationOptions { /** * Optional bootstrap data to use for the identify operation. If - * {@link ElectronIdentifyOptions.bootstrap} is provided in identifyOptions, it takes precedence. + * {@link LDIdentifyOptions.bootstrap} is provided in identifyOptions, it takes precedence. */ bootstrap?: unknown; @@ -20,7 +19,7 @@ export interface LDStartOptions extends LDWaitForInitializationOptions { * Optional identify options to use for the first identify. Since the first identify is not * sheddable, the sheddable option is omitted from this type. */ - identifyOptions?: Omit; + identifyOptions?: Omit; } export interface LDClient extends Omit { @@ -31,12 +30,12 @@ export interface LDClient extends Omit { * Must not be called before {@link LDClient.start} has been called. * * @param context The context to identify. - * @param identifyOptions Optional configuration including {@link ElectronIdentifyOptions.bootstrap}. + * @param identifyOptions Optional configuration including {@link LDIdentifyOptions.bootstrap}. * @returns A promise which resolves to an object containing the result of the identify operation. */ identify( context: LDContext, - identifyOptions?: ElectronIdentifyOptions, + identifyOptions?: LDIdentifyOptions, ): Promise; /** diff --git a/packages/sdk/electron/src/LDCommon.ts b/packages/sdk/electron/src/LDCommon.ts index 6e113deb9b..3a17d78719 100644 --- a/packages/sdk/electron/src/LDCommon.ts +++ b/packages/sdk/electron/src/LDCommon.ts @@ -1,10 +1,7 @@ import { BasicLogger, BasicLoggerOptions, LDLogger } from '@launchdarkly/js-client-sdk-common'; -import { ElectronIdentifyOptions as LDIdentifyOptions } from './ElectronIdentifyOptions'; - -export type { LDIdentifyOptions }; - export type { + LDIdentifyOptions, AutoEnvAttributes, BasicLogger, BasicLoggerOptions, From fc2ccfd8275c0011cdd1c84f36807621005ad61a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:30:15 +0000 Subject: [PATCH 4/7] style: fix prettier formatting in ElectronDataManager and LDClient Co-Authored-By: Steven Zhang --- packages/sdk/electron/src/ElectronDataManager.ts | 1 + packages/sdk/electron/src/LDClient.ts | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/sdk/electron/src/ElectronDataManager.ts b/packages/sdk/electron/src/ElectronDataManager.ts index 20abbb452d..048e586b6b 100644 --- a/packages/sdk/electron/src/ElectronDataManager.ts +++ b/packages/sdk/electron/src/ElectronDataManager.ts @@ -13,6 +13,7 @@ import { Platform, readFlagsFromBootstrap, } from '@launchdarkly/js-client-sdk-common'; + import type { ValidatedOptions } from './options'; const logTag = '[ElectronDataManager]'; diff --git a/packages/sdk/electron/src/LDClient.ts b/packages/sdk/electron/src/LDClient.ts index 986c5653e9..4f1815e89f 100644 --- a/packages/sdk/electron/src/LDClient.ts +++ b/packages/sdk/electron/src/LDClient.ts @@ -33,10 +33,7 @@ export interface LDClient extends Omit { * @param identifyOptions Optional configuration including {@link LDIdentifyOptions.bootstrap}. * @returns A promise which resolves to an object containing the result of the identify operation. */ - identify( - context: LDContext, - identifyOptions?: LDIdentifyOptions, - ): Promise; + identify(context: LDContext, identifyOptions?: LDIdentifyOptions): Promise; /** * Starts the client by performing the first identify with the initial context passed to From 877092726b3bb7dee4154882ec7d56e14ebb3aaf Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:51:55 +0000 Subject: [PATCH 5/7] test: add React Native bootstrap unit tests Co-Authored-By: Steven Zhang --- .../ReactNativeLDClient.bootstrap.test.ts | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 packages/sdk/react-native/__tests__/ReactNativeLDClient.bootstrap.test.ts diff --git a/packages/sdk/react-native/__tests__/ReactNativeLDClient.bootstrap.test.ts b/packages/sdk/react-native/__tests__/ReactNativeLDClient.bootstrap.test.ts new file mode 100644 index 0000000000..1a9e3d59d4 --- /dev/null +++ b/packages/sdk/react-native/__tests__/ReactNativeLDClient.bootstrap.test.ts @@ -0,0 +1,155 @@ +import { AutoEnvAttributes, LDLogger } from '@launchdarkly/js-client-sdk-common'; + +import createPlatform from '../src/platform'; +import PlatformCrypto from '../src/platform/crypto'; +import PlatformEncoding from '../src/platform/PlatformEncoding'; +import PlatformInfo from '../src/platform/PlatformInfo'; +import PlatformStorage from '../src/platform/PlatformStorage'; +import ReactNativeLDClient from '../src/ReactNativeLDClient'; + +jest.mock('../src/platform', () => ({ + __esModule: true, + default: jest.fn((logger: LDLogger) => ({ + crypto: new PlatformCrypto(), + info: new PlatformInfo(logger, {}), + requests: { + createEventSource: jest.fn(), + fetch: jest.fn(), + getEventSourceCapabilities: jest.fn(), + }, + encoding: new PlatformEncoding(), + storage: new PlatformStorage(logger), + })), +})); + +const goodBootstrapData = { + 'string-flag': 'is bob', + 'my-boolean-flag': false, + $flagsState: { + 'string-flag': { + variation: 1, + version: 3, + }, + 'my-boolean-flag': { + variation: 1, + version: 11, + }, + }, + $valid: true, +}; + +const goodBootstrapDataWithReasons = { + 'string-flag': 'is bob', + 'my-boolean-flag': false, + json: ['a', 'b', 'c', 'd'], + $flagsState: { + 'string-flag': { + variation: 1, + version: 3, + reason: { kind: 'OFF' }, + }, + 'my-boolean-flag': { + variation: 1, + version: 11, + reason: { kind: 'OFF' }, + }, + json: { + variation: 1, + version: 3, + reason: { kind: 'OFF' }, + }, + }, + $valid: true, +}; + +describe('ReactNativeLDClient bootstrap', () => { + let logger: LDLogger; + + beforeEach(() => { + logger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; + }); + + it('can use bootstrap data with identify', async () => { + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { + sendEvents: false, + initialConnectionMode: 'offline', + logger, + }); + + await client.identify( + { kind: 'user', key: 'bob' }, + { bootstrap: goodBootstrapDataWithReasons }, + ); + + expect(client.jsonVariationDetail('json', undefined)).toEqual({ + reason: { kind: 'OFF' }, + value: ['a', 'b', 'c', 'd'], + variationIndex: 1, + }); + }); + + it('can evaluate string and boolean flags from bootstrap data', async () => { + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { + sendEvents: false, + initialConnectionMode: 'offline', + logger, + }); + + await client.identify({ kind: 'user', key: 'bob' }, { bootstrap: goodBootstrapData }); + + expect(client.stringVariation('string-flag', 'default')).toBe('is bob'); + expect(client.boolVariation('my-boolean-flag', true)).toBe(false); + }); + + it('uses the latest bootstrap data when re-identifying with new bootstrap data', async () => { + const newBootstrapData = { + 'string-flag': 'is alice', + 'my-boolean-flag': true, + $flagsState: { + 'string-flag': { + variation: 1, + version: 4, + }, + 'my-boolean-flag': { + variation: 0, + version: 12, + }, + }, + $valid: true, + }; + + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { + sendEvents: false, + initialConnectionMode: 'offline', + logger, + }); + + await client.identify({ kind: 'user', key: 'bob' }, { bootstrap: goodBootstrapData }); + + expect(client.stringVariation('string-flag', 'default')).toBe('is bob'); + expect(client.boolVariation('my-boolean-flag', false)).toBe(false); + + await client.identify({ kind: 'user', key: 'alice' }, { bootstrap: newBootstrapData }); + + expect(client.stringVariation('string-flag', 'default')).toBe('is alice'); + expect(client.boolVariation('my-boolean-flag', false)).toBe(true); + }); + + it('returns defaults when no bootstrap data is provided', async () => { + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { + sendEvents: false, + initialConnectionMode: 'offline', + logger, + }); + + await client.identify({ kind: 'user', key: 'bob' }); + + expect(client.stringVariation('string-flag', 'default')).toBe('default'); + expect(client.boolVariation('my-boolean-flag', true)).toBe(true); + }); +}); From e97d4d86a84178dfe2f81f0fd63d459602ad8018 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:54:58 +0000 Subject: [PATCH 6/7] fix: remove unused createPlatform import in bootstrap test Co-Authored-By: Steven Zhang --- .../react-native/__tests__/ReactNativeLDClient.bootstrap.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/sdk/react-native/__tests__/ReactNativeLDClient.bootstrap.test.ts b/packages/sdk/react-native/__tests__/ReactNativeLDClient.bootstrap.test.ts index 1a9e3d59d4..156b80ac82 100644 --- a/packages/sdk/react-native/__tests__/ReactNativeLDClient.bootstrap.test.ts +++ b/packages/sdk/react-native/__tests__/ReactNativeLDClient.bootstrap.test.ts @@ -1,6 +1,5 @@ import { AutoEnvAttributes, LDLogger } from '@launchdarkly/js-client-sdk-common'; -import createPlatform from '../src/platform'; import PlatformCrypto from '../src/platform/crypto'; import PlatformEncoding from '../src/platform/PlatformEncoding'; import PlatformInfo from '../src/platform/PlatformInfo'; From 93e83c4569b78c6556658cf019f0d02c970c2939 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:02:46 +0000 Subject: [PATCH 7/7] fix: address Bugbot issues - move bootstrap to MobileDataManager, remove presetFlags from identifyResult - Remove presetFlags() from LDClientImpl.identifyResult() to fix change event suppression on re-identify with bootstrap (Browser/Electron regression) - Add _finishIdentifyFromBootstrap() to MobileDataManager so React Native resolves identify immediately when bootstrap is provided, matching the pattern used by Browser/Electron data managers Co-Authored-By: Steven Zhang --- .../sdk/react-native/src/MobileDataManager.ts | 30 +++++++++++++++++-- .../shared/sdk-client/src/LDClientImpl.ts | 12 -------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/sdk/react-native/src/MobileDataManager.ts b/packages/sdk/react-native/src/MobileDataManager.ts index 04d0a5d6fb..0ba41a69ba 100644 --- a/packages/sdk/react-native/src/MobileDataManager.ts +++ b/packages/sdk/react-native/src/MobileDataManager.ts @@ -11,6 +11,7 @@ import { LDIdentifyOptions, makeRequestor, Platform, + readFlagsFromBootstrap, } from '@launchdarkly/js-client-sdk-common'; import { ValidatedOptions } from './options'; @@ -63,12 +64,20 @@ export default class MobileDataManager extends BaseDataManager { return; } this.context = context; + + // When bootstrap is provided, resolve identify immediately then fall through to connect. + if (identifyOptions?.bootstrap) { + this._finishIdentifyFromBootstrap(context, identifyOptions, identifyResolve); + } + // Bootstrap path already called resolve so we use this to prevent duplicate resolve calls. + const resolvedFromBootstrap = !!identifyOptions?.bootstrap; + const offline = this.connectionMode === 'offline'; // In offline mode we do not support waiting for results. const waitForNetworkResults = !!identifyOptions?.waitForNetworkResults && !offline; const loadedFromCache = await this.flagManager.loadCached(context); - if (loadedFromCache && !waitForNetworkResults) { + if (loadedFromCache && !waitForNetworkResults && !resolvedFromBootstrap) { this._debugLog('Identify completing with cached flags'); identifyResolve(); } @@ -85,7 +94,9 @@ export default class MobileDataManager extends BaseDataManager { this._debugLog( 'Offline identify - no cached flags, using defaults or already loaded flags.', ); - identifyResolve(); + if (!resolvedFromBootstrap) { + identifyResolve(); + } } } else { // Context has been validated in LDClientImpl.identify @@ -93,6 +104,21 @@ export default class MobileDataManager extends BaseDataManager { } } + private _finishIdentifyFromBootstrap( + context: Context, + identifyOpts: LDIdentifyOptions, + identifyResolve: () => void, + ): void { + let { bootstrapParsed } = identifyOpts; + if (!bootstrapParsed) { + bootstrapParsed = readFlagsFromBootstrap(this.logger, identifyOpts.bootstrap); + } + this.flagManager.setBootstrap(context, bootstrapParsed); + this._debugLog('Identify - Initialization completed from bootstrap'); + + identifyResolve(); + } + private _setupConnection( context: Context, identifyResolve?: () => void, diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index fe4fcfe740..beecf992fb 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -52,7 +52,6 @@ import { } from './evaluation/evaluationDetail'; import createEventProcessor from './events/createEventProcessor'; import EventFactory from './events/EventFactory'; -import { readFlagsFromBootstrap } from './flag-manager/bootstrap'; import DefaultFlagManager, { FlagManager, LDDebugOverride } from './flag-manager/FlagManager'; import { FlagChangeType } from './flag-manager/FlagUpdater'; import { ItemDescriptor } from './flag-manager/ItemDescriptor'; @@ -340,17 +339,6 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult { execute: async (beforeResult) => { const { context, checkedContext } = beforeResult!; - if (identifyOptions?.bootstrap) { - try { - const bootstrapParsed = - identifyOptions.bootstrapParsed ?? - readFlagsFromBootstrap(this.logger, identifyOptions.bootstrap); - this.presetFlags(bootstrapParsed); - } catch (error) { - this.logger.error('Failed to bootstrap data', error); - } - } - if (!checkedContext.valid) { const error = new Error('Context was unspecified or had no key'); this.emitter.emit('error', context, error);