Skip to content

Commit ed29a8a

Browse files
committed
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
1 parent 391ea12 commit ed29a8a

File tree

10 files changed

+211
-23
lines changed

10 files changed

+211
-23
lines changed

packages/sdk/browser/src/BrowserClient.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
LDPluginEnvironmentMetadata,
1919
LDTimeoutError,
2020
Platform,
21+
safeRegisterDebugOverridePlugins
2122
} from '@launchdarkly/js-client-sdk-common';
2223

2324
import { getHref } from './BrowserApi';
@@ -207,6 +208,11 @@ class BrowserClientImpl extends LDClientImpl {
207208
client,
208209
this._plugins || [],
209210
);
211+
212+
const override = this.getDebugOverrides()
213+
if (override) {
214+
safeRegisterDebugOverridePlugins(this.logger, override, this._plugins || [])
215+
}
210216
}
211217

212218
override async identify(context: LDContext, identifyOptions?: LDIdentifyOptions): Promise<void> {

packages/sdk/browser/src/LDPlugin.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { Hook, LDPluginBase } from '@launchdarkly/js-client-sdk-common';
2-
1+
import { Hook, LDPlugin as LDPluginBase } from '@launchdarkly/js-client-sdk-common';
32
import { LDClient } from './LDClient';
43

54
/**

packages/sdk/browser/src/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export type {
4343
LDIdentifyError,
4444
LDIdentifyTimeout,
4545
LDIdentifyShed,
46+
LDDebugOverride,
4647
} from '@launchdarkly/js-client-sdk-common';
4748

4849
/**

packages/shared/sdk-client/src/LDClientImpl.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {
4141
} from './evaluation/evaluationDetail';
4242
import createEventProcessor from './events/createEventProcessor';
4343
import EventFactory from './events/EventFactory';
44-
import DefaultFlagManager, { FlagManager } from './flag-manager/FlagManager';
44+
import DefaultFlagManager, { LDDebugOverride, FlagManager } from './flag-manager/FlagManager';
4545
import { FlagChangeType } from './flag-manager/FlagUpdater';
4646
import HookRunner from './HookRunner';
4747
import { getInspectorHook } from './inspection/getInspectorHook';
@@ -127,7 +127,7 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult {
127127

128128
this._flagManager.on((context, flagKeys, type) => {
129129
this._handleInspectionChanged(flagKeys, type);
130-
const ldContext = Context.toLDContext(context);
130+
const ldContext = context ? Context.toLDContext(context) : null;
131131
this.emitter.emit('change', ldContext, flagKeys);
132132
flagKeys.forEach((it) => {
133133
this.emitter.emit(`change:${it}`, ldContext);
@@ -582,6 +582,14 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult {
582582
this._eventProcessor?.sendEvent(event);
583583
}
584584

585+
protected getDebugOverrides(): LDDebugOverride | null {
586+
if (this._flagManager.getDebugOverride) {
587+
return this._flagManager.getDebugOverride()
588+
}
589+
590+
return null
591+
}
592+
585593
private _handleInspectionChanged(flagKeys: Array<string>, type: FlagChangeType) {
586594
if (!this._inspectorManager.hasInspectors()) {
587595
return;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { LDPluginBase } from '@launchdarkly/js-sdk-common';
2+
import { LDDebugOverride } from '../flag-manager/FlagManager';
3+
4+
export interface LDPlugin<TClient, THook> extends LDPluginBase<TClient, THook> {
5+
/**
6+
* An optional function called if the plugin wants to register debug capabilities.
7+
* This method allows plugins to receive a debug override interface for
8+
* temporarily overriding flag values during development and testing.
9+
*
10+
* @experimental This interface is experimental and intended for use by LaunchDarkly tools at this time.
11+
* The API may change in future versions.
12+
*
13+
* @param debugOverride The debug override interface instance
14+
*/
15+
registerDebug?(debugOverride: LDDebugOverride): void;
16+
}

packages/shared/sdk-client/src/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export { ConnectionMode };
99
export * from './LDIdentifyOptions';
1010
export * from './LDInspection';
1111
export * from './LDIdentifyResult';
12+
export * from './LDPlugin';

packages/shared/sdk-client/src/flag-manager/FlagManager.ts

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Context, LDLogger, Platform } from '@launchdarkly/js-sdk-common';
1+
import { Context, LDFlagValue, LDLogger, Platform } from '@launchdarkly/js-sdk-common';
22

33
import { namespaceForEnvironment } from '../storage/namespaceUtils';
44
import FlagPersistence from './FlagPersistence';
@@ -55,12 +55,67 @@ export interface FlagManager {
5555
* Unregister a flag change callback.
5656
*/
5757
off(callback: FlagsChangeCallback): void;
58+
59+
// REVIEWER: My reasoning here is to have the flagmanager implementation determine
60+
// whether or not we can support debug plugins so I put the override methods here.
61+
// Would like some thoughts on this as it is a deviation from previous implementation.
62+
63+
/**
64+
* Obtain debug override functions that allows plugins
65+
* to manipulate the outcome of the flags managed by
66+
* this manager
67+
*
68+
* @experimental This function is experimental and intended for use by LaunchDarkly tools at this time.
69+
*/
70+
getDebugOverride?(): LDDebugOverride
71+
}
72+
73+
/**
74+
* Debug interface for plugins that need to override flag values during development.
75+
* This interface provides methods to temporarily override flag values that take
76+
* precedence over the actual flag values from LaunchDarkly. These overrides are
77+
* useful for testing, development, and debugging scenarios.
78+
*
79+
* @experimental This interface is experimental and intended for use by LaunchDarkly tools at this time.
80+
* The API may change in future versions.
81+
*/
82+
export interface LDDebugOverride {
83+
/**
84+
* Set an override value for a flag that takes precedence over the real flag value.
85+
*
86+
* @param flagKey The flag key.
87+
* @param value The override value.
88+
*/
89+
setOverride(flagKey: string, value: LDFlagValue): void;
90+
91+
/**
92+
* Remove an override value for a flag, reverting to the real flag value.
93+
*
94+
* @param flagKey The flag key.
95+
*/
96+
removeOverride(flagKey: string): void;
97+
98+
/**
99+
* Clear all override values, reverting all flags to their real values.
100+
*/
101+
clearAllOverrides(): void;
102+
103+
/**
104+
* Get all currently active flag overrides.
105+
*
106+
* @returns
107+
* An object containing all active overrides as key-value pairs,
108+
* where keys are flag keys and values are the overridden flag values.
109+
* Returns an empty object if no overrides are active.
110+
*/
111+
getAllOverrides(): { [key: string]: ItemDescriptor };
58112
}
59113

60114
export default class DefaultFlagManager implements FlagManager {
61115
private _flagStore = new DefaultFlagStore();
62116
private _flagUpdater: FlagUpdater;
63117
private _flagPersistencePromise: Promise<FlagPersistence>;
118+
private _overrides?: { [key: string]: LDFlagValue };
64119

65120
/**
66121
* @param platform implementation of various platform provided functionality
@@ -107,11 +162,25 @@ export default class DefaultFlagManager implements FlagManager {
107162
}
108163

109164
get(key: string): ItemDescriptor | undefined {
165+
if (this._overrides && Object.prototype.hasOwnProperty.call(this._overrides, key)) {
166+
return this._convertValueToOverrideDescripter(this._overrides[key]);
167+
}
168+
110169
return this._flagStore.get(key);
111170
}
112171

113172
getAll(): { [key: string]: ItemDescriptor } {
114-
return this._flagStore.getAll();
173+
if (this._overrides) {
174+
return {
175+
...this._flagStore.getAll(),
176+
...Object.entries(this._overrides).reduce((acc: {[key: string]: ItemDescriptor}, [key, value]) => {
177+
acc[key] = this._convertValueToOverrideDescripter(value);
178+
return acc
179+
}, {})
180+
}
181+
} else {
182+
return this._flagStore.getAll();
183+
}
115184
}
116185

117186
setBootstrap(context: Context, newFlags: { [key: string]: ItemDescriptor }): void {
@@ -139,4 +208,63 @@ export default class DefaultFlagManager implements FlagManager {
139208
off(callback: FlagsChangeCallback): void {
140209
this._flagUpdater.off(callback);
141210
}
211+
212+
private _convertValueToOverrideDescripter(value: LDFlagValue): ItemDescriptor {
213+
return {
214+
flag: {
215+
value: value,
216+
version: 0
217+
},
218+
version: 0
219+
};
220+
}
221+
222+
setOverride(key: string, value: LDFlagValue) {
223+
if (!this._overrides) {
224+
this._overrides = {};
225+
}
226+
this._overrides[key] = value;
227+
this._flagUpdater.handleFlagChanges(null, [key], 'override');
228+
}
229+
230+
removeOverride(flagKey: string) {
231+
if (!this._overrides || !Object.prototype.hasOwnProperty.call(this._overrides, flagKey)) {
232+
return; // No override to remove
233+
}
234+
235+
delete this._overrides[flagKey];
236+
237+
// If no more overrides, reset to undefined for performance
238+
if (Object.keys(this._overrides).length === 0) {
239+
this._overrides = undefined;
240+
}
241+
242+
this._flagUpdater.handleFlagChanges(null, [flagKey], 'override');
243+
}
244+
245+
clearAllOverrides() {
246+
if (!this._overrides) {
247+
return {}; // No overrides to clear, return empty object for consistency
248+
}
249+
250+
const clearedOverrides = { ...this._overrides };
251+
this._overrides = undefined; // Reset to undefined
252+
this._flagUpdater.handleFlagChanges(null, Object.keys(clearedOverrides), 'override');
253+
return clearedOverrides;
254+
}
255+
256+
getAllOverrides() {
257+
if (!this._overrides) {
258+
return {};
259+
}
260+
const result = {} as { [key: string]: ItemDescriptor };
261+
Object.entries(this._overrides).forEach(([key, value]) => {
262+
result[key] = this._convertValueToOverrideDescripter(value);
263+
});
264+
return result;
265+
}
266+
267+
getDebugOverride(): LDDebugOverride {
268+
return this as LDDebugOverride;
269+
}
142270
}

packages/shared/sdk-client/src/flag-manager/FlagUpdater.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import calculateChangedKeys from './calculateChangedKeys';
44
import FlagStore from './FlagStore';
55
import { ItemDescriptor } from './ItemDescriptor';
66

7-
export type FlagChangeType = 'init' | 'patch';
7+
export type FlagChangeType = 'init' | 'patch' | 'override';
88

99
/**
1010
* This callback indicates that the details associated with one or more flags
@@ -20,7 +20,12 @@ export type FlagChangeType = 'init' | 'patch';
2020
* will call a variation method for flag values which you require.
2121
*/
2222
export type FlagsChangeCallback = (
23-
context: Context,
23+
// REVIEWER: This is probably not desired, but I think there are some updates
24+
// such as overrides that do not really have a context? Unless I am misunderstanding
25+
// what context is exactly. Being able to support a null context may also help
26+
// with distinguishing between being in the emphemeral state between the start of
27+
// initialization and the end of identification and having an invalid context?
28+
context: Context | null,
2429
flagKeys: Array<string>,
2530
type: FlagChangeType,
2631
) => void;
@@ -41,19 +46,23 @@ export default class FlagUpdater {
4146
this._logger = logger;
4247
}
4348

49+
handleFlagChanges(context: Context | null, keys: string[], type: FlagChangeType): void {
50+
this._changeCallbacks.forEach((callback) => {
51+
try {
52+
callback(context, keys, type);
53+
} catch (err) {
54+
/* intentionally empty */
55+
}
56+
});
57+
}
58+
4459
init(context: Context, newFlags: { [key: string]: ItemDescriptor }) {
4560
this._activeContextKey = context.canonicalKey;
4661
const oldFlags = this._flagStore.getAll();
4762
this._flagStore.init(newFlags);
4863
const changed = calculateChangedKeys(oldFlags, newFlags);
4964
if (changed.length > 0) {
50-
this._changeCallbacks.forEach((callback) => {
51-
try {
52-
callback(context, changed, 'init');
53-
} catch (err) {
54-
/* intentionally empty */
55-
}
56-
});
65+
this.handleFlagChanges(context, changed, 'init');
5766
}
5867
}
5968

@@ -78,13 +87,7 @@ export default class FlagUpdater {
7887
}
7988

8089
this._flagStore.insertOrUpdate(key, item);
81-
this._changeCallbacks.forEach((callback) => {
82-
try {
83-
callback(context, [key], 'patch');
84-
} catch (err) {
85-
/* intentionally empty */
86-
}
87-
});
90+
this.handleFlagChanges(context, [key], 'patch');
8891
return true;
8992
}
9093

packages/shared/sdk-client/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@ export type {
3636
LDIdentifyTimeout,
3737
LDIdentifyShed,
3838
LDClientIdentifyResult,
39+
LDPlugin,
3940
} from './api';
4041

4142
export type { DataManager, DataManagerFactory, ConnectionParams } from './DataManager';
42-
export type { FlagManager } from './flag-manager/FlagManager';
43+
export type { FlagManager, LDDebugOverride } from './flag-manager/FlagManager';
44+
export {safeRegisterDebugOverridePlugins} from './plugins/safeRegisterDebugOverridePlugins';
4345
export type { Configuration } from './configuration/Configuration';
4446

4547
export type { LDEmitter };
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { internal, LDLogger } from "@launchdarkly/js-sdk-common";
2+
import { LDPlugin } from "../api";
3+
import { LDDebugOverride } from "../flag-manager/FlagManager";
4+
5+
/**
6+
* Safe register debug override plugins.
7+
*
8+
* @param logger - The logger to use for logging errors.
9+
* @param debugOverride - The debug override to register.
10+
* @param plugins - The plugins to register.
11+
*/
12+
export function safeRegisterDebugOverridePlugins<TClient, THook>(
13+
logger: LDLogger,
14+
debugOverride: LDDebugOverride,
15+
plugins: LDPlugin<TClient, THook>[]
16+
): void {
17+
plugins.forEach(plugin => {
18+
try {
19+
plugin.registerDebug?.(debugOverride);
20+
} catch (error) {
21+
logger.error(`Exception thrown registering plugin ${internal.safeGetName(logger, plugin)}.`);
22+
}
23+
});
24+
};

0 commit comments

Comments
 (0)