launchdarkly-js-sdk-common 5.7.0 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +14 -0
- package/CODEOWNERS +1 -1
- package/package.json +3 -3
- package/src/FlagStore.js +144 -0
- package/src/__tests__/LDClient-debugOverride-test.js +342 -0
- package/src/__tests__/LDClient-plugins-test.js +63 -5
- package/src/index.js +127 -31
- package/src/plugins.js +17 -0
- package/test-types.ts +1 -1
- package/typings.d.ts +62 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `launchdarkly-js-sdk-common` package will be documented in this file. Changes that affect the dependent SDKs such as `launchdarkly-js-client-sdk` should also be logged in those projects, in the next release that uses the updated version of this package. This project adheres to [Semantic Versioning](http://semver.org).
|
|
4
4
|
|
|
5
|
+
## [5.8.0](https://github.com/launchdarkly/js-sdk-common/compare/5.7.1...5.8.0) (2025-09-05)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* Add experimental debug override functionality. ([#132](https://github.com/launchdarkly/js-sdk-common/issues/132)) ([8b2f757](https://github.com/launchdarkly/js-sdk-common/commit/8b2f757e9e070226a10f32dcf943501a8f8f1e4a))
|
|
11
|
+
|
|
12
|
+
## [5.7.1](https://github.com/launchdarkly/js-sdk-common/compare/5.7.0...5.7.1) (2025-05-30)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* updating identify code documentation ([82c6071](https://github.com/launchdarkly/js-sdk-common/commit/82c6071760cccbe3000a30f46fb19e169ed8573e))
|
|
18
|
+
|
|
5
19
|
## [5.7.0](https://github.com/launchdarkly/js-sdk-common/compare/5.6.0...5.7.0) (2025-05-22)
|
|
6
20
|
|
|
7
21
|
|
package/CODEOWNERS
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# Repository Maintainers
|
|
2
|
-
* @launchdarkly/team-sdk
|
|
2
|
+
* @launchdarkly/team-sdk-js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "launchdarkly-js-sdk-common",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.8.0",
|
|
4
4
|
"description": "LaunchDarkly SDK for JavaScript - common code",
|
|
5
5
|
"author": "LaunchDarkly <team@launchdarkly.com>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"launchdarkly-js-test-helpers": "1.1.0",
|
|
45
45
|
"prettier": "1.19.1",
|
|
46
46
|
"readline-sync": "^1.4.9",
|
|
47
|
-
"
|
|
48
|
-
"
|
|
47
|
+
"typedoc": "^0.25.13",
|
|
48
|
+
"typescript": "~5.4.5"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"base64-js": "^1.3.0",
|
package/src/FlagStore.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const utils = require('./utils');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FlagStore - Centralized flag store and access point for all feature flags
|
|
5
|
+
*
|
|
6
|
+
* This module manages two types of feature flags:
|
|
7
|
+
* 1. Regular flags - Retrieved from LaunchDarkly servers or bootstrap data
|
|
8
|
+
* 2. Override flags - Local overrides for debugging/testing
|
|
9
|
+
*
|
|
10
|
+
* When a flag is requested:
|
|
11
|
+
* - If an override exists for that flag, the override value is returned
|
|
12
|
+
* - Otherwise, the regular flag value is returned
|
|
13
|
+
*/
|
|
14
|
+
function FlagStore() {
|
|
15
|
+
let flags = {};
|
|
16
|
+
// The flag overrides are set lazily to allow bypassing property checks when no overrides are present.
|
|
17
|
+
let flagOverrides;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gets a single flag by key, with overrides taking precedence over regular flags
|
|
21
|
+
* @param {string} key The flag key to retrieve
|
|
22
|
+
* @returns {Object|null} The flag object or null if not found
|
|
23
|
+
*/
|
|
24
|
+
function get(key) {
|
|
25
|
+
// Check overrides first, then real flags
|
|
26
|
+
if (flagOverrides && utils.objectHasOwnProperty(flagOverrides, key) && flagOverrides[key]) {
|
|
27
|
+
return flagOverrides[key];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (flags && utils.objectHasOwnProperty(flags, key) && flags[key] && !flags[key].deleted) {
|
|
31
|
+
return flags[key];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Gets all flags with overrides applied
|
|
39
|
+
* @returns {Object} Object containing all flags with any overrides applied
|
|
40
|
+
*/
|
|
41
|
+
function getFlagsWithOverrides() {
|
|
42
|
+
const result = {};
|
|
43
|
+
|
|
44
|
+
// Add all flags first
|
|
45
|
+
for (const key in flags) {
|
|
46
|
+
const flag = get(key);
|
|
47
|
+
if (flag) {
|
|
48
|
+
result[key] = flag;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Override with any flagOverrides (they take precedence)
|
|
53
|
+
if (flagOverrides) {
|
|
54
|
+
for (const key in flagOverrides) {
|
|
55
|
+
const override = get(key);
|
|
56
|
+
if (override) {
|
|
57
|
+
result[key] = override;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Replaces all flags with new flag data
|
|
67
|
+
* @param {Object} newFlags - Object containing the new flag data
|
|
68
|
+
*/
|
|
69
|
+
function setFlags(newFlags) {
|
|
70
|
+
flags = { ...newFlags };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Sets an override value for a specific flag
|
|
75
|
+
* @param {string} key The flag key to override
|
|
76
|
+
* @param {*} value The override value for the flag
|
|
77
|
+
*/
|
|
78
|
+
function setOverride(key, value) {
|
|
79
|
+
if (!flagOverrides) {
|
|
80
|
+
flagOverrides = {};
|
|
81
|
+
}
|
|
82
|
+
flagOverrides[key] = { value };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Removes an override for a specific flag
|
|
87
|
+
* @param {string} key The flag key to remove the override for
|
|
88
|
+
*/
|
|
89
|
+
function removeOverride(key) {
|
|
90
|
+
if (!flagOverrides || !flagOverrides[key]) {
|
|
91
|
+
return; // No override to remove
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
delete flagOverrides[key];
|
|
95
|
+
|
|
96
|
+
// If no more overrides, reset to undefined for performance
|
|
97
|
+
if (Object.keys(flagOverrides).length === 0) {
|
|
98
|
+
flagOverrides = undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Clears all flag overrides and returns the cleared overrides
|
|
104
|
+
* @returns {Object} The overrides that were cleared, useful for tracking what was removed
|
|
105
|
+
*/
|
|
106
|
+
function clearAllOverrides() {
|
|
107
|
+
if (!flagOverrides) {
|
|
108
|
+
return {}; // No overrides to clear, return empty object for consistency
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const clearedOverrides = { ...flagOverrides };
|
|
112
|
+
flagOverrides = undefined; // Reset to undefined
|
|
113
|
+
return clearedOverrides;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Gets the internal flag state without overrides applied
|
|
118
|
+
* @returns {Object} The internal flag data structure
|
|
119
|
+
*/
|
|
120
|
+
function getFlags() {
|
|
121
|
+
return flags;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Gets the flag overrides data
|
|
126
|
+
* @returns {Object} The flag overrides object, or empty object if no overrides exist
|
|
127
|
+
*/
|
|
128
|
+
function getFlagOverrides() {
|
|
129
|
+
return flagOverrides || {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
clearAllOverrides,
|
|
134
|
+
get,
|
|
135
|
+
getFlagOverrides,
|
|
136
|
+
getFlags,
|
|
137
|
+
getFlagsWithOverrides,
|
|
138
|
+
removeOverride,
|
|
139
|
+
setFlags,
|
|
140
|
+
setOverride,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = FlagStore;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
const { initialize } = require('../index');
|
|
2
|
+
const stubPlatform = require('./stubPlatform');
|
|
3
|
+
const { respondJson } = require('./mockHttp');
|
|
4
|
+
const { makeBootstrap } = require('./testUtils');
|
|
5
|
+
|
|
6
|
+
// Mock the logger functions
|
|
7
|
+
const mockLogger = () => ({
|
|
8
|
+
error: jest.fn(),
|
|
9
|
+
warn: jest.fn(),
|
|
10
|
+
info: jest.fn(),
|
|
11
|
+
debug: jest.fn(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Define a basic Plugin structure for tests
|
|
15
|
+
const createTestPlugin = (name = 'Test Plugin') => {
|
|
16
|
+
const plugin = {
|
|
17
|
+
getMetadata: jest.fn().mockReturnValue({ name }),
|
|
18
|
+
register: jest.fn(),
|
|
19
|
+
getHooks: jest.fn().mockReturnValue([]),
|
|
20
|
+
registerDebug: jest.fn(),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return plugin;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Helper to initialize the client for tests
|
|
27
|
+
async function withClient(initialContext, configOverrides = {}, plugins = [], testFn) {
|
|
28
|
+
const platform = stubPlatform.defaults();
|
|
29
|
+
const server = platform.testing.http.newServer();
|
|
30
|
+
const logger = mockLogger();
|
|
31
|
+
|
|
32
|
+
// Disable streaming and event sending unless overridden
|
|
33
|
+
const defaults = {
|
|
34
|
+
baseUrl: server.url,
|
|
35
|
+
streaming: false,
|
|
36
|
+
sendEvents: false,
|
|
37
|
+
useLdd: false,
|
|
38
|
+
logger: logger,
|
|
39
|
+
plugins: plugins,
|
|
40
|
+
};
|
|
41
|
+
const config = { ...defaults, ...configOverrides };
|
|
42
|
+
const { client, start } = initialize('env', initialContext, config, platform);
|
|
43
|
+
|
|
44
|
+
server.byDefault(respondJson({}));
|
|
45
|
+
start();
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await client.waitForInitialization(10);
|
|
49
|
+
await testFn(client, logger, platform);
|
|
50
|
+
} finally {
|
|
51
|
+
await client.close();
|
|
52
|
+
server.close();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('LDDebugOverride', () => {
|
|
57
|
+
describe('setOverride method', () => {
|
|
58
|
+
it('should set override value returned by variation method', async () => {
|
|
59
|
+
let debugOverrideInterface;
|
|
60
|
+
const mockPlugin = createTestPlugin('test-plugin');
|
|
61
|
+
mockPlugin.registerDebug.mockImplementation(debugOverride => {
|
|
62
|
+
debugOverrideInterface = debugOverride;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => {
|
|
66
|
+
expect(client.variation('test-flag', 'default')).toBe('default');
|
|
67
|
+
|
|
68
|
+
debugOverrideInterface.setOverride('test-flag', 'override-value');
|
|
69
|
+
expect(client.variation('test-flag', 'default')).toBe('override-value');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should override values taking precedence over real flag values from bootstrap', async () => {
|
|
74
|
+
let debugOverrideInterface;
|
|
75
|
+
const mockPlugin = createTestPlugin('test-plugin');
|
|
76
|
+
mockPlugin.registerDebug.mockImplementation(debugOverride => {
|
|
77
|
+
debugOverrideInterface = debugOverride;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const flags = makeBootstrap({ 'existing-flag': { value: 'real-value', version: 1 } });
|
|
81
|
+
|
|
82
|
+
await withClient({ key: 'user-key', kind: 'user' }, { bootstrap: flags }, [mockPlugin], async client => {
|
|
83
|
+
expect(client.variation('existing-flag', 'default')).toBe('real-value');
|
|
84
|
+
|
|
85
|
+
debugOverrideInterface.setOverride('existing-flag', 'override-value');
|
|
86
|
+
expect(client.variation('existing-flag', 'default')).toBe('override-value');
|
|
87
|
+
|
|
88
|
+
debugOverrideInterface.removeOverride('existing-flag');
|
|
89
|
+
expect(client.variation('existing-flag', 'default')).toBe('real-value');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('removeOverride method', () => {
|
|
95
|
+
it('should remove individual override and revert to default', async () => {
|
|
96
|
+
let debugOverrideInterface;
|
|
97
|
+
const mockPlugin = createTestPlugin('test-plugin');
|
|
98
|
+
mockPlugin.registerDebug.mockImplementation(debugOverride => {
|
|
99
|
+
debugOverrideInterface = debugOverride;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => {
|
|
103
|
+
debugOverrideInterface.setOverride('test-flag', 'override-value');
|
|
104
|
+
expect(client.variation('test-flag', 'default')).toBe('override-value');
|
|
105
|
+
|
|
106
|
+
debugOverrideInterface.removeOverride('test-flag');
|
|
107
|
+
expect(client.variation('test-flag', 'default')).toBe('default');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should remove only the specified override leaving others intact', async () => {
|
|
112
|
+
let debugOverrideInterface;
|
|
113
|
+
const mockPlugin = createTestPlugin('test-plugin');
|
|
114
|
+
mockPlugin.registerDebug.mockImplementation(debugOverride => {
|
|
115
|
+
debugOverrideInterface = debugOverride;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => {
|
|
119
|
+
debugOverrideInterface.setOverride('flag1', 'value1');
|
|
120
|
+
debugOverrideInterface.setOverride('flag2', 'value2');
|
|
121
|
+
debugOverrideInterface.setOverride('flag3', 'value3');
|
|
122
|
+
|
|
123
|
+
debugOverrideInterface.removeOverride('flag2');
|
|
124
|
+
|
|
125
|
+
expect(client.variation('flag1', 'default')).toBe('value1');
|
|
126
|
+
expect(client.variation('flag2', 'default')).toBe('default');
|
|
127
|
+
expect(client.variation('flag3', 'default')).toBe('value3');
|
|
128
|
+
|
|
129
|
+
const allOverrides = debugOverrideInterface.getAllOverrides();
|
|
130
|
+
expect(allOverrides).toEqual({
|
|
131
|
+
flag1: 'value1',
|
|
132
|
+
flag3: 'value3',
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle removing non-existent override without throwing error', async () => {
|
|
138
|
+
let debugOverrideInterface;
|
|
139
|
+
const mockPlugin = createTestPlugin('test-plugin');
|
|
140
|
+
mockPlugin.registerDebug.mockImplementation(debugOverride => {
|
|
141
|
+
debugOverrideInterface = debugOverride;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => {
|
|
145
|
+
debugOverrideInterface.setOverride('existing-flag', 'value');
|
|
146
|
+
|
|
147
|
+
// Should not throw error
|
|
148
|
+
expect(() => {
|
|
149
|
+
debugOverrideInterface.removeOverride('non-existent-flag');
|
|
150
|
+
}).not.toThrow();
|
|
151
|
+
|
|
152
|
+
// Existing override should remain
|
|
153
|
+
expect(client.variation('existing-flag', 'default')).toBe('value');
|
|
154
|
+
const allOverrides = debugOverrideInterface.getAllOverrides();
|
|
155
|
+
expect(allOverrides).toEqual({ 'existing-flag': 'value' });
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should be callable multiple times on same flag key safely', async () => {
|
|
160
|
+
let debugOverrideInterface;
|
|
161
|
+
const mockPlugin = createTestPlugin('test-plugin');
|
|
162
|
+
mockPlugin.registerDebug.mockImplementation(debugOverride => {
|
|
163
|
+
debugOverrideInterface = debugOverride;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => {
|
|
167
|
+
debugOverrideInterface.setOverride('test-flag', 'value');
|
|
168
|
+
|
|
169
|
+
debugOverrideInterface.removeOverride('test-flag');
|
|
170
|
+
expect(client.variation('test-flag', 'default')).toBe('default');
|
|
171
|
+
|
|
172
|
+
// Removing again should not cause issues
|
|
173
|
+
expect(() => {
|
|
174
|
+
debugOverrideInterface.removeOverride('test-flag');
|
|
175
|
+
}).not.toThrow();
|
|
176
|
+
|
|
177
|
+
expect(debugOverrideInterface.getAllOverrides()).toEqual({});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('clearAllOverrides method', () => {
|
|
183
|
+
it('should clear all overrides and revert all flags to their default values', async () => {
|
|
184
|
+
let debugOverrideInterface;
|
|
185
|
+
const mockPlugin = createTestPlugin('test-plugin');
|
|
186
|
+
mockPlugin.registerDebug.mockImplementation(debugOverride => {
|
|
187
|
+
debugOverrideInterface = debugOverride;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => {
|
|
191
|
+
debugOverrideInterface.setOverride('flag1', 'value1');
|
|
192
|
+
debugOverrideInterface.setOverride('flag2', 'value2');
|
|
193
|
+
|
|
194
|
+
debugOverrideInterface.clearAllOverrides();
|
|
195
|
+
expect(client.variation('flag1', 'default')).toBe('default');
|
|
196
|
+
expect(client.variation('flag2', 'default')).toBe('default');
|
|
197
|
+
expect(debugOverrideInterface.getAllOverrides()).toEqual({});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should operate safely when no overrides exist', async () => {
|
|
202
|
+
let debugOverrideInterface;
|
|
203
|
+
const mockPlugin = createTestPlugin('test-plugin');
|
|
204
|
+
mockPlugin.registerDebug.mockImplementation(debugOverride => {
|
|
205
|
+
debugOverrideInterface = debugOverride;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async () => {
|
|
209
|
+
// Should not throw error when no overrides exist
|
|
210
|
+
expect(() => {
|
|
211
|
+
debugOverrideInterface.clearAllOverrides();
|
|
212
|
+
}).not.toThrow();
|
|
213
|
+
|
|
214
|
+
expect(debugOverrideInterface.getAllOverrides()).toEqual({});
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('getAllOverrides method', () => {
|
|
220
|
+
it('should return all current overrides', async () => {
|
|
221
|
+
let debugOverrideInterface;
|
|
222
|
+
const mockPlugin = createTestPlugin('test-plugin');
|
|
223
|
+
mockPlugin.registerDebug.mockImplementation(debugOverride => {
|
|
224
|
+
debugOverrideInterface = debugOverride;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async () => {
|
|
228
|
+
debugOverrideInterface.setOverride('test-flag', 'override-value');
|
|
229
|
+
|
|
230
|
+
const allOverrides = debugOverrideInterface.getAllOverrides();
|
|
231
|
+
expect(allOverrides).toEqual({ 'test-flag': 'override-value' });
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should return empty object when no overrides have been set', async () => {
|
|
236
|
+
let debugOverrideInterface;
|
|
237
|
+
const mockPlugin = createTestPlugin('test-plugin');
|
|
238
|
+
mockPlugin.registerDebug.mockImplementation(debugOverride => {
|
|
239
|
+
debugOverrideInterface = debugOverride;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async () => {
|
|
243
|
+
const allOverrides = debugOverrideInterface.getAllOverrides();
|
|
244
|
+
expect(allOverrides).toEqual({});
|
|
245
|
+
expect(typeof allOverrides).toBe('object');
|
|
246
|
+
expect(Array.isArray(allOverrides)).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should return immutable copy not reference to internal state', async () => {
|
|
251
|
+
let debugOverrideInterface;
|
|
252
|
+
const mockPlugin = createTestPlugin('test-plugin');
|
|
253
|
+
mockPlugin.registerDebug.mockImplementation(debugOverride => {
|
|
254
|
+
debugOverrideInterface = debugOverride;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => {
|
|
258
|
+
debugOverrideInterface.setOverride('test-flag', 'original-value');
|
|
259
|
+
|
|
260
|
+
const overrides1 = debugOverrideInterface.getAllOverrides();
|
|
261
|
+
const overrides2 = debugOverrideInterface.getAllOverrides();
|
|
262
|
+
|
|
263
|
+
// Should be different objects
|
|
264
|
+
expect(overrides1).not.toBe(overrides2);
|
|
265
|
+
|
|
266
|
+
// Modifying returned object should not affect internal state
|
|
267
|
+
overrides1['new-flag'] = 'new-value';
|
|
268
|
+
delete overrides1['test-flag'];
|
|
269
|
+
|
|
270
|
+
expect(client.variation('test-flag', 'default')).toBe('original-value');
|
|
271
|
+
expect(client.variation('new-flag', 'default')).toBe('default');
|
|
272
|
+
|
|
273
|
+
const overrides3 = debugOverrideInterface.getAllOverrides();
|
|
274
|
+
expect(overrides3).toEqual({ 'test-flag': 'original-value' });
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should maintain consistency across different operations', async () => {
|
|
279
|
+
let debugOverrideInterface;
|
|
280
|
+
const mockPlugin = createTestPlugin('test-plugin');
|
|
281
|
+
mockPlugin.registerDebug.mockImplementation(debugOverride => {
|
|
282
|
+
debugOverrideInterface = debugOverride;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async () => {
|
|
286
|
+
// Test consistency through various operations
|
|
287
|
+
expect(debugOverrideInterface.getAllOverrides()).toEqual({});
|
|
288
|
+
|
|
289
|
+
debugOverrideInterface.setOverride('flag1', 'value1');
|
|
290
|
+
expect(debugOverrideInterface.getAllOverrides()).toEqual({ flag1: 'value1' });
|
|
291
|
+
|
|
292
|
+
debugOverrideInterface.setOverride('flag2', 'value2');
|
|
293
|
+
expect(debugOverrideInterface.getAllOverrides()).toEqual({ flag1: 'value1', flag2: 'value2' });
|
|
294
|
+
|
|
295
|
+
debugOverrideInterface.removeOverride('flag1');
|
|
296
|
+
expect(debugOverrideInterface.getAllOverrides()).toEqual({ flag2: 'value2' });
|
|
297
|
+
|
|
298
|
+
debugOverrideInterface.setOverride('flag2', 'updated-value2');
|
|
299
|
+
expect(debugOverrideInterface.getAllOverrides()).toEqual({ flag2: 'updated-value2' });
|
|
300
|
+
|
|
301
|
+
debugOverrideInterface.clearAllOverrides();
|
|
302
|
+
expect(debugOverrideInterface.getAllOverrides()).toEqual({});
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('integration with client methods', () => {
|
|
308
|
+
it('should work correctly with variationDetail method', async () => {
|
|
309
|
+
let debugOverrideInterface;
|
|
310
|
+
const mockPlugin = createTestPlugin('test-plugin');
|
|
311
|
+
mockPlugin.registerDebug.mockImplementation(debugOverride => {
|
|
312
|
+
debugOverrideInterface = debugOverride;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => {
|
|
316
|
+
debugOverrideInterface.setOverride('test-flag', 'override-value');
|
|
317
|
+
|
|
318
|
+
const detail = client.variationDetail('test-flag', 'default');
|
|
319
|
+
expect(detail.value).toBe('override-value');
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should include overrides in allFlags method output', async () => {
|
|
324
|
+
let debugOverrideInterface;
|
|
325
|
+
const mockPlugin = createTestPlugin('test-plugin');
|
|
326
|
+
mockPlugin.registerDebug.mockImplementation(debugOverride => {
|
|
327
|
+
debugOverrideInterface = debugOverride;
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const flags = makeBootstrap({ 'real-flag': { value: 'real-value', version: 1 } });
|
|
331
|
+
|
|
332
|
+
await withClient({ key: 'user-key', kind: 'user' }, { bootstrap: flags }, [mockPlugin], async client => {
|
|
333
|
+
debugOverrideInterface.setOverride('override-flag', 'override-value');
|
|
334
|
+
debugOverrideInterface.setOverride('real-flag', 'overridden-real-value');
|
|
335
|
+
|
|
336
|
+
const allFlags = client.allFlags();
|
|
337
|
+
expect(allFlags['real-flag']).toBe('overridden-real-value');
|
|
338
|
+
expect(allFlags['override-flag']).toBe('override-value');
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
});
|
|
@@ -21,11 +21,19 @@ const createTestHook = (name = 'Test Hook') => ({
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
// Define a basic Plugin structure for tests
|
|
24
|
-
const createTestPlugin = (name = 'Test Plugin', hooks = []) =>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
const createTestPlugin = (name = 'Test Plugin', hooks = [], includeDebug = false) => {
|
|
25
|
+
const plugin = {
|
|
26
|
+
getMetadata: jest.fn().mockReturnValue({ name }),
|
|
27
|
+
register: jest.fn(),
|
|
28
|
+
getHooks: jest.fn().mockReturnValue(hooks),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (includeDebug) {
|
|
32
|
+
plugin.registerDebug = jest.fn();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return plugin;
|
|
36
|
+
};
|
|
29
37
|
|
|
30
38
|
// Helper to initialize the client for tests
|
|
31
39
|
async function withClient(initialContext, configOverrides = {}, plugins = [], testFn) {
|
|
@@ -212,3 +220,53 @@ it('passes correct environmentMetadata without optional fields', async () => {
|
|
|
212
220
|
}
|
|
213
221
|
);
|
|
214
222
|
});
|
|
223
|
+
|
|
224
|
+
it('registers plugins and calls registerDebug when a plugin implements it', async () => {
|
|
225
|
+
const mockPlugin = createTestPlugin('test-plugin', [], true);
|
|
226
|
+
|
|
227
|
+
await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async () => {
|
|
228
|
+
expect(mockPlugin.register).toHaveBeenCalled();
|
|
229
|
+
|
|
230
|
+
// Verify that registerDebug was called
|
|
231
|
+
expect(mockPlugin.registerDebug).toHaveBeenCalledTimes(1);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('registers plugins but does not call registerDebug when a plugin does not implement it', async () => {
|
|
236
|
+
const mockPlugin = createTestPlugin('test-plugin', [], false);
|
|
237
|
+
|
|
238
|
+
await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async () => {
|
|
239
|
+
expect(mockPlugin.register).toHaveBeenCalled();
|
|
240
|
+
|
|
241
|
+
// Verify that registerDebug was not called
|
|
242
|
+
expect(mockPlugin.registerDebug).toBeUndefined();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('registers multiple plugins and calls registerDebug selectively', async () => {
|
|
247
|
+
const mockPluginWithDebug1 = createTestPlugin('test-plugin-with-debug-1', [], true);
|
|
248
|
+
const mockPluginWithDebug2 = createTestPlugin('test-plugin-with-debug-2', [], true);
|
|
249
|
+
const mockPluginWithoutDebug1 = createTestPlugin('test-plugin-without-debug-1', [], false);
|
|
250
|
+
const mockPluginWithoutDebug2 = createTestPlugin('test-plugin-without-debug-2', [], false);
|
|
251
|
+
|
|
252
|
+
await withClient(
|
|
253
|
+
{ key: 'user-key', kind: 'user' },
|
|
254
|
+
{},
|
|
255
|
+
[mockPluginWithDebug1, mockPluginWithoutDebug1, mockPluginWithDebug2, mockPluginWithoutDebug2],
|
|
256
|
+
async () => {
|
|
257
|
+
// Verify all plugins were registered
|
|
258
|
+
expect(mockPluginWithDebug1.register).toHaveBeenCalled();
|
|
259
|
+
expect(mockPluginWithDebug2.register).toHaveBeenCalled();
|
|
260
|
+
expect(mockPluginWithoutDebug1.register).toHaveBeenCalled();
|
|
261
|
+
expect(mockPluginWithoutDebug2.register).toHaveBeenCalled();
|
|
262
|
+
|
|
263
|
+
// Verify that registerDebug was called only on plugins that implement it
|
|
264
|
+
expect(mockPluginWithDebug1.registerDebug).toHaveBeenCalledTimes(1);
|
|
265
|
+
expect(mockPluginWithDebug2.registerDebug).toHaveBeenCalledTimes(1);
|
|
266
|
+
|
|
267
|
+
// Verify that registerDebug was not called on plugins that don't implement it
|
|
268
|
+
expect(mockPluginWithoutDebug1.registerDebug).toBeUndefined();
|
|
269
|
+
expect(mockPluginWithoutDebug2.registerDebug).toBeUndefined();
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
});
|
package/src/index.js
CHANGED
|
@@ -18,7 +18,13 @@ const { checkContext, getContextKeys } = require('./context');
|
|
|
18
18
|
const { InspectorTypes, InspectorManager } = require('./InspectorManager');
|
|
19
19
|
const timedPromise = require('./timedPromise');
|
|
20
20
|
const createHookRunner = require('./HookRunner');
|
|
21
|
-
const
|
|
21
|
+
const FlagStore = require('./FlagStore');
|
|
22
|
+
const {
|
|
23
|
+
getPluginHooks,
|
|
24
|
+
registerPlugins,
|
|
25
|
+
registerPluginsForDebugOverride,
|
|
26
|
+
createPluginEnvironment,
|
|
27
|
+
} = require('./plugins');
|
|
22
28
|
const changeEvent = 'change';
|
|
23
29
|
const internalChangeEvent = 'internal-change';
|
|
24
30
|
const highTimeoutThreshold = 5;
|
|
@@ -76,7 +82,8 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
76
82
|
|
|
77
83
|
const requestor = Requestor(platform, options, environment);
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
const flagStore = FlagStore();
|
|
86
|
+
|
|
80
87
|
let useLocalStorage;
|
|
81
88
|
let streamActive;
|
|
82
89
|
let streamForcedState = options.streaming;
|
|
@@ -188,7 +195,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
188
195
|
function notifyInspectionFlagsChanged() {
|
|
189
196
|
if (inspectorManager.hasListeners(InspectorTypes.flagDetailsChanged)) {
|
|
190
197
|
inspectorManager.onFlags(
|
|
191
|
-
Object.entries(
|
|
198
|
+
Object.entries(flagStore.getFlagsWithOverrides())
|
|
192
199
|
.map(([key, value]) => ({ key, detail: getFlagDetail(value) }))
|
|
193
200
|
.reduce((acc, cur) => {
|
|
194
201
|
// eslint-disable-next-line no-param-reassign
|
|
@@ -232,7 +239,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
232
239
|
default: defaultValue,
|
|
233
240
|
creationDate: now.getTime(),
|
|
234
241
|
};
|
|
235
|
-
const flag =
|
|
242
|
+
const flag = flagStore.getFlags()[key];
|
|
236
243
|
if (flag) {
|
|
237
244
|
event.version = flag.flagVersion ? flag.flagVersion : flag.version;
|
|
238
245
|
event.trackEvents = flag.trackEvents;
|
|
@@ -262,7 +269,10 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
262
269
|
if (stateProvider) {
|
|
263
270
|
// We're being controlled by another client instance, so only that instance is allowed to change the context
|
|
264
271
|
logger.warn(messages.identifyDisabled());
|
|
265
|
-
return utils.wrapPromiseCallback(
|
|
272
|
+
return utils.wrapPromiseCallback(
|
|
273
|
+
Promise.resolve(utils.transformVersionedValuesToValues(flagStore.getFlagsWithOverrides())),
|
|
274
|
+
onDone
|
|
275
|
+
);
|
|
266
276
|
}
|
|
267
277
|
let afterIdentify;
|
|
268
278
|
const clearFirst = useLocalStorage && persistentFlagStore ? persistentFlagStore.clearFlags() : Promise.resolve();
|
|
@@ -328,10 +338,10 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
328
338
|
|
|
329
339
|
function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent, isAllFlags, notifyInspection) {
|
|
330
340
|
let detail;
|
|
331
|
-
let flag;
|
|
332
341
|
|
|
333
|
-
|
|
334
|
-
|
|
342
|
+
const flag = flagStore.get(key);
|
|
343
|
+
|
|
344
|
+
if (flag) {
|
|
335
345
|
detail = getFlagDetail(flag);
|
|
336
346
|
if (flag.value === null || flag.value === undefined) {
|
|
337
347
|
detail.value = defaultValue;
|
|
@@ -373,21 +383,14 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
373
383
|
function allFlags() {
|
|
374
384
|
const results = {};
|
|
375
385
|
|
|
376
|
-
|
|
386
|
+
const allFlags = flagStore.getFlagsWithOverrides();
|
|
387
|
+
|
|
388
|
+
if (!allFlags) {
|
|
377
389
|
return results;
|
|
378
390
|
}
|
|
379
391
|
|
|
380
|
-
for (const key in
|
|
381
|
-
|
|
382
|
-
results[key] = variationDetailInternal(
|
|
383
|
-
key,
|
|
384
|
-
null,
|
|
385
|
-
!options.sendEventsOnlyForVariation,
|
|
386
|
-
false,
|
|
387
|
-
true,
|
|
388
|
-
false
|
|
389
|
-
).value;
|
|
390
|
-
}
|
|
392
|
+
for (const key in allFlags) {
|
|
393
|
+
results[key] = variationDetailInternal(key, null, !options.sendEventsOnlyForVariation, false, true, false).value;
|
|
391
394
|
}
|
|
392
395
|
|
|
393
396
|
return results;
|
|
@@ -485,6 +488,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
485
488
|
// If both the flag and the patch have a version property, then the patch version must be
|
|
486
489
|
// greater than the flag version for us to accept the patch. If either one has no version
|
|
487
490
|
// then the patch always succeeds.
|
|
491
|
+
const flags = flagStore.getFlags();
|
|
488
492
|
const oldFlag = flags[data.key];
|
|
489
493
|
if (!oldFlag || !oldFlag.version || !data.version || oldFlag.version < data.version) {
|
|
490
494
|
logger.debug(messages.debugStreamPatch(data.key));
|
|
@@ -492,6 +496,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
492
496
|
const newFlag = utils.extend({}, data);
|
|
493
497
|
delete newFlag['key'];
|
|
494
498
|
flags[data.key] = newFlag;
|
|
499
|
+
flagStore.setFlags(flags);
|
|
495
500
|
const newDetail = getFlagDetail(newFlag);
|
|
496
501
|
if (oldFlag) {
|
|
497
502
|
mods[data.key] = { previous: oldFlag.value, current: newDetail };
|
|
@@ -509,6 +514,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
509
514
|
if (!data) {
|
|
510
515
|
return;
|
|
511
516
|
}
|
|
517
|
+
const flags = flagStore.getFlags();
|
|
512
518
|
if (!flags[data.key] || flags[data.key].version < data.version) {
|
|
513
519
|
logger.debug(messages.debugStreamDelete(data.key));
|
|
514
520
|
const mods = {};
|
|
@@ -516,6 +522,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
516
522
|
mods[data.key] = { previous: flags[data.key].value };
|
|
517
523
|
}
|
|
518
524
|
flags[data.key] = { version: data.version, deleted: true };
|
|
525
|
+
flagStore.setFlags(flags);
|
|
519
526
|
notifyInspectionFlagChanged(data, flags[data.key]);
|
|
520
527
|
handleFlagChanges(mods); // don't wait for this Promise to be resolved
|
|
521
528
|
} else {
|
|
@@ -542,6 +549,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
542
549
|
return Promise.resolve();
|
|
543
550
|
}
|
|
544
551
|
|
|
552
|
+
const flags = flagStore.getFlags();
|
|
545
553
|
for (const key in flags) {
|
|
546
554
|
if (utils.objectHasOwnProperty(flags, key) && flags[key]) {
|
|
547
555
|
if (newFlags[key] && !utils.deepEquals(newFlags[key].value, flags[key].value)) {
|
|
@@ -557,7 +565,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
557
565
|
}
|
|
558
566
|
}
|
|
559
567
|
|
|
560
|
-
|
|
568
|
+
flagStore.setFlags({ ...newFlags });
|
|
561
569
|
|
|
562
570
|
notifyInspectionFlagsChanged();
|
|
563
571
|
|
|
@@ -580,7 +588,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
580
588
|
});
|
|
581
589
|
|
|
582
590
|
emitter.emit(changeEvent, changeEventParams);
|
|
583
|
-
emitter.emit(internalChangeEvent,
|
|
591
|
+
emitter.emit(internalChangeEvent, flagStore.getFlagsWithOverrides());
|
|
584
592
|
|
|
585
593
|
// By default, we send feature evaluation events whenever we have received new flag values -
|
|
586
594
|
// the client has in effect evaluated these flags just by receiving them. This can be suppressed
|
|
@@ -595,7 +603,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
595
603
|
}
|
|
596
604
|
|
|
597
605
|
if (useLocalStorage && persistentFlagStore) {
|
|
598
|
-
return persistentFlagStore.saveFlags(
|
|
606
|
+
return persistentFlagStore.saveFlags(flagStore.getFlags());
|
|
599
607
|
} else {
|
|
600
608
|
return Promise.resolve();
|
|
601
609
|
}
|
|
@@ -666,7 +674,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
666
674
|
if (typeof options.bootstrap === 'object') {
|
|
667
675
|
// Set the flags as soon as possible before we get into any async code, so application code can read
|
|
668
676
|
// them even if the ready event has not yet fired.
|
|
669
|
-
|
|
677
|
+
flagStore.setFlags(readFlagsFromBootstrap(options.bootstrap));
|
|
670
678
|
}
|
|
671
679
|
|
|
672
680
|
if (stateProvider) {
|
|
@@ -718,7 +726,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
718
726
|
function finishInitWithLocalStorage() {
|
|
719
727
|
return persistentFlagStore.loadFlags().then(storedFlags => {
|
|
720
728
|
if (storedFlags === null || storedFlags === undefined) {
|
|
721
|
-
|
|
729
|
+
flagStore.setFlags({});
|
|
722
730
|
return requestor
|
|
723
731
|
.fetchFlagSettings(ident.getContext(), hash)
|
|
724
732
|
.then(requestedFlags => replaceAllFlags(requestedFlags || {}))
|
|
@@ -731,7 +739,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
731
739
|
// We're reading the flags from local storage. Signal that we're ready,
|
|
732
740
|
// then update localStorage for the next page load. We won't signal changes or update
|
|
733
741
|
// the in-memory flags unless you subscribe for changes
|
|
734
|
-
|
|
742
|
+
flagStore.setFlags(storedFlags);
|
|
735
743
|
utils.onNextTick(signalSuccessfulInit);
|
|
736
744
|
|
|
737
745
|
return requestor
|
|
@@ -746,14 +754,14 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
746
754
|
return requestor
|
|
747
755
|
.fetchFlagSettings(ident.getContext(), hash)
|
|
748
756
|
.then(requestedFlags => {
|
|
749
|
-
|
|
757
|
+
flagStore.setFlags(requestedFlags || {});
|
|
750
758
|
|
|
751
759
|
notifyInspectionFlagsChanged();
|
|
752
760
|
// Note, we don't need to call updateSettings here because local storage and change events are not relevant
|
|
753
761
|
signalSuccessfulInit();
|
|
754
762
|
})
|
|
755
763
|
.catch(err => {
|
|
756
|
-
|
|
764
|
+
flagStore.setFlags({});
|
|
757
765
|
signalFailedInit(err);
|
|
758
766
|
});
|
|
759
767
|
}
|
|
@@ -761,7 +769,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
761
769
|
function initFromStateProvider(state) {
|
|
762
770
|
environment = state.environment;
|
|
763
771
|
ident.setContext(state.context);
|
|
764
|
-
|
|
772
|
+
flagStore.setFlags({ ...state.flags });
|
|
765
773
|
utils.onNextTick(signalSuccessfulInit);
|
|
766
774
|
}
|
|
767
775
|
|
|
@@ -800,7 +808,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
800
808
|
}
|
|
801
809
|
const finishClose = () => {
|
|
802
810
|
closed = true;
|
|
803
|
-
|
|
811
|
+
flagStore.setFlags({});
|
|
804
812
|
};
|
|
805
813
|
const p = Promise.resolve()
|
|
806
814
|
.then(() => {
|
|
@@ -820,7 +828,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
820
828
|
|
|
821
829
|
function getFlagsInternal() {
|
|
822
830
|
// used by Electron integration
|
|
823
|
-
return
|
|
831
|
+
return flagStore.getFlagsWithOverrides();
|
|
824
832
|
}
|
|
825
833
|
|
|
826
834
|
function waitForInitializationWithTimeout(timeout) {
|
|
@@ -880,6 +888,94 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
880
888
|
|
|
881
889
|
registerPlugins(logger, pluginEnvironment, client, plugins);
|
|
882
890
|
|
|
891
|
+
function setOverride(key, value) {
|
|
892
|
+
const mods = {};
|
|
893
|
+
|
|
894
|
+
const currentFlag = flagStore.get(key);
|
|
895
|
+
const currentValue = currentFlag ? currentFlag.value : null;
|
|
896
|
+
|
|
897
|
+
if (currentValue === value) {
|
|
898
|
+
logger.debug(`setOverride: No change needed for ${key}, value already ${value}`);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
flagStore.setOverride(key, value);
|
|
903
|
+
const newFlag = flagStore.get(key);
|
|
904
|
+
const newDetail = getFlagDetail(newFlag);
|
|
905
|
+
|
|
906
|
+
mods[key] = { previous: currentValue, current: newDetail };
|
|
907
|
+
|
|
908
|
+
notifyInspectionFlagChanged({ key }, newFlag);
|
|
909
|
+
handleFlagChanges(mods);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function removeOverride(key) {
|
|
913
|
+
const flagOverrides = flagStore.getFlagOverrides();
|
|
914
|
+
if (!flagOverrides[key]) {
|
|
915
|
+
return; // No override to remove
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const mods = {};
|
|
919
|
+
const oldOverride = flagOverrides[key];
|
|
920
|
+
const flags = flagStore.getFlags();
|
|
921
|
+
const realFlag = flags[key];
|
|
922
|
+
|
|
923
|
+
mods[key] = { previous: oldOverride.value, current: realFlag ? getFlagDetail(realFlag) : undefined };
|
|
924
|
+
|
|
925
|
+
flagStore.removeOverride(key);
|
|
926
|
+
notifyInspectionFlagChanged({ key }, realFlag);
|
|
927
|
+
handleFlagChanges(mods); // don't wait for this Promise to be resolved
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function clearAllOverrides() {
|
|
931
|
+
const flagOverrides = flagStore.getFlagOverrides();
|
|
932
|
+
if (Object.keys(flagOverrides).length === 0) {
|
|
933
|
+
return; // No overrides to clear
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const mods = {};
|
|
937
|
+
const flags = flagStore.getFlags();
|
|
938
|
+
Object.keys(flagOverrides).forEach(key => {
|
|
939
|
+
const oldOverride = flagOverrides[key];
|
|
940
|
+
const realFlag = flags[key];
|
|
941
|
+
|
|
942
|
+
mods[key] = { previous: oldOverride.value, current: realFlag ? getFlagDetail(realFlag) : undefined };
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
flagStore.clearAllOverrides();
|
|
946
|
+
|
|
947
|
+
if (Object.keys(mods).length > 0) {
|
|
948
|
+
handleFlagChanges(mods); // don't wait for this Promise to be resolved
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function getAllOverrides() {
|
|
953
|
+
const flagOverrides = flagStore.getFlagOverrides();
|
|
954
|
+
|
|
955
|
+
if (!flagOverrides) {
|
|
956
|
+
return {}; // No overrides set
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const result = {};
|
|
960
|
+
Object.keys(flagOverrides).forEach(key => {
|
|
961
|
+
const override = flagOverrides[key];
|
|
962
|
+
if (override) {
|
|
963
|
+
result[key] = override.value;
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
return result;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const debugOverride = {
|
|
970
|
+
setOverride: setOverride,
|
|
971
|
+
removeOverride: removeOverride,
|
|
972
|
+
clearAllOverrides: clearAllOverrides,
|
|
973
|
+
getAllOverrides: getAllOverrides,
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
// Register plugins for debug override capabilities
|
|
977
|
+
registerPluginsForDebugOverride(logger, debugOverride, plugins);
|
|
978
|
+
|
|
883
979
|
return {
|
|
884
980
|
client: client, // The client object containing all public methods.
|
|
885
981
|
options: options, // The validated configuration object, including all defaults.
|
package/src/plugins.js
CHANGED
|
@@ -56,6 +56,22 @@ function registerPlugins(logger, environmentMetadata, client, plugins) {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Registers plugins for debug override capabilities
|
|
61
|
+
* @param {{ error: (message: string) => void }} logger - The logger instance
|
|
62
|
+
* @param {Object} debugOverride - The debug override interface object
|
|
63
|
+
* @param {Array<{registerDebug?: (debugOverride: object) => void}>} plugins - Array of plugin objects that may implement registerDebug
|
|
64
|
+
*/
|
|
65
|
+
function registerPluginsForDebugOverride(logger, debugOverride, plugins) {
|
|
66
|
+
plugins.forEach(plugin => {
|
|
67
|
+
try {
|
|
68
|
+
plugin.registerDebug?.(debugOverride);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.error(`Exception thrown registering debug override with plugin ${getPluginName(logger, plugin)}.`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
/**
|
|
60
76
|
* Creates a plugin environment object
|
|
61
77
|
* @param {{userAgent: string, version: string}} platform - The platform object
|
|
@@ -105,5 +121,6 @@ function createPluginEnvironment(platform, env, options) {
|
|
|
105
121
|
module.exports = {
|
|
106
122
|
getPluginHooks,
|
|
107
123
|
registerPlugins,
|
|
124
|
+
registerPluginsForDebugOverride,
|
|
108
125
|
createPluginEnvironment,
|
|
109
126
|
};
|
package/test-types.ts
CHANGED
|
@@ -55,7 +55,7 @@ const plugin: ld.LDPlugin = {
|
|
|
55
55
|
}),
|
|
56
56
|
register(client: ld.LDClientBase, environmentMetadata: ld.LDPluginEnvironmentMetadata): void {
|
|
57
57
|
},
|
|
58
|
-
|
|
58
|
+
registerDebug(debugOverride: ld.LDDebugOverride): void {},
|
|
59
59
|
getHooks(metadata: ld.LDPluginEnvironmentMetadata): ld.Hook[] {
|
|
60
60
|
return [];
|
|
61
61
|
},
|
package/typings.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ declare module 'launchdarkly-js-sdk-common' {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
77
|
+
* Metadata about a hook implementation.
|
|
78
78
|
*/
|
|
79
79
|
export interface HookMetadata {
|
|
80
80
|
/**
|
|
@@ -149,7 +149,6 @@ declare module 'launchdarkly-js-sdk-common' {
|
|
|
149
149
|
readonly metricValue?: number;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
|
|
153
152
|
/**
|
|
154
153
|
* Interface for extending SDK functionality via hooks.
|
|
155
154
|
*/
|
|
@@ -251,7 +250,7 @@ declare module 'launchdarkly-js-sdk-common' {
|
|
|
251
250
|
}
|
|
252
251
|
|
|
253
252
|
/**
|
|
254
|
-
*
|
|
253
|
+
* Metadata about a plugin implementation.
|
|
255
254
|
*
|
|
256
255
|
* May be used in logs and analytics to identify the plugin.
|
|
257
256
|
*/
|
|
@@ -363,6 +362,59 @@ export interface LDPlugin {
|
|
|
363
362
|
* @param metadata
|
|
364
363
|
*/
|
|
365
364
|
getHooks?(metadata: LDPluginEnvironmentMetadata): Hook[];
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* An optional function called if the plugin wants to register debug capabilities.
|
|
368
|
+
* This method allows plugins to receive a debug override interface for
|
|
369
|
+
* temporarily overriding flag values during development and testing.
|
|
370
|
+
*
|
|
371
|
+
* @experimental This interface is experimental and intended for use by LaunchDarkly tools at this time.
|
|
372
|
+
* The API may change in future versions.
|
|
373
|
+
*
|
|
374
|
+
* @param debugOverride The debug override interface instance
|
|
375
|
+
*/
|
|
376
|
+
registerDebug?(debugOverride: LDDebugOverride): void;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Debug interface for plugins that need to override flag values during development.
|
|
381
|
+
* This interface provides methods to temporarily override flag values that take
|
|
382
|
+
* precedence over the actual flag values from LaunchDarkly. These overrides are
|
|
383
|
+
* useful for testing, development, and debugging scenarios.
|
|
384
|
+
*
|
|
385
|
+
* @experimental This interface is experimental and intended for use by LaunchDarkly tools at this time.
|
|
386
|
+
* The API may change in future versions.
|
|
387
|
+
*/
|
|
388
|
+
export interface LDDebugOverride {
|
|
389
|
+
/**
|
|
390
|
+
* Set an override value for a flag that takes precedence over the real flag value.
|
|
391
|
+
*
|
|
392
|
+
* @param flagKey The flag key.
|
|
393
|
+
* @param value The override value.
|
|
394
|
+
*/
|
|
395
|
+
setOverride(flagKey: string, value: LDFlagValue): void;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Remove an override value for a flag, reverting to the real flag value.
|
|
399
|
+
*
|
|
400
|
+
* @param flagKey The flag key.
|
|
401
|
+
*/
|
|
402
|
+
removeOverride(flagKey: string): void;
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Clear all override values, reverting all flags to their real values.
|
|
406
|
+
*/
|
|
407
|
+
clearAllOverrides(): void;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Get all currently active flag overrides.
|
|
411
|
+
*
|
|
412
|
+
* @returns
|
|
413
|
+
* An object containing all active overrides as key-value pairs,
|
|
414
|
+
* where keys are flag keys and values are the overridden flag values.
|
|
415
|
+
* Returns an empty object if no overrides are active.
|
|
416
|
+
*/
|
|
417
|
+
getAllOverrides(): LDFlagSet;
|
|
366
418
|
}
|
|
367
419
|
|
|
368
420
|
/**
|
|
@@ -1071,6 +1123,13 @@ export interface LDPlugin {
|
|
|
1071
1123
|
* Changing the current context also causes all feature flag values to be reloaded. Until that has
|
|
1072
1124
|
* finished, calls to {@link variation} will still return flag values for the previous context. You can
|
|
1073
1125
|
* use a callback or a Promise to determine when the new flag values are available.
|
|
1126
|
+
*
|
|
1127
|
+
* It is possible that the identify call will fail. In that case, when using a callback, the callback will receive
|
|
1128
|
+
* an error value. While the SDK will continue to function, the developer will need to be aware that
|
|
1129
|
+
* calls to {@link variation} will still return flag values for the previous context.
|
|
1130
|
+
*
|
|
1131
|
+
* When using a promise, it is important that you handle the rejection case;
|
|
1132
|
+
* otherwise it will become an unhandled Promise rejection, which is a serious error on some platforms.
|
|
1074
1133
|
*
|
|
1075
1134
|
* @param context
|
|
1076
1135
|
* The context properties. Must contain at least the `key` property.
|