diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 465bcfbaac..b7c1d0bdce 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -149,6 +149,8 @@ class BrowserClientImpl extends LDClientImpl { ), getImplementationHooks: (environmentMetadata: LDPluginEnvironmentMetadata) => internal.safeGetHooks(logger, environmentMetadata, validatedBrowserOptions.plugins), + registerDebugOverrides: (debugOverride) => + safeRegisterDebugOverridePlugins(logger, debugOverride, validatedBrowserOptions.plugins), credentialType: 'clientSideId', requiresStart: true, initialContext, @@ -223,11 +225,6 @@ 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/electron/src/ElectronClient.ts b/packages/sdk/electron/src/ElectronClient.ts index b32a7b1cba..f97090f069 100644 --- a/packages/sdk/electron/src/ElectronClient.ts +++ b/packages/sdk/electron/src/ElectronClient.ts @@ -23,6 +23,7 @@ import { LDWaitForInitializationOptions, LDWaitForInitializationResult, mobileFdv1Endpoints, + safeRegisterDebugOverridePlugins, } from '@launchdarkly/js-client-sdk-common'; import ElectronDataManager from './ElectronDataManager'; @@ -88,6 +89,8 @@ export class ElectronClient extends LDClientImpl { highTimeoutThreshold: 15, getImplementationHooks: (_environmentMetadata: LDPluginEnvironmentMetadata) => internal.safeGetHooks(logger, _environmentMetadata, validatedElectronOptions.plugins), + registerDebugOverrides: (debugOverride) => + safeRegisterDebugOverridePlugins(logger, debugOverride, validatedElectronOptions.plugins), credentialType: useClientSideId ? 'clientSideId' : 'mobileKey', requiresStart: true, initialContext, diff --git a/packages/shared/sdk-client/__tests__/plugins/safeRegisterDebugOverridePlugins.test.ts b/packages/shared/sdk-client/__tests__/plugins/safeRegisterDebugOverridePlugins.test.ts new file mode 100644 index 0000000000..99cc1041ac --- /dev/null +++ b/packages/shared/sdk-client/__tests__/plugins/safeRegisterDebugOverridePlugins.test.ts @@ -0,0 +1,111 @@ +import { LDLogger } from '@launchdarkly/js-sdk-common'; + +import { LDPluginBase } from '../../src/api'; +import { LDDebugOverride } from '../../src/api/LDDebugOverride'; +import { safeRegisterDebugOverridePlugins } from '../../src/plugins/safeRegisterDebugOverridePlugins'; + +function createMockLogger(): LDLogger { + return { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; +} + +function createMockDebugOverride(): LDDebugOverride { + return { + setOverride: jest.fn(), + removeOverride: jest.fn(), + clearAllOverrides: jest.fn(), + getAllOverrides: jest.fn().mockReturnValue({}), + }; +} + +it('calls registerDebug on every plugin that implements it', () => { + const logger = createMockLogger(); + const debugOverride = createMockDebugOverride(); + const mockClient = { id: 'test-client' }; + + const plugin1: LDPluginBase = { + getMetadata: jest.fn().mockReturnValue({ name: 'plugin1' }), + register: jest.fn(), + registerDebug: jest.fn(), + }; + + const plugin2: LDPluginBase = { + getMetadata: jest.fn().mockReturnValue({ name: 'plugin2' }), + register: jest.fn(), + registerDebug: jest.fn(), + }; + + safeRegisterDebugOverridePlugins(logger, debugOverride, [plugin1, plugin2]); + + expect(plugin1.registerDebug).toHaveBeenCalledWith(debugOverride); + expect(plugin2.registerDebug).toHaveBeenCalledWith(debugOverride); + expect(logger.error).not.toHaveBeenCalled(); +}); + +it('skips plugins that do not implement registerDebug', () => { + const logger = createMockLogger(); + const debugOverride = createMockDebugOverride(); + const mockClient = { id: 'test-client' }; + + const pluginWithDebug: LDPluginBase = { + getMetadata: jest.fn().mockReturnValue({ name: 'with-debug' }), + register: jest.fn(), + registerDebug: jest.fn(), + }; + + const pluginWithoutDebug: LDPluginBase = { + getMetadata: jest.fn().mockReturnValue({ name: 'no-debug' }), + register: jest.fn(), + }; + + safeRegisterDebugOverridePlugins(logger, debugOverride, [ + pluginWithoutDebug, + pluginWithDebug, + ]); + + expect(pluginWithDebug.registerDebug).toHaveBeenCalledWith(debugOverride); + expect(logger.error).not.toHaveBeenCalled(); +}); + +it('continues processing and logs error when registerDebug throws', () => { + const logger = createMockLogger(); + const debugOverride = createMockDebugOverride(); + const mockClient = { id: 'test-client' }; + + const throwingPlugin: LDPluginBase = { + getMetadata: jest.fn().mockReturnValue({ name: 'error-plugin' }), + register: jest.fn(), + registerDebug: jest.fn().mockImplementation(() => { + throw new Error('register-debug failure'); + }), + }; + + const workingPlugin: LDPluginBase = { + getMetadata: jest.fn().mockReturnValue({ name: 'working-plugin' }), + register: jest.fn(), + registerDebug: jest.fn(), + }; + + safeRegisterDebugOverridePlugins(logger, debugOverride, [throwingPlugin, workingPlugin]); + + expect(throwingPlugin.registerDebug).toHaveBeenCalledWith(debugOverride); + expect(workingPlugin.registerDebug).toHaveBeenCalledWith(debugOverride); + expect(logger.error).toHaveBeenCalledWith( + 'Exception thrown registering plugin error-plugin.', + ); +}); + +it('handles an empty plugins array without error', () => { + const logger = createMockLogger(); + const debugOverride = createMockDebugOverride(); + + expect(() => + safeRegisterDebugOverridePlugins(logger, debugOverride, []), + ).not.toThrow(); + + expect(logger.error).not.toHaveBeenCalled(); +}); diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index d60d737558..8594fa26b2 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -55,7 +55,7 @@ import { 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 DefaultFlagManager, { FlagManager } from './flag-manager/FlagManager'; import { FlagChangeType } from './flag-manager/FlagUpdater'; import { ItemDescriptor } from './flag-manager/ItemDescriptor'; import HookRunner from './HookRunner'; @@ -146,6 +146,12 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult { this._config.disableCache ?? false, this._config.logger, ); + + const debugOverride = this._flagManager.getDebugOverride?.(); + if (debugOverride && internalOptions?.registerDebugOverrides) { + internalOptions.registerDebugOverrides(debugOverride); + } + this._diagnosticsManager = createDiagnosticsManager(sdkKey, this._config, platform); this._eventProcessor = createEventProcessor( sdkKey, @@ -813,10 +819,6 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult { this._eventProcessor?.sendEvent(event); } - protected getDebugOverrides(): LDDebugOverride | undefined { - return this._flagManager.getDebugOverride?.(); - } - private _handleInspectionChanged(flagKeys: Array, type: FlagChangeType) { if (!this._inspectorManager.hasInspectors()) { return; diff --git a/packages/shared/sdk-client/src/api/LDDebugOverride.ts b/packages/shared/sdk-client/src/api/LDDebugOverride.ts new file mode 100644 index 0000000000..3fbe2ab756 --- /dev/null +++ b/packages/shared/sdk-client/src/api/LDDebugOverride.ts @@ -0,0 +1,44 @@ +import { LDFlagValue } from '@launchdarkly/js-sdk-common'; + +import { ItemDescriptor } from '../flag-manager/ItemDescriptor'; + +/** + * 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 }; +} diff --git a/packages/shared/sdk-client/src/api/LDPlugin.ts b/packages/shared/sdk-client/src/api/LDPlugin.ts index d441384a01..8e52d379cb 100644 --- a/packages/shared/sdk-client/src/api/LDPlugin.ts +++ b/packages/shared/sdk-client/src/api/LDPlugin.ts @@ -1,6 +1,6 @@ import { LDPluginBase as LDPluginBaseCommon } from '@launchdarkly/js-sdk-common'; -import { LDDebugOverride } from '../flag-manager/FlagManager'; +import { LDDebugOverride } from './LDDebugOverride'; export interface LDPluginBase extends LDPluginBaseCommon { /** diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index de296ec2bd..b77799533a 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -12,6 +12,7 @@ import { import { Hook, type LDOptions } from '../api'; import { LDContext } from '../api/LDContext'; import { LDInspection } from '../api/LDInspection'; +import type { LDDebugOverride } from '../api/LDDebugOverride'; import type { InternalDataSystemOptions, PlatformDataSystemDefaults, @@ -27,6 +28,7 @@ export interface LDClientInternalOptions extends internal.LDInternalOptions { credentialType: 'clientSideId' | 'mobileKey'; getLegacyStorageKeys?: () => string[]; dataSystemDefaults?: PlatformDataSystemDefaults; + registerDebugOverrides?: (debugOverride: LDDebugOverride) => void; /** * When true, the SDK requires `start()` to be called before `identify()`. diff --git a/packages/shared/sdk-client/src/flag-manager/FlagManager.ts b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts index 0ae031594c..f84983d571 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagManager.ts +++ b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts @@ -1,5 +1,6 @@ import { Context, internal, LDFlagValue, LDLogger, Platform } from '@launchdarkly/js-sdk-common'; +import { LDDebugOverride } from '../api/LDDebugOverride'; import { namespaceForEnvironment } from '../storage/namespaceUtils'; import FlagPersistence from './FlagPersistence'; import { createDefaultFlagStore } from './FlagStore'; @@ -90,47 +91,6 @@ export interface FlagManager { 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 = createDefaultFlagStore(); private _flagUpdater: FlagUpdater; diff --git a/packages/shared/sdk-client/src/index.ts b/packages/shared/sdk-client/src/index.ts index 951ddc078c..2305855ef3 100644 --- a/packages/shared/sdk-client/src/index.ts +++ b/packages/shared/sdk-client/src/index.ts @@ -48,7 +48,8 @@ export type { } from './api'; export type { DataManager, DataManagerFactory, ConnectionParams } from './DataManager'; -export type { FlagManager, LDDebugOverride } from './flag-manager/FlagManager'; +export type { FlagManager } from './flag-manager/FlagManager'; +export type { LDDebugOverride } from './api/LDDebugOverride'; export { safeRegisterDebugOverridePlugins } from './plugins/safeRegisterDebugOverridePlugins'; export type { Configuration } from './configuration/Configuration'; export { default as validateOptions } from './configuration/validateOptions'; diff --git a/packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts b/packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts index accda39705..a27f266ec9 100644 --- a/packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts +++ b/packages/shared/sdk-client/src/plugins/safeRegisterDebugOverridePlugins.ts @@ -1,7 +1,7 @@ import { internal, LDLogger } from '@launchdarkly/js-sdk-common'; import { LDPluginBase } from '../api'; -import { LDDebugOverride } from '../flag-manager/FlagManager'; +import { LDDebugOverride } from '../api/LDDebugOverride'; /** * Safe register debug override plugins.