Skip to content

Commit db57d3f

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 2c89dfa commit db57d3f

File tree

10 files changed

+210
-22
lines changed

10 files changed

+210
-22
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 { readFlagsFromBootstrap } from './bootstrap';
@@ -212,6 +213,11 @@ class BrowserClientImpl extends LDClientImpl {
212213
client,
213214
this._plugins || [],
214215
);
216+
217+
const override = this.getDebugOverrides()
218+
if (override) {
219+
safeRegisterDebugOverridePlugins(this.logger, override, this._plugins || [])
220+
}
215221
}
216222

217223
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
@@ -45,7 +45,7 @@ import {
4545
} from './evaluation/evaluationDetail';
4646
import createEventProcessor from './events/createEventProcessor';
4747
import EventFactory from './events/EventFactory';
48-
import DefaultFlagManager, { FlagManager } from './flag-manager/FlagManager';
48+
import DefaultFlagManager, { LDDebugOverride, FlagManager } from './flag-manager/FlagManager';
4949
import { FlagChangeType } from './flag-manager/FlagUpdater';
5050
import { ItemDescriptor } from './flag-manager/ItemDescriptor';
5151
import HookRunner from './HookRunner';
@@ -132,7 +132,7 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult {
132132

133133
this._flagManager.on((context, flagKeys, type) => {
134134
this._handleInspectionChanged(flagKeys, type);
135-
const ldContext = Context.toLDContext(context);
135+
const ldContext = context ? Context.toLDContext(context) : null;
136136
this.emitter.emit('change', ldContext, flagKeys);
137137
flagKeys.forEach((it) => {
138138
this.emitter.emit(`change:${it}`, ldContext);
@@ -607,6 +607,14 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult {
607607
this._eventProcessor?.sendEvent(event);
608608
}
609609

610+
protected getDebugOverrides(): LDDebugOverride | null {
611+
if (this._flagManager.getDebugOverride) {
612+
return this._flagManager.getDebugOverride()
613+
}
614+
615+
return null
616+
}
617+
610618
private _handleInspectionChanged(flagKeys: Array<string>, type: FlagChangeType) {
611619
if (!this._inspectorManager.hasInspectors()) {
612620
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';
@@ -64,12 +64,67 @@ export interface FlagManager {
6464
* Unregister a flag change callback.
6565
*/
6666
off(callback: FlagsChangeCallback): void;
67+
68+
// REVIEWER: My reasoning here is to have the flagmanager implementation determine
69+
// whether or not we can support debug plugins so I put the override methods here.
70+
// Would like some thoughts on this as it is a deviation from previous implementation.
71+
72+
/**
73+
* Obtain debug override functions that allows plugins
74+
* to manipulate the outcome of the flags managed by
75+
* this manager
76+
*
77+
* @experimental This function is experimental and intended for use by LaunchDarkly tools at this time.
78+
*/
79+
getDebugOverride?(): LDDebugOverride
80+
}
81+
82+
/**
83+
* Debug interface for plugins that need to override flag values during development.
84+
* This interface provides methods to temporarily override flag values that take
85+
* precedence over the actual flag values from LaunchDarkly. These overrides are
86+
* useful for testing, development, and debugging scenarios.
87+
*
88+
* @experimental This interface is experimental and intended for use by LaunchDarkly tools at this time.
89+
* The API may change in future versions.
90+
*/
91+
export interface LDDebugOverride {
92+
/**
93+
* Set an override value for a flag that takes precedence over the real flag value.
94+
*
95+
* @param flagKey The flag key.
96+
* @param value The override value.
97+
*/
98+
setOverride(flagKey: string, value: LDFlagValue): void;
99+
100+
/**
101+
* Remove an override value for a flag, reverting to the real flag value.
102+
*
103+
* @param flagKey The flag key.
104+
*/
105+
removeOverride(flagKey: string): void;
106+
107+
/**
108+
* Clear all override values, reverting all flags to their real values.
109+
*/
110+
clearAllOverrides(): void;
111+
112+
/**
113+
* Get all currently active flag overrides.
114+
*
115+
* @returns
116+
* An object containing all active overrides as key-value pairs,
117+
* where keys are flag keys and values are the overridden flag values.
118+
* Returns an empty object if no overrides are active.
119+
*/
120+
getAllOverrides(): { [key: string]: ItemDescriptor };
67121
}
68122

69123
export default class DefaultFlagManager implements FlagManager {
70124
private _flagStore = new DefaultFlagStore();
71125
private _flagUpdater: FlagUpdater;
72126
private _flagPersistencePromise: Promise<FlagPersistence>;
127+
private _overrides?: { [key: string]: LDFlagValue };
73128

74129
/**
75130
* @param platform implementation of various platform provided functionality
@@ -116,11 +171,25 @@ export default class DefaultFlagManager implements FlagManager {
116171
}
117172

118173
get(key: string): ItemDescriptor | undefined {
174+
if (this._overrides && Object.prototype.hasOwnProperty.call(this._overrides, key)) {
175+
return this._convertValueToOverrideDescripter(this._overrides[key]);
176+
}
177+
119178
return this._flagStore.get(key);
120179
}
121180

122181
getAll(): { [key: string]: ItemDescriptor } {
123-
return this._flagStore.getAll();
182+
if (this._overrides) {
183+
return {
184+
...this._flagStore.getAll(),
185+
...Object.entries(this._overrides).reduce((acc: {[key: string]: ItemDescriptor}, [key, value]) => {
186+
acc[key] = this._convertValueToOverrideDescripter(value);
187+
return acc
188+
}, {})
189+
}
190+
} else {
191+
return this._flagStore.getAll();
192+
}
124193
}
125194

126195
presetFlags(newFlags: { [key: string]: ItemDescriptor }): void {
@@ -152,4 +221,63 @@ export default class DefaultFlagManager implements FlagManager {
152221
off(callback: FlagsChangeCallback): void {
153222
this._flagUpdater.off(callback);
154223
}
224+
225+
private _convertValueToOverrideDescripter(value: LDFlagValue): ItemDescriptor {
226+
return {
227+
flag: {
228+
value: value,
229+
version: 0
230+
},
231+
version: 0
232+
};
233+
}
234+
235+
setOverride(key: string, value: LDFlagValue) {
236+
if (!this._overrides) {
237+
this._overrides = {};
238+
}
239+
this._overrides[key] = value;
240+
this._flagUpdater.handleFlagChanges(null, [key], 'override');
241+
}
242+
243+
removeOverride(flagKey: string) {
244+
if (!this._overrides || !Object.prototype.hasOwnProperty.call(this._overrides, flagKey)) {
245+
return; // No override to remove
246+
}
247+
248+
delete this._overrides[flagKey];
249+
250+
// If no more overrides, reset to undefined for performance
251+
if (Object.keys(this._overrides).length === 0) {
252+
this._overrides = undefined;
253+
}
254+
255+
this._flagUpdater.handleFlagChanges(null, [flagKey], 'override');
256+
}
257+
258+
clearAllOverrides() {
259+
if (!this._overrides) {
260+
return {}; // No overrides to clear, return empty object for consistency
261+
}
262+
263+
const clearedOverrides = { ...this._overrides };
264+
this._overrides = undefined; // Reset to undefined
265+
this._flagUpdater.handleFlagChanges(null, Object.keys(clearedOverrides), 'override');
266+
return clearedOverrides;
267+
}
268+
269+
getAllOverrides() {
270+
if (!this._overrides) {
271+
return {};
272+
}
273+
const result = {} as { [key: string]: ItemDescriptor };
274+
Object.entries(this._overrides).forEach(([key, value]) => {
275+
result[key] = this._convertValueToOverrideDescripter(value);
276+
});
277+
return result;
278+
}
279+
280+
getDebugOverride(): LDDebugOverride {
281+
return this as LDDebugOverride;
282+
}
155283
}

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

Lines changed: 18 additions & 15 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,6 +20,11 @@ export type FlagChangeType = 'init' | 'patch';
2020
* will call a variation method for flag values which you require.
2121
*/
2222
export type FlagsChangeCallback = (
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?
2328
context: Context,
2429
flagKeys: Array<string>,
2530
type: FlagChangeType,
@@ -41,19 +46,23 @@ export default class FlagUpdater {
4146
this._logger = logger;
4247
}
4348

49+
handleFlagChanges(context: Context, 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._activeContext = context;
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(this._activeContext!, [key], 'patch');
84-
} catch (err) {
85-
/* intentionally empty */
86-
}
87-
});
90+
this.handleFlagChanges(this._activeContext!, [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)