From 2c89dfab76f3ec3894b0ee6f86394db9140f41d3 Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Wed, 3 Dec 2025 15:53:43 -0600 Subject: [PATCH 1/6] feat: adding `waitForInitialize` to browser 4.x --- packages/sdk/browser/src/BrowserClient.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 90bcc337c..8c30ef11e 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -16,6 +16,7 @@ import { LDHeaders, LDIdentifyResult, LDPluginEnvironmentMetadata, + LDTimeoutError, Platform, } from '@launchdarkly/js-client-sdk-common'; From db57d3f3b941e3580632373417b59da73f967dc1 Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Wed, 10 Dec 2025 11:52:42 -0600 Subject: [PATCH 2/6] feat: implement debug override functionality in client SDK - Added `LDDebugOverride` interface to manage flag value overrides during development. - Introduced `safeRegisterDebugOverridePlugins` function to register plugins with debug capabilities. - Updated `FlagManager` to support debug overrides, including methods to set, remove, and clear overrides. - Enhanced `LDClientImpl` to utilize debug overrides during client initialization. - Refactored `LDPlugin` interface to include optional `registerDebug` method for plugins. This commit will enable `@launchdarkly/toolbar` to use 4.x --- packages/sdk/browser/src/BrowserClient.ts | 6 + packages/sdk/browser/src/LDPlugin.ts | 3 +- packages/sdk/browser/src/common.ts | 1 + .../shared/sdk-client/src/LDClientImpl.ts | 12 +- .../shared/sdk-client/src/api/LDPlugin.ts | 16 +++ packages/shared/sdk-client/src/api/index.ts | 1 + .../src/flag-manager/FlagManager.ts | 132 +++++++++++++++++- .../src/flag-manager/FlagUpdater.ts | 33 +++-- packages/shared/sdk-client/src/index.ts | 4 +- .../safeRegisterDebugOverridePlugins.ts | 24 ++++ 10 files changed, 210 insertions(+), 22 deletions(-) create mode 100644 packages/shared/sdk-client/src/api/LDPlugin.ts create mode 100644 packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 8c30ef11e..50b8e00ec 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -18,6 +18,7 @@ import { LDPluginEnvironmentMetadata, LDTimeoutError, Platform, + safeRegisterDebugOverridePlugins } from '@launchdarkly/js-client-sdk-common'; import { readFlagsFromBootstrap } from './bootstrap'; @@ -212,6 +213,11 @@ class BrowserClientImpl extends LDClientImpl { client, this._plugins || [], ); + + const override = this.getDebugOverrides() + if (override) { + safeRegisterDebugOverridePlugins(this.logger, override, this._plugins || []) + } } override async identify(context: LDContext, identifyOptions?: LDIdentifyOptions): Promise { diff --git a/packages/sdk/browser/src/LDPlugin.ts b/packages/sdk/browser/src/LDPlugin.ts index de06f6db3..fdc162391 100644 --- a/packages/sdk/browser/src/LDPlugin.ts +++ b/packages/sdk/browser/src/LDPlugin.ts @@ -1,5 +1,4 @@ -import { Hook, LDPluginBase } from '@launchdarkly/js-client-sdk-common'; - +import { Hook, LDPlugin as LDPluginBase } from '@launchdarkly/js-client-sdk-common'; import { LDClient } from './LDClient'; /** diff --git a/packages/sdk/browser/src/common.ts b/packages/sdk/browser/src/common.ts index 8c643b83b..763e56a78 100644 --- a/packages/sdk/browser/src/common.ts +++ b/packages/sdk/browser/src/common.ts @@ -43,6 +43,7 @@ export type { LDIdentifyError, LDIdentifyTimeout, LDIdentifyShed, + LDDebugOverride, } from '@launchdarkly/js-client-sdk-common'; /** diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 2c229ab0f..ec9ec9951 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -45,7 +45,7 @@ import { } from './evaluation/evaluationDetail'; import createEventProcessor from './events/createEventProcessor'; import EventFactory from './events/EventFactory'; -import DefaultFlagManager, { FlagManager } from './flag-manager/FlagManager'; +import DefaultFlagManager, { LDDebugOverride, FlagManager } from './flag-manager/FlagManager'; import { FlagChangeType } from './flag-manager/FlagUpdater'; import { ItemDescriptor } from './flag-manager/ItemDescriptor'; import HookRunner from './HookRunner'; @@ -132,7 +132,7 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult { this._flagManager.on((context, flagKeys, type) => { this._handleInspectionChanged(flagKeys, type); - const ldContext = Context.toLDContext(context); + const ldContext = context ? Context.toLDContext(context) : null; this.emitter.emit('change', ldContext, flagKeys); flagKeys.forEach((it) => { this.emitter.emit(`change:${it}`, ldContext); @@ -607,6 +607,14 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult { this._eventProcessor?.sendEvent(event); } + protected getDebugOverrides(): LDDebugOverride | null { + if (this._flagManager.getDebugOverride) { + return this._flagManager.getDebugOverride() + } + + return null + } + private _handleInspectionChanged(flagKeys: Array, type: FlagChangeType) { if (!this._inspectorManager.hasInspectors()) { return; diff --git a/packages/shared/sdk-client/src/api/LDPlugin.ts b/packages/shared/sdk-client/src/api/LDPlugin.ts new file mode 100644 index 000000000..fca4a4d48 --- /dev/null +++ b/packages/shared/sdk-client/src/api/LDPlugin.ts @@ -0,0 +1,16 @@ +import { LDPluginBase } from '@launchdarkly/js-sdk-common'; +import { LDDebugOverride } from '../flag-manager/FlagManager'; + +export interface LDPlugin extends LDPluginBase { + /** + * An optional function called if the plugin wants to register debug capabilities. + * This method allows plugins to receive a debug override interface for + * temporarily overriding flag values during development and testing. + * + * @experimental This interface is experimental and intended for use by LaunchDarkly tools at this time. + * The API may change in future versions. + * + * @param debugOverride The debug override interface instance + */ + registerDebug?(debugOverride: LDDebugOverride): void; +} diff --git a/packages/shared/sdk-client/src/api/index.ts b/packages/shared/sdk-client/src/api/index.ts index 1695ac67a..0705c6104 100644 --- a/packages/shared/sdk-client/src/api/index.ts +++ b/packages/shared/sdk-client/src/api/index.ts @@ -9,3 +9,4 @@ export { ConnectionMode }; export * from './LDIdentifyOptions'; export * from './LDInspection'; export * from './LDIdentifyResult'; +export * from './LDPlugin'; \ No newline at end of file diff --git a/packages/shared/sdk-client/src/flag-manager/FlagManager.ts b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts index abf662131..6bf5d290f 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagManager.ts +++ b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts @@ -1,4 +1,4 @@ -import { Context, LDLogger, Platform } from '@launchdarkly/js-sdk-common'; +import { Context, LDFlagValue, LDLogger, Platform } from '@launchdarkly/js-sdk-common'; import { namespaceForEnvironment } from '../storage/namespaceUtils'; import FlagPersistence from './FlagPersistence'; @@ -64,12 +64,67 @@ export interface FlagManager { * Unregister a flag change callback. */ off(callback: FlagsChangeCallback): void; + + // REVIEWER: My reasoning here is to have the flagmanager implementation determine + // whether or not we can support debug plugins so I put the override methods here. + // Would like some thoughts on this as it is a deviation from previous implementation. + + /** + * Obtain debug override functions that allows plugins + * to manipulate the outcome of the flags managed by + * this manager + * + * @experimental This function is experimental and intended for use by LaunchDarkly tools at this time. + */ + getDebugOverride?(): LDDebugOverride +} + +/** + * Debug interface for plugins that need to override flag values during development. + * This interface provides methods to temporarily override flag values that take + * precedence over the actual flag values from LaunchDarkly. These overrides are + * useful for testing, development, and debugging scenarios. + * + * @experimental This interface is experimental and intended for use by LaunchDarkly tools at this time. + * The API may change in future versions. + */ +export interface LDDebugOverride { + /** + * Set an override value for a flag that takes precedence over the real flag value. + * + * @param flagKey The flag key. + * @param value The override value. + */ + setOverride(flagKey: string, value: LDFlagValue): void; + + /** + * Remove an override value for a flag, reverting to the real flag value. + * + * @param flagKey The flag key. + */ + removeOverride(flagKey: string): void; + + /** + * Clear all override values, reverting all flags to their real values. + */ + clearAllOverrides(): void; + + /** + * Get all currently active flag overrides. + * + * @returns + * An object containing all active overrides as key-value pairs, + * where keys are flag keys and values are the overridden flag values. + * Returns an empty object if no overrides are active. + */ + getAllOverrides(): { [key: string]: ItemDescriptor }; } export default class DefaultFlagManager implements FlagManager { private _flagStore = new DefaultFlagStore(); private _flagUpdater: FlagUpdater; private _flagPersistencePromise: Promise; + private _overrides?: { [key: string]: LDFlagValue }; /** * @param platform implementation of various platform provided functionality @@ -116,11 +171,25 @@ export default class DefaultFlagManager implements FlagManager { } get(key: string): ItemDescriptor | undefined { + if (this._overrides && Object.prototype.hasOwnProperty.call(this._overrides, key)) { + return this._convertValueToOverrideDescripter(this._overrides[key]); + } + return this._flagStore.get(key); } getAll(): { [key: string]: ItemDescriptor } { - return this._flagStore.getAll(); + if (this._overrides) { + return { + ...this._flagStore.getAll(), + ...Object.entries(this._overrides).reduce((acc: {[key: string]: ItemDescriptor}, [key, value]) => { + acc[key] = this._convertValueToOverrideDescripter(value); + return acc + }, {}) + } + } else { + return this._flagStore.getAll(); + } } presetFlags(newFlags: { [key: string]: ItemDescriptor }): void { @@ -152,4 +221,63 @@ export default class DefaultFlagManager implements FlagManager { off(callback: FlagsChangeCallback): void { this._flagUpdater.off(callback); } + + private _convertValueToOverrideDescripter(value: LDFlagValue): ItemDescriptor { + return { + flag: { + value: value, + version: 0 + }, + version: 0 + }; + } + + setOverride(key: string, value: LDFlagValue) { + if (!this._overrides) { + this._overrides = {}; + } + this._overrides[key] = value; + this._flagUpdater.handleFlagChanges(null, [key], 'override'); + } + + removeOverride(flagKey: string) { + if (!this._overrides || !Object.prototype.hasOwnProperty.call(this._overrides, flagKey)) { + return; // No override to remove + } + + delete this._overrides[flagKey]; + + // If no more overrides, reset to undefined for performance + if (Object.keys(this._overrides).length === 0) { + this._overrides = undefined; + } + + this._flagUpdater.handleFlagChanges(null, [flagKey], 'override'); + } + + clearAllOverrides() { + if (!this._overrides) { + return {}; // No overrides to clear, return empty object for consistency + } + + const clearedOverrides = { ...this._overrides }; + this._overrides = undefined; // Reset to undefined + this._flagUpdater.handleFlagChanges(null, Object.keys(clearedOverrides), 'override'); + return clearedOverrides; + } + + getAllOverrides() { + if (!this._overrides) { + return {}; + } + const result = {} as { [key: string]: ItemDescriptor }; + Object.entries(this._overrides).forEach(([key, value]) => { + result[key] = this._convertValueToOverrideDescripter(value); + }); + return result; + } + + getDebugOverride(): LDDebugOverride { + return this as LDDebugOverride; + } } diff --git a/packages/shared/sdk-client/src/flag-manager/FlagUpdater.ts b/packages/shared/sdk-client/src/flag-manager/FlagUpdater.ts index 03366dacf..e0feb5b84 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagUpdater.ts +++ b/packages/shared/sdk-client/src/flag-manager/FlagUpdater.ts @@ -4,7 +4,7 @@ import calculateChangedKeys from './calculateChangedKeys'; import FlagStore from './FlagStore'; import { ItemDescriptor } from './ItemDescriptor'; -export type FlagChangeType = 'init' | 'patch'; +export type FlagChangeType = 'init' | 'patch' | 'override'; /** * This callback indicates that the details associated with one or more flags @@ -20,6 +20,11 @@ export type FlagChangeType = 'init' | 'patch'; * will call a variation method for flag values which you require. */ export type FlagsChangeCallback = ( + // REVIEWER: This is probably not desired, but I think there are some updates + // such as overrides that do not really have a context? Unless I am misunderstanding + // what context is exactly. Being able to support a null context may also help + // with distinguishing between being in the emphemeral state between the start of + // initialization and the end of identification and having an invalid context? context: Context, flagKeys: Array, type: FlagChangeType, @@ -41,19 +46,23 @@ export default class FlagUpdater { this._logger = logger; } + handleFlagChanges(context: Context, keys: string[], type: FlagChangeType): void { + this._changeCallbacks.forEach((callback) => { + try { + callback(context, keys, type); + } catch (err) { + /* intentionally empty */ + } + }); + } + init(context: Context, newFlags: { [key: string]: ItemDescriptor }) { this._activeContext = context; const oldFlags = this._flagStore.getAll(); this._flagStore.init(newFlags); const changed = calculateChangedKeys(oldFlags, newFlags); if (changed.length > 0) { - this._changeCallbacks.forEach((callback) => { - try { - callback(context, changed, 'init'); - } catch (err) { - /* intentionally empty */ - } - }); + this.handleFlagChanges(context, changed, 'init'); } } @@ -78,13 +87,7 @@ export default class FlagUpdater { } this._flagStore.insertOrUpdate(key, item); - this._changeCallbacks.forEach((callback) => { - try { - callback(this._activeContext!, [key], 'patch'); - } catch (err) { - /* intentionally empty */ - } - }); + this.handleFlagChanges(this._activeContext!, [key], 'patch'); return true; } diff --git a/packages/shared/sdk-client/src/index.ts b/packages/shared/sdk-client/src/index.ts index 01f5f672d..5cff89748 100644 --- a/packages/shared/sdk-client/src/index.ts +++ b/packages/shared/sdk-client/src/index.ts @@ -36,10 +36,12 @@ export type { LDIdentifyTimeout, LDIdentifyShed, LDClientIdentifyResult, + LDPlugin, } from './api'; export type { DataManager, DataManagerFactory, ConnectionParams } from './DataManager'; -export type { FlagManager } from './flag-manager/FlagManager'; +export type { FlagManager, LDDebugOverride } from './flag-manager/FlagManager'; +export {safeRegisterDebugOverridePlugins} from './plugins/safeRegisterDebugOverridePlugins'; export type { Configuration } from './configuration/Configuration'; export type { LDEmitter }; diff --git a/packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts b/packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts new file mode 100644 index 000000000..5897d1e59 --- /dev/null +++ b/packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts @@ -0,0 +1,24 @@ +import { internal, LDLogger } from "@launchdarkly/js-sdk-common"; +import { LDPlugin } from "../api"; +import { LDDebugOverride } from "../flag-manager/FlagManager"; + +/** + * Safe register debug override plugins. + * + * @param logger - The logger to use for logging errors. + * @param debugOverride - The debug override to register. + * @param plugins - The plugins to register. + */ +export function safeRegisterDebugOverridePlugins( + logger: LDLogger, + debugOverride: LDDebugOverride, + plugins: LDPlugin[] +): void { + plugins.forEach(plugin => { + try { + plugin.registerDebug?.(debugOverride); + } catch (error) { + logger.error(`Exception thrown registering plugin ${internal.safeGetName(logger, plugin)}.`); + } + }); +}; From fc458c56b9344bd693ee43acadea060d003da401 Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Wed, 10 Dec 2025 12:51:53 -0600 Subject: [PATCH 3/6] test: add unit tests for FlagManager debug override functionality --- .../flag-manager/FlagManager.test.ts | 231 ++++++++++++++++++ .../shared/sdk-client/src/LDClientImpl.ts | 6 +- .../shared/sdk-client/src/api/LDPlugin.ts | 5 +- packages/shared/sdk-client/src/api/index.ts | 2 +- .../src/flag-manager/FlagManager.ts | 24 +- packages/shared/sdk-client/src/index.ts | 2 +- .../safeRegisterDebugOverridePlugins.ts | 13 +- 7 files changed, 259 insertions(+), 24 deletions(-) create mode 100644 packages/shared/sdk-client/__tests__/flag-manager/FlagManager.test.ts diff --git a/packages/shared/sdk-client/__tests__/flag-manager/FlagManager.test.ts b/packages/shared/sdk-client/__tests__/flag-manager/FlagManager.test.ts new file mode 100644 index 000000000..76ef9495c --- /dev/null +++ b/packages/shared/sdk-client/__tests__/flag-manager/FlagManager.test.ts @@ -0,0 +1,231 @@ +import { Context, Crypto, Hasher, LDLogger, Platform, Storage } from '@launchdarkly/js-sdk-common'; + +import DefaultFlagManager from '../../src/flag-manager/FlagManager'; +import { FlagsChangeCallback } from '../../src/flag-manager/FlagUpdater'; +import { ItemDescriptor } from '../../src/flag-manager/ItemDescriptor'; +import { Flag } from '../../src/types'; + +const TEST_SDK_KEY = 'test-sdk-key'; +const TEST_MAX_CACHED_CONTEXTS = 5; + +function makeMockPlatform(storage: Storage, crypto: Crypto): Platform { + return { + storage, + crypto, + info: { + platformData: jest.fn(), + sdkData: jest.fn(), + }, + requests: { + fetch: jest.fn(), + createEventSource: jest.fn(), + getEventSourceCapabilities: jest.fn(), + }, + }; +} + +function makeMemoryStorage(): Storage { + const data = new Map(); + return { + get: async (key: string) => { + const value = data.get(key); + return value !== undefined ? value : null; + }, + set: async (key: string, value: string) => { + data.set(key, value); + }, + clear: async (key: string) => { + data.delete(key); + }, + }; +} + +function makeMockCrypto() { + let counter = 0; + let lastInput = ''; + const hasher: Hasher = { + update: jest.fn((input) => { + lastInput = input; + return hasher; + }), + digest: jest.fn(() => `${lastInput}Hashed`), + }; + + return { + createHash: jest.fn(() => hasher), + createHmac: jest.fn(), + randomUUID: jest.fn(() => { + counter += 1; + return `${counter}`; + }), + }; +} + +function makeMockLogger(): LDLogger { + return { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; +} + +function makeMockFlag(version: number = 1, value: any = 'test-value'): Flag { + return { + version, + flagVersion: version, + value, + variation: 0, + trackEvents: false, + }; +} + +function makeMockItemDescriptor(version: number = 1, value: any = 'test-value'): ItemDescriptor { + return { + version, + flag: makeMockFlag(version, value), + }; +} + +describe('FlagManager override tests', () => { + let flagManager: DefaultFlagManager; + let mockPlatform: Platform; + let mockLogger: LDLogger; + + beforeEach(() => { + mockLogger = makeMockLogger(); + mockPlatform = makeMockPlatform(makeMemoryStorage(), makeMockCrypto()); + flagManager = new DefaultFlagManager( + mockPlatform, + TEST_SDK_KEY, + TEST_MAX_CACHED_CONTEXTS, + mockLogger, + ); + }); + + it('setOverride takes precedence over flag store value', async () => { + const context = Context.fromLDContext({ kind: 'user', key: 'user-key' }); + const flags = { + 'test-flag': makeMockItemDescriptor(1, 'store-value'), + }; + + await flagManager.init(context, flags); + expect(flagManager.get('test-flag')?.flag.value).toBe('store-value'); + + const debugOverride = flagManager.getDebugOverride(); + debugOverride?.setOverride('test-flag', 'override-value'); + + expect(flagManager.get('test-flag')?.flag.value).toBe('override-value'); + }); + + it('setOverride triggers flag change callback', () => { + const mockCallback: FlagsChangeCallback = jest.fn(); + flagManager.on(mockCallback); + + const debugOverride = flagManager.getDebugOverride(); + debugOverride?.setOverride('test-flag', 'override-value'); + + expect(mockCallback).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith(null, ['test-flag'], 'override'); + }); + + it('removeOverride does nothing when override does not exist', () => { + const debugOverride = flagManager.getDebugOverride(); + expect(() => { + debugOverride?.removeOverride('non-existent-flag'); + }).not.toThrow(); + }); + + it('removeOverride reverts to flag store value when override is removed', async () => { + const context = Context.fromLDContext({ kind: 'user', key: 'user-key' }); + const flags = { + 'test-flag': makeMockItemDescriptor(1, 'store-value'), + }; + + await flagManager.init(context, flags); + const debugOverride = flagManager.getDebugOverride(); + debugOverride?.setOverride('test-flag', 'override-value'); + expect(flagManager.get('test-flag')?.flag.value).toBe('override-value'); + + debugOverride?.removeOverride('test-flag'); + expect(flagManager.get('test-flag')?.flag.value).toBe('store-value'); + }); + + it('removeOverride triggers flag change callback', () => { + const mockCallback: FlagsChangeCallback = jest.fn(); + flagManager.on(mockCallback); + + const debugOverride = flagManager.getDebugOverride(); + debugOverride?.setOverride('test-flag', 'override-value'); + debugOverride?.removeOverride('test-flag'); + + expect(mockCallback).toHaveBeenCalledTimes(2); + expect(mockCallback).toHaveBeenNthCalledWith(1, null, ['test-flag'], 'override'); + expect(mockCallback).toHaveBeenNthCalledWith(2, null, ['test-flag'], 'override'); + }); + + it('clearAllOverrides removes all overrides', () => { + const debugOverride = flagManager.getDebugOverride(); + debugOverride?.setOverride('flag1', 'value1'); + debugOverride?.setOverride('flag2', 'value2'); + debugOverride?.setOverride('flag3', 'value3'); + + expect(Object.keys(flagManager.getAllOverrides())).toHaveLength(3); + + debugOverride?.clearAllOverrides(); + expect(Object.keys(flagManager.getAllOverrides())).toHaveLength(0); + }); + + it('clearAllOverrides triggers flag change callback for all flags', () => { + const mockCallback: FlagsChangeCallback = jest.fn(); + flagManager.on(mockCallback); + + const debugOverride = flagManager.getDebugOverride(); + debugOverride?.setOverride('flag1', 'value1'); + debugOverride?.setOverride('flag2', 'value2'); + (mockCallback as jest.Mock).mockClear(); + + debugOverride?.clearAllOverrides(); + expect(mockCallback).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith(null, ['flag1', 'flag2'], 'override'); + }); + + it('getAllOverrides returns all overrides as ItemDescriptors', () => { + const debugOverride = flagManager.getDebugOverride(); + debugOverride?.setOverride('flag1', 'value1'); + debugOverride?.setOverride('flag2', 42); + debugOverride?.setOverride('flag3', true); + + const overrides = debugOverride?.getAllOverrides(); + expect(overrides).toHaveProperty('flag1'); + expect(overrides).toHaveProperty('flag2'); + expect(overrides).toHaveProperty('flag3'); + expect(overrides?.flag1.flag.value).toBe('value1'); + expect(overrides?.flag2.flag.value).toBe(42); + expect(overrides?.flag3.flag.value).toBe(true); + expect(overrides?.flag1.version).toBe(0); + expect(overrides?.flag2.version).toBe(0); + expect(overrides?.flag3.version).toBe(0); + }); + + it('getAll merges overrides with flag store values', async () => { + const context = Context.fromLDContext({ kind: 'user', key: 'user-key' }); + const flags = { + 'store-flag': makeMockItemDescriptor(1, 'store-value'), + 'shared-flag': makeMockItemDescriptor(1, 'store-value'), + }; + + await flagManager.init(context, flags); + const debugOverride = flagManager.getDebugOverride(); + debugOverride?.setOverride('shared-flag', 'override-value'); + debugOverride?.setOverride('override-only-flag', 'override-value'); + + const allFlags = flagManager.getAll(); + expect(allFlags).toHaveProperty('store-flag'); + expect(allFlags).toHaveProperty('shared-flag'); + expect(allFlags).toHaveProperty('override-only-flag'); + expect(allFlags['store-flag'].flag.value).toBe('store-value'); + expect(allFlags['shared-flag'].flag.value).toBe('override-value'); + expect(allFlags['override-only-flag'].flag.value).toBe('override-value'); + }); +}); diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index ec9ec9951..824289048 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -45,7 +45,7 @@ import { } from './evaluation/evaluationDetail'; import createEventProcessor from './events/createEventProcessor'; import EventFactory from './events/EventFactory'; -import DefaultFlagManager, { LDDebugOverride, FlagManager } from './flag-manager/FlagManager'; +import DefaultFlagManager, { FlagManager, LDDebugOverride } from './flag-manager/FlagManager'; import { FlagChangeType } from './flag-manager/FlagUpdater'; import { ItemDescriptor } from './flag-manager/ItemDescriptor'; import HookRunner from './HookRunner'; @@ -609,10 +609,10 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult { protected getDebugOverrides(): LDDebugOverride | null { if (this._flagManager.getDebugOverride) { - return this._flagManager.getDebugOverride() + return this._flagManager.getDebugOverride(); } - return null + return null; } private _handleInspectionChanged(flagKeys: Array, type: FlagChangeType) { diff --git a/packages/shared/sdk-client/src/api/LDPlugin.ts b/packages/shared/sdk-client/src/api/LDPlugin.ts index fca4a4d48..7d6cd79f7 100644 --- a/packages/shared/sdk-client/src/api/LDPlugin.ts +++ b/packages/shared/sdk-client/src/api/LDPlugin.ts @@ -1,8 +1,9 @@ import { LDPluginBase } from '@launchdarkly/js-sdk-common'; + import { LDDebugOverride } from '../flag-manager/FlagManager'; export interface LDPlugin extends LDPluginBase { - /** + /** * An optional function called if the plugin wants to register debug capabilities. * This method allows plugins to receive a debug override interface for * temporarily overriding flag values during development and testing. @@ -12,5 +13,5 @@ export interface LDPlugin extends LDPluginBase { * * @param debugOverride The debug override interface instance */ - registerDebug?(debugOverride: LDDebugOverride): void; + registerDebug?(debugOverride: LDDebugOverride): void; } diff --git a/packages/shared/sdk-client/src/api/index.ts b/packages/shared/sdk-client/src/api/index.ts index 0705c6104..d2203c232 100644 --- a/packages/shared/sdk-client/src/api/index.ts +++ b/packages/shared/sdk-client/src/api/index.ts @@ -9,4 +9,4 @@ export { ConnectionMode }; export * from './LDIdentifyOptions'; export * from './LDInspection'; export * from './LDIdentifyResult'; -export * from './LDPlugin'; \ No newline at end of file +export * from './LDPlugin'; diff --git a/packages/shared/sdk-client/src/flag-manager/FlagManager.ts b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts index 6bf5d290f..5a1c24133 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagManager.ts +++ b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts @@ -76,7 +76,7 @@ export interface FlagManager { * * @experimental This function is experimental and intended for use by LaunchDarkly tools at this time. */ - getDebugOverride?(): LDDebugOverride + getDebugOverride?(): LDDebugOverride; } /** @@ -182,14 +182,16 @@ export default class DefaultFlagManager implements FlagManager { if (this._overrides) { return { ...this._flagStore.getAll(), - ...Object.entries(this._overrides).reduce((acc: {[key: string]: ItemDescriptor}, [key, value]) => { - acc[key] = this._convertValueToOverrideDescripter(value); - return acc - }, {}) - } - } else { - return this._flagStore.getAll(); + ...Object.entries(this._overrides).reduce( + (acc: { [key: string]: ItemDescriptor }, [key, value]) => { + acc[key] = this._convertValueToOverrideDescripter(value); + return acc; + }, + {}, + ), + }; } + return this._flagStore.getAll(); } presetFlags(newFlags: { [key: string]: ItemDescriptor }): void { @@ -225,10 +227,10 @@ export default class DefaultFlagManager implements FlagManager { private _convertValueToOverrideDescripter(value: LDFlagValue): ItemDescriptor { return { flag: { - value: value, - version: 0 + value, + version: 0, }, - version: 0 + version: 0, }; } diff --git a/packages/shared/sdk-client/src/index.ts b/packages/shared/sdk-client/src/index.ts index 5cff89748..dd0c35ea5 100644 --- a/packages/shared/sdk-client/src/index.ts +++ b/packages/shared/sdk-client/src/index.ts @@ -41,7 +41,7 @@ export type { export type { DataManager, DataManagerFactory, ConnectionParams } from './DataManager'; export type { FlagManager, LDDebugOverride } from './flag-manager/FlagManager'; -export {safeRegisterDebugOverridePlugins} from './plugins/safeRegisterDebugOverridePlugins'; +export { safeRegisterDebugOverridePlugins } from './plugins/safeRegisterDebugOverridePlugins'; export type { Configuration } from './configuration/Configuration'; export type { LDEmitter }; diff --git a/packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts b/packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts index 5897d1e59..9f864591c 100644 --- a/packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts +++ b/packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts @@ -1,6 +1,7 @@ -import { internal, LDLogger } from "@launchdarkly/js-sdk-common"; -import { LDPlugin } from "../api"; -import { LDDebugOverride } from "../flag-manager/FlagManager"; +import { internal, LDLogger } from '@launchdarkly/js-sdk-common'; + +import { LDPlugin } from '../api'; +import { LDDebugOverride } from '../flag-manager/FlagManager'; /** * Safe register debug override plugins. @@ -12,13 +13,13 @@ import { LDDebugOverride } from "../flag-manager/FlagManager"; export function safeRegisterDebugOverridePlugins( logger: LDLogger, debugOverride: LDDebugOverride, - plugins: LDPlugin[] + plugins: LDPlugin[], ): void { - plugins.forEach(plugin => { + plugins.forEach((plugin) => { try { plugin.registerDebug?.(debugOverride); } catch (error) { logger.error(`Exception thrown registering plugin ${internal.safeGetName(logger, plugin)}.`); } }); -}; +} From a45f0c2ee37020d15c127e146d07232ea8f45d74 Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Wed, 10 Dec 2025 12:56:07 -0600 Subject: [PATCH 4/6] chore: fixing lint issues --- packages/sdk/browser/src/BrowserClient.ts | 6 +++--- packages/sdk/browser/src/LDPlugin.ts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 50b8e00ec..2ebec0006 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -18,7 +18,7 @@ import { LDPluginEnvironmentMetadata, LDTimeoutError, Platform, - safeRegisterDebugOverridePlugins + safeRegisterDebugOverridePlugins, } from '@launchdarkly/js-client-sdk-common'; import { readFlagsFromBootstrap } from './bootstrap'; @@ -214,9 +214,9 @@ class BrowserClientImpl extends LDClientImpl { this._plugins || [], ); - const override = this.getDebugOverrides() + const override = this.getDebugOverrides(); if (override) { - safeRegisterDebugOverridePlugins(this.logger, override, this._plugins || []) + safeRegisterDebugOverridePlugins(this.logger, override, this._plugins || []); } } diff --git a/packages/sdk/browser/src/LDPlugin.ts b/packages/sdk/browser/src/LDPlugin.ts index fdc162391..6777a99ce 100644 --- a/packages/sdk/browser/src/LDPlugin.ts +++ b/packages/sdk/browser/src/LDPlugin.ts @@ -1,4 +1,5 @@ import { Hook, LDPlugin as LDPluginBase } from '@launchdarkly/js-client-sdk-common'; + import { LDClient } from './LDClient'; /** From 60d8969412c3aac3b91dc9b130c38d99af0c5432 Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Wed, 10 Dec 2025 13:48:46 -0600 Subject: [PATCH 5/6] chore: fix build issue --- packages/sdk/browser/src/LDPlugin.ts | 2 +- packages/shared/sdk-client/src/api/LDPlugin.ts | 4 ++-- .../shared/sdk-client/src/flag-manager/FlagManager.ts | 11 ++++------- packages/shared/sdk-client/src/index.ts | 2 +- .../src/plugins/safeRegisterDebugOverridePlugins.ts | 4 ++-- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/sdk/browser/src/LDPlugin.ts b/packages/sdk/browser/src/LDPlugin.ts index 6777a99ce..de06f6db3 100644 --- a/packages/sdk/browser/src/LDPlugin.ts +++ b/packages/sdk/browser/src/LDPlugin.ts @@ -1,4 +1,4 @@ -import { Hook, LDPlugin as LDPluginBase } from '@launchdarkly/js-client-sdk-common'; +import { Hook, LDPluginBase } from '@launchdarkly/js-client-sdk-common'; import { LDClient } from './LDClient'; diff --git a/packages/shared/sdk-client/src/api/LDPlugin.ts b/packages/shared/sdk-client/src/api/LDPlugin.ts index 7d6cd79f7..d441384a0 100644 --- a/packages/shared/sdk-client/src/api/LDPlugin.ts +++ b/packages/shared/sdk-client/src/api/LDPlugin.ts @@ -1,8 +1,8 @@ -import { LDPluginBase } from '@launchdarkly/js-sdk-common'; +import { LDPluginBase as LDPluginBaseCommon } from '@launchdarkly/js-sdk-common'; import { LDDebugOverride } from '../flag-manager/FlagManager'; -export interface LDPlugin extends LDPluginBase { +export interface LDPluginBase extends LDPluginBaseCommon { /** * An optional function called if the plugin wants to register debug capabilities. * This method allows plugins to receive a debug override interface for diff --git a/packages/shared/sdk-client/src/flag-manager/FlagManager.ts b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts index 5a1c24133..0638b4d9b 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagManager.ts +++ b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts @@ -258,14 +258,11 @@ export default class DefaultFlagManager implements FlagManager { } clearAllOverrides() { - if (!this._overrides) { - return {}; // No overrides to clear, return empty object for consistency + if (this._overrides) { + const clearedOverrides = { ...this._overrides }; + this._overrides = undefined; // Reset to undefined + this._flagUpdater.handleFlagChanges(null, Object.keys(clearedOverrides), 'override'); } - - const clearedOverrides = { ...this._overrides }; - this._overrides = undefined; // Reset to undefined - this._flagUpdater.handleFlagChanges(null, Object.keys(clearedOverrides), 'override'); - return clearedOverrides; } getAllOverrides() { diff --git a/packages/shared/sdk-client/src/index.ts b/packages/shared/sdk-client/src/index.ts index dd0c35ea5..5c13e7a86 100644 --- a/packages/shared/sdk-client/src/index.ts +++ b/packages/shared/sdk-client/src/index.ts @@ -36,7 +36,7 @@ export type { LDIdentifyTimeout, LDIdentifyShed, LDClientIdentifyResult, - LDPlugin, + LDPluginBase, } from './api'; export type { DataManager, DataManagerFactory, ConnectionParams } from './DataManager'; diff --git a/packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts b/packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts index 9f864591c..ca127908d 100644 --- a/packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts +++ b/packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts @@ -1,6 +1,6 @@ import { internal, LDLogger } from '@launchdarkly/js-sdk-common'; -import { LDPlugin } from '../api'; +import { LDPluginBase } from '../api'; import { LDDebugOverride } from '../flag-manager/FlagManager'; /** @@ -13,7 +13,7 @@ import { LDDebugOverride } from '../flag-manager/FlagManager'; export function safeRegisterDebugOverridePlugins( logger: LDLogger, debugOverride: LDDebugOverride, - plugins: LDPlugin[], + plugins: LDPluginBase[], ): void { plugins.forEach((plugin) => { try { From b8f8d9a5b8914cf12ffa6b5307c5cd3a8f483e68 Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Mon, 15 Dec 2025 16:26:02 -0600 Subject: [PATCH 6/6] fix: issues from rebase --- packages/sdk/browser/src/BrowserClient.ts | 1 - .../flag-manager/FlagManager.test.ts | 35 +++++++++++++++---- .../shared/sdk-client/src/LDClientImpl.ts | 10 ++---- .../src/flag-manager/FlagManager.ts | 17 ++++----- .../src/flag-manager/FlagUpdater.ts | 31 ++++++++-------- 5 files changed, 56 insertions(+), 38 deletions(-) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 2ebec0006..e765a71fa 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -16,7 +16,6 @@ import { LDHeaders, LDIdentifyResult, LDPluginEnvironmentMetadata, - LDTimeoutError, Platform, safeRegisterDebugOverridePlugins, } from '@launchdarkly/js-client-sdk-common'; diff --git a/packages/shared/sdk-client/__tests__/flag-manager/FlagManager.test.ts b/packages/shared/sdk-client/__tests__/flag-manager/FlagManager.test.ts index 76ef9495c..d8fe44000 100644 --- a/packages/shared/sdk-client/__tests__/flag-manager/FlagManager.test.ts +++ b/packages/shared/sdk-client/__tests__/flag-manager/FlagManager.test.ts @@ -118,7 +118,14 @@ describe('FlagManager override tests', () => { expect(flagManager.get('test-flag')?.flag.value).toBe('override-value'); }); - it('setOverride triggers flag change callback', () => { + it('setOverride triggers flag change callback', async () => { + const context = Context.fromLDContext({ kind: 'user', key: 'user-key' }); + const flags = { + 'test-flag': makeMockItemDescriptor(1, 'store-value'), + }; + + await flagManager.init(context, flags); + const mockCallback: FlagsChangeCallback = jest.fn(); flagManager.on(mockCallback); @@ -126,7 +133,7 @@ describe('FlagManager override tests', () => { debugOverride?.setOverride('test-flag', 'override-value'); expect(mockCallback).toHaveBeenCalledTimes(1); - expect(mockCallback).toHaveBeenCalledWith(null, ['test-flag'], 'override'); + expect(mockCallback).toHaveBeenCalledWith(context, ['test-flag'], 'override'); }); it('removeOverride does nothing when override does not exist', () => { @@ -151,7 +158,14 @@ describe('FlagManager override tests', () => { expect(flagManager.get('test-flag')?.flag.value).toBe('store-value'); }); - it('removeOverride triggers flag change callback', () => { + it('removeOverride triggers flag change callback', async () => { + const context = Context.fromLDContext({ kind: 'user', key: 'user-key' }); + const flags = { + 'test-flag': makeMockItemDescriptor(1, 'store-value'), + }; + + await flagManager.init(context, flags); + const mockCallback: FlagsChangeCallback = jest.fn(); flagManager.on(mockCallback); @@ -160,8 +174,8 @@ describe('FlagManager override tests', () => { debugOverride?.removeOverride('test-flag'); expect(mockCallback).toHaveBeenCalledTimes(2); - expect(mockCallback).toHaveBeenNthCalledWith(1, null, ['test-flag'], 'override'); - expect(mockCallback).toHaveBeenNthCalledWith(2, null, ['test-flag'], 'override'); + expect(mockCallback).toHaveBeenNthCalledWith(1, context, ['test-flag'], 'override'); + expect(mockCallback).toHaveBeenNthCalledWith(2, context, ['test-flag'], 'override'); }); it('clearAllOverrides removes all overrides', () => { @@ -176,18 +190,25 @@ describe('FlagManager override tests', () => { expect(Object.keys(flagManager.getAllOverrides())).toHaveLength(0); }); - it('clearAllOverrides triggers flag change callback for all flags', () => { + it('clearAllOverrides triggers flag change callback for all flags', async () => { const mockCallback: FlagsChangeCallback = jest.fn(); flagManager.on(mockCallback); const debugOverride = flagManager.getDebugOverride(); + const context = Context.fromLDContext({ kind: 'user', key: 'user-key' }); + const flags = { + 'test-flag': makeMockItemDescriptor(1, 'store-value'), + }; + + await flagManager.init(context, flags); + debugOverride?.setOverride('flag1', 'value1'); debugOverride?.setOverride('flag2', 'value2'); (mockCallback as jest.Mock).mockClear(); debugOverride?.clearAllOverrides(); expect(mockCallback).toHaveBeenCalledTimes(1); - expect(mockCallback).toHaveBeenCalledWith(null, ['flag1', 'flag2'], 'override'); + expect(mockCallback).toHaveBeenCalledWith(context, ['flag1', 'flag2'], 'override'); }); it('getAllOverrides returns all overrides as ItemDescriptors', () => { diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 824289048..1dc13c0ec 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -132,7 +132,7 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult { this._flagManager.on((context, flagKeys, type) => { this._handleInspectionChanged(flagKeys, type); - const ldContext = context ? Context.toLDContext(context) : null; + const ldContext = Context.toLDContext(context); this.emitter.emit('change', ldContext, flagKeys); flagKeys.forEach((it) => { this.emitter.emit(`change:${it}`, ldContext); @@ -607,12 +607,8 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult { this._eventProcessor?.sendEvent(event); } - protected getDebugOverrides(): LDDebugOverride | null { - if (this._flagManager.getDebugOverride) { - return this._flagManager.getDebugOverride(); - } - - return null; + protected getDebugOverrides(): LDDebugOverride | undefined { + return this._flagManager.getDebugOverride?.(); } private _handleInspectionChanged(flagKeys: Array, type: FlagChangeType) { diff --git a/packages/shared/sdk-client/src/flag-manager/FlagManager.ts b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts index 0638b4d9b..7ad670a9e 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagManager.ts +++ b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts @@ -65,10 +65,6 @@ export interface FlagManager { */ off(callback: FlagsChangeCallback): void; - // REVIEWER: My reasoning here is to have the flagmanager implementation determine - // whether or not we can support debug plugins so I put the override methods here. - // Would like some thoughts on this as it is a deviation from previous implementation. - /** * Obtain debug override functions that allows plugins * to manipulate the outcome of the flags managed by @@ -239,7 +235,7 @@ export default class DefaultFlagManager implements FlagManager { this._overrides = {}; } this._overrides[key] = value; - this._flagUpdater.handleFlagChanges(null, [key], 'override'); + this._flagUpdater.handleFlagChanges([key], 'override'); } removeOverride(flagKey: string) { @@ -254,14 +250,14 @@ export default class DefaultFlagManager implements FlagManager { this._overrides = undefined; } - this._flagUpdater.handleFlagChanges(null, [flagKey], 'override'); + this._flagUpdater.handleFlagChanges([flagKey], 'override'); } clearAllOverrides() { if (this._overrides) { const clearedOverrides = { ...this._overrides }; this._overrides = undefined; // Reset to undefined - this._flagUpdater.handleFlagChanges(null, Object.keys(clearedOverrides), 'override'); + this._flagUpdater.handleFlagChanges(Object.keys(clearedOverrides), 'override'); } } @@ -277,6 +273,11 @@ export default class DefaultFlagManager implements FlagManager { } getDebugOverride(): LDDebugOverride { - return this as LDDebugOverride; + return { + setOverride: this.setOverride.bind(this), + removeOverride: this.removeOverride.bind(this), + clearAllOverrides: this.clearAllOverrides.bind(this), + getAllOverrides: this.getAllOverrides.bind(this), + }; } } diff --git a/packages/shared/sdk-client/src/flag-manager/FlagUpdater.ts b/packages/shared/sdk-client/src/flag-manager/FlagUpdater.ts index e0feb5b84..57948e21d 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagUpdater.ts +++ b/packages/shared/sdk-client/src/flag-manager/FlagUpdater.ts @@ -20,11 +20,6 @@ export type FlagChangeType = 'init' | 'patch' | 'override'; * will call a variation method for flag values which you require. */ export type FlagsChangeCallback = ( - // REVIEWER: This is probably not desired, but I think there are some updates - // such as overrides that do not really have a context? Unless I am misunderstanding - // what context is exactly. Being able to support a null context may also help - // with distinguishing between being in the emphemeral state between the start of - // initialization and the end of identification and having an invalid context? context: Context, flagKeys: Array, type: FlagChangeType, @@ -46,14 +41,20 @@ export default class FlagUpdater { this._logger = logger; } - handleFlagChanges(context: Context, keys: string[], type: FlagChangeType): void { - this._changeCallbacks.forEach((callback) => { - try { - callback(context, keys, type); - } catch (err) { - /* intentionally empty */ - } - }); + handleFlagChanges(keys: string[], type: FlagChangeType): void { + if (this._activeContext) { + this._changeCallbacks.forEach((callback) => { + try { + callback(this._activeContext!, keys, type); + } catch (err) { + /* intentionally empty */ + } + }); + } else { + this._logger.warn( + 'Received a change event wihtout an active context. Changes will not be propagated.', + ); + } } init(context: Context, newFlags: { [key: string]: ItemDescriptor }) { @@ -62,7 +63,7 @@ export default class FlagUpdater { this._flagStore.init(newFlags); const changed = calculateChangedKeys(oldFlags, newFlags); if (changed.length > 0) { - this.handleFlagChanges(context, changed, 'init'); + this.handleFlagChanges(changed, 'init'); } } @@ -87,7 +88,7 @@ export default class FlagUpdater { } this._flagStore.insertOrUpdate(key, item); - this.handleFlagChanges(this._activeContext!, [key], 'patch'); + this.handleFlagChanges([key], 'patch'); return true; }