Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions packages/sdk/browser/src/BrowserClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<void> {
Expand Down
3 changes: 3 additions & 0 deletions packages/sdk/electron/src/ElectronClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
LDWaitForInitializationOptions,
LDWaitForInitializationResult,
mobileFdv1Endpoints,
safeRegisterDebugOverridePlugins,
} from '@launchdarkly/js-client-sdk-common';

import ElectronDataManager from './ElectronDataManager';
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof mockClient, unknown> = {
getMetadata: jest.fn().mockReturnValue({ name: 'plugin1' }),
register: jest.fn(),
registerDebug: jest.fn(),
};

const plugin2: LDPluginBase<typeof mockClient, unknown> = {
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<typeof mockClient, unknown> = {
getMetadata: jest.fn().mockReturnValue({ name: 'with-debug' }),
register: jest.fn(),
registerDebug: jest.fn(),
};

const pluginWithoutDebug: LDPluginBase<typeof mockClient, unknown> = {
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<typeof mockClient, unknown> = {
getMetadata: jest.fn().mockReturnValue({ name: 'error-plugin' }),
register: jest.fn(),
registerDebug: jest.fn().mockImplementation(() => {
throw new Error('register-debug failure');
}),
};

const workingPlugin: LDPluginBase<typeof mockClient, unknown> = {
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();
});
12 changes: 7 additions & 5 deletions packages/shared/sdk-client/src/LDClientImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<string>, type: FlagChangeType) {
if (!this._inspectorManager.hasInspectors()) {
return;
Expand Down
44 changes: 44 additions & 0 deletions packages/shared/sdk-client/src/api/LDDebugOverride.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
2 changes: 1 addition & 1 deletion packages/shared/sdk-client/src/api/LDPlugin.ts
Original file line number Diff line number Diff line change
@@ -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<TClient, THook> extends LDPluginBaseCommon<TClient, THook> {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()`.
Expand Down
42 changes: 1 addition & 41 deletions packages/shared/sdk-client/src/flag-manager/FlagManager.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion packages/shared/sdk-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Loading