@launchdarkly/toolbar 0.11.1-beta.1 → 0.13.0-beta.1
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/README.md +17 -13
- package/dist/hooks/AfterEvaluationHook.d.ts +17 -0
- package/dist/hooks/AfterIdentifyHook.d.ts +18 -0
- package/dist/hooks/AfterTrackHook.d.ts +17 -0
- package/dist/hooks/EventStore.d.ts +11 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/index.d.ts +6 -3
- package/dist/js/index.js +3355 -2475
- package/dist/js/plugins/FlagOverridePlugin.js +133 -0
- package/dist/js/plugins/index.js +425 -0
- package/dist/plugins/EventInterceptionPlugin.d.ts +30 -0
- package/dist/plugins/FlagOverridePlugin.d.ts +8 -3
- package/dist/plugins/index.d.ts +2 -0
- package/dist/tests/hooks/AfterEvaluationHook.test.d.ts +1 -0
- package/dist/tests/hooks/AfterIdentifyHook.test.d.ts +1 -0
- package/dist/tests/hooks/AfterTrackHook.test.d.ts +1 -0
- package/dist/tests/hooks/EventStore.test.d.ts +1 -0
- package/dist/types/events.d.ts +64 -0
- package/dist/types/plugin.d.ts +21 -0
- package/dist/ui/Toolbar/LaunchDarklyToolbar.d.ts +4 -2
- package/dist/ui/Toolbar/TabContent/EventsTabContent.css.d.ts +18 -0
- package/dist/ui/Toolbar/TabContent/EventsTabContent.d.ts +6 -1
- package/dist/ui/Toolbar/components/DoNotTrackWarning.css.d.ts +5 -0
- package/dist/ui/Toolbar/components/DoNotTrackWarning.d.ts +1 -0
- package/dist/ui/Toolbar/components/ExpandedToolbarContent.d.ts +3 -2
- package/dist/ui/Toolbar/components/TabContentRenderer.d.ts +3 -2
- package/dist/ui/Toolbar/components/index.d.ts +1 -0
- package/dist/ui/Toolbar/constants/animations.d.ts +27 -0
- package/dist/ui/Toolbar/hooks/index.d.ts +2 -0
- package/dist/ui/Toolbar/hooks/useCurrentDate.d.ts +5 -0
- package/dist/ui/Toolbar/hooks/useEvents.d.ts +13 -0
- package/dist/ui/Toolbar/types/toolbar.d.ts +3 -3
- package/dist/utils/browser.d.ts +7 -0
- package/dist/utils/index.d.ts +1 -0
- package/package.json +18 -6
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const DEFAULT_STORAGE_NAMESPACE = 'ld-flag-override';
|
|
2
|
+
class FlagOverridePlugin {
|
|
3
|
+
debugOverride;
|
|
4
|
+
config;
|
|
5
|
+
ldClient = null;
|
|
6
|
+
constructor(config = {}){
|
|
7
|
+
this.config = {
|
|
8
|
+
storageNamespace: config.storageNamespace ?? DEFAULT_STORAGE_NAMESPACE
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
getMetadata() {
|
|
12
|
+
return {
|
|
13
|
+
name: 'FlagOverridePlugin'
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
getHooks(_metadata) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
register(ldClient) {
|
|
20
|
+
this.ldClient = ldClient;
|
|
21
|
+
}
|
|
22
|
+
registerDebug(debugOverride) {
|
|
23
|
+
this.debugOverride = debugOverride;
|
|
24
|
+
this.loadExistingOverrides();
|
|
25
|
+
}
|
|
26
|
+
loadExistingOverrides() {
|
|
27
|
+
if (!this.debugOverride) return;
|
|
28
|
+
const storage = this.getStorage();
|
|
29
|
+
if (!storage) return;
|
|
30
|
+
try {
|
|
31
|
+
for(let i = 0; i < storage.length; i++){
|
|
32
|
+
const key = storage.key(i);
|
|
33
|
+
if (!key?.startsWith(this.config.storageNamespace + ':')) continue;
|
|
34
|
+
const storedValue = storage.getItem(key);
|
|
35
|
+
if (storedValue) try {
|
|
36
|
+
const value = JSON.parse(storedValue);
|
|
37
|
+
const flagKey = key.replace(this.config.storageNamespace + ':', '');
|
|
38
|
+
this.debugOverride.setOverride(flagKey, value);
|
|
39
|
+
} catch {
|
|
40
|
+
console.warn('flagOverridePlugin: Invalid stored value for', key);
|
|
41
|
+
storage.removeItem(key);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('flagOverridePlugin: Error loading existing overrides:', error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
setOverride(flagKey, value) {
|
|
49
|
+
if (!this.debugOverride) return void console.warn('flagOverridePlugin: Debug interface not available');
|
|
50
|
+
if (!flagKey || 'string' != typeof flagKey) return void console.error('flagOverridePlugin: Invalid flag key:', flagKey);
|
|
51
|
+
if (void 0 === value) return void console.error('flagOverridePlugin: Cannot set undefined value for flag override');
|
|
52
|
+
try {
|
|
53
|
+
this.persistOverride(flagKey, value);
|
|
54
|
+
this.debugOverride.setOverride(flagKey, value);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('flagOverridePlugin: Failed to set override:', error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
removeOverride(flagKey) {
|
|
60
|
+
if (!this.debugOverride) return void console.warn('flagOverridePlugin: Debug interface not available');
|
|
61
|
+
if (!flagKey || 'string' != typeof flagKey) return void console.error('flagOverridePlugin: Invalid flag key:', flagKey);
|
|
62
|
+
try {
|
|
63
|
+
this.removePersistedOverride(flagKey);
|
|
64
|
+
this.debugOverride.removeOverride(flagKey);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('flagOverridePlugin: Failed to remove override:', error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
clearAllOverrides() {
|
|
70
|
+
if (!this.debugOverride) return void console.warn('flagOverridePlugin: Debug interface not available');
|
|
71
|
+
try {
|
|
72
|
+
this.clearPersistedOverrides();
|
|
73
|
+
this.debugOverride.clearAllOverrides();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('flagOverridePlugin: Failed to clear overrides:', error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
getAllOverrides() {
|
|
79
|
+
if (!this.debugOverride) {
|
|
80
|
+
console.warn('flagOverridePlugin: Debug interface not available');
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return this.debugOverride.getAllOverrides();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('flagOverridePlugin: Failed to get overrides:', error);
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
getClient() {
|
|
91
|
+
return this.ldClient;
|
|
92
|
+
}
|
|
93
|
+
getStorage() {
|
|
94
|
+
if ('undefined' == typeof window) return null;
|
|
95
|
+
return window.localStorage;
|
|
96
|
+
}
|
|
97
|
+
persistOverride(flagKey, value) {
|
|
98
|
+
const storage = this.getStorage();
|
|
99
|
+
if (!storage) return;
|
|
100
|
+
try {
|
|
101
|
+
const storageKey = `${this.config.storageNamespace}:${flagKey}`;
|
|
102
|
+
storage.setItem(storageKey, JSON.stringify(value));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('flagOverridePlugin: Failed to persist override:', error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
removePersistedOverride(flagKey) {
|
|
108
|
+
const storage = this.getStorage();
|
|
109
|
+
if (!storage) return;
|
|
110
|
+
try {
|
|
111
|
+
const storageKey = `${this.config.storageNamespace}:${flagKey}`;
|
|
112
|
+
storage.removeItem(storageKey);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('flagOverridePlugin: Failed to remove persisted override:', error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
clearPersistedOverrides() {
|
|
118
|
+
const storage = this.getStorage();
|
|
119
|
+
if (!storage) return;
|
|
120
|
+
try {
|
|
121
|
+
const keysToRemove = [];
|
|
122
|
+
const prefix = this.config.storageNamespace + ':';
|
|
123
|
+
for(let i = 0; i < storage.length; i++){
|
|
124
|
+
const key = storage.key(i);
|
|
125
|
+
if (key?.startsWith(prefix)) keysToRemove.push(key);
|
|
126
|
+
}
|
|
127
|
+
keysToRemove.forEach((key)=>storage.removeItem(key));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('flagOverridePlugin: Failed to clear persisted overrides:', error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export { FlagOverridePlugin };
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
const DEFAULT_STORAGE_NAMESPACE = 'ld-flag-override';
|
|
2
|
+
class FlagOverridePlugin {
|
|
3
|
+
debugOverride;
|
|
4
|
+
config;
|
|
5
|
+
ldClient = null;
|
|
6
|
+
constructor(config = {}){
|
|
7
|
+
this.config = {
|
|
8
|
+
storageNamespace: config.storageNamespace ?? DEFAULT_STORAGE_NAMESPACE
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
getMetadata() {
|
|
12
|
+
return {
|
|
13
|
+
name: 'FlagOverridePlugin'
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
getHooks(_metadata) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
register(ldClient) {
|
|
20
|
+
this.ldClient = ldClient;
|
|
21
|
+
}
|
|
22
|
+
registerDebug(debugOverride) {
|
|
23
|
+
this.debugOverride = debugOverride;
|
|
24
|
+
this.loadExistingOverrides();
|
|
25
|
+
}
|
|
26
|
+
loadExistingOverrides() {
|
|
27
|
+
if (!this.debugOverride) return;
|
|
28
|
+
const storage = this.getStorage();
|
|
29
|
+
if (!storage) return;
|
|
30
|
+
try {
|
|
31
|
+
for(let i = 0; i < storage.length; i++){
|
|
32
|
+
const key = storage.key(i);
|
|
33
|
+
if (!key?.startsWith(this.config.storageNamespace + ':')) continue;
|
|
34
|
+
const storedValue = storage.getItem(key);
|
|
35
|
+
if (storedValue) try {
|
|
36
|
+
const value = JSON.parse(storedValue);
|
|
37
|
+
const flagKey = key.replace(this.config.storageNamespace + ':', '');
|
|
38
|
+
this.debugOverride.setOverride(flagKey, value);
|
|
39
|
+
} catch {
|
|
40
|
+
console.warn('flagOverridePlugin: Invalid stored value for', key);
|
|
41
|
+
storage.removeItem(key);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('flagOverridePlugin: Error loading existing overrides:', error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
setOverride(flagKey, value) {
|
|
49
|
+
if (!this.debugOverride) return void console.warn('flagOverridePlugin: Debug interface not available');
|
|
50
|
+
if (!flagKey || 'string' != typeof flagKey) return void console.error('flagOverridePlugin: Invalid flag key:', flagKey);
|
|
51
|
+
if (void 0 === value) return void console.error('flagOverridePlugin: Cannot set undefined value for flag override');
|
|
52
|
+
try {
|
|
53
|
+
this.persistOverride(flagKey, value);
|
|
54
|
+
this.debugOverride.setOverride(flagKey, value);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('flagOverridePlugin: Failed to set override:', error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
removeOverride(flagKey) {
|
|
60
|
+
if (!this.debugOverride) return void console.warn('flagOverridePlugin: Debug interface not available');
|
|
61
|
+
if (!flagKey || 'string' != typeof flagKey) return void console.error('flagOverridePlugin: Invalid flag key:', flagKey);
|
|
62
|
+
try {
|
|
63
|
+
this.removePersistedOverride(flagKey);
|
|
64
|
+
this.debugOverride.removeOverride(flagKey);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('flagOverridePlugin: Failed to remove override:', error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
clearAllOverrides() {
|
|
70
|
+
if (!this.debugOverride) return void console.warn('flagOverridePlugin: Debug interface not available');
|
|
71
|
+
try {
|
|
72
|
+
this.clearPersistedOverrides();
|
|
73
|
+
this.debugOverride.clearAllOverrides();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('flagOverridePlugin: Failed to clear overrides:', error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
getAllOverrides() {
|
|
79
|
+
if (!this.debugOverride) {
|
|
80
|
+
console.warn('flagOverridePlugin: Debug interface not available');
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return this.debugOverride.getAllOverrides();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('flagOverridePlugin: Failed to get overrides:', error);
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
getClient() {
|
|
91
|
+
return this.ldClient;
|
|
92
|
+
}
|
|
93
|
+
getStorage() {
|
|
94
|
+
if ('undefined' == typeof window) return null;
|
|
95
|
+
return window.localStorage;
|
|
96
|
+
}
|
|
97
|
+
persistOverride(flagKey, value) {
|
|
98
|
+
const storage = this.getStorage();
|
|
99
|
+
if (!storage) return;
|
|
100
|
+
try {
|
|
101
|
+
const storageKey = `${this.config.storageNamespace}:${flagKey}`;
|
|
102
|
+
storage.setItem(storageKey, JSON.stringify(value));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('flagOverridePlugin: Failed to persist override:', error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
removePersistedOverride(flagKey) {
|
|
108
|
+
const storage = this.getStorage();
|
|
109
|
+
if (!storage) return;
|
|
110
|
+
try {
|
|
111
|
+
const storageKey = `${this.config.storageNamespace}:${flagKey}`;
|
|
112
|
+
storage.removeItem(storageKey);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('flagOverridePlugin: Failed to remove persisted override:', error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
clearPersistedOverrides() {
|
|
118
|
+
const storage = this.getStorage();
|
|
119
|
+
if (!storage) return;
|
|
120
|
+
try {
|
|
121
|
+
const keysToRemove = [];
|
|
122
|
+
const prefix = this.config.storageNamespace + ':';
|
|
123
|
+
for(let i = 0; i < storage.length; i++){
|
|
124
|
+
const key = storage.key(i);
|
|
125
|
+
if (key?.startsWith(prefix)) keysToRemove.push(key);
|
|
126
|
+
}
|
|
127
|
+
keysToRemove.forEach((key)=>storage.removeItem(key));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('flagOverridePlugin: Failed to clear persisted overrides:', error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const MAX_EVENTS = 100;
|
|
134
|
+
class EventStore {
|
|
135
|
+
events = [];
|
|
136
|
+
listeners = new Set();
|
|
137
|
+
addEvent(event) {
|
|
138
|
+
try {
|
|
139
|
+
this.events.push(event);
|
|
140
|
+
if (this.events.length > MAX_EVENTS) this.events.splice(0, this.events.length - MAX_EVENTS);
|
|
141
|
+
this.notifyListeners();
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.warn('Event store error:', error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
getEvents() {
|
|
147
|
+
return [
|
|
148
|
+
...this.events
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
subscribe(listener) {
|
|
152
|
+
this.listeners.add(listener);
|
|
153
|
+
listener();
|
|
154
|
+
return ()=>this.listeners.delete(listener);
|
|
155
|
+
}
|
|
156
|
+
clear() {
|
|
157
|
+
this.events = [];
|
|
158
|
+
this.notifyListeners();
|
|
159
|
+
}
|
|
160
|
+
destroy() {
|
|
161
|
+
this.listeners.clear();
|
|
162
|
+
this.events = [];
|
|
163
|
+
}
|
|
164
|
+
notifyListeners() {
|
|
165
|
+
this.listeners.forEach((listener)=>{
|
|
166
|
+
try {
|
|
167
|
+
listener();
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.warn('Listener error:', error);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
class AfterTrackHook {
|
|
175
|
+
config;
|
|
176
|
+
idCounter = 0;
|
|
177
|
+
constructor(config = {}){
|
|
178
|
+
this.config = {
|
|
179
|
+
filter: config.filter,
|
|
180
|
+
onNewEvent: config.onNewEvent
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
getMetadata() {
|
|
184
|
+
return {
|
|
185
|
+
name: 'AfterTrackHook'
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
afterTrack(hookContext) {
|
|
189
|
+
try {
|
|
190
|
+
const syntheticContext = {
|
|
191
|
+
kind: 'custom',
|
|
192
|
+
key: hookContext.key,
|
|
193
|
+
context: hookContext.context,
|
|
194
|
+
data: hookContext.data,
|
|
195
|
+
metricValue: hookContext.metricValue,
|
|
196
|
+
creationDate: Date.now(),
|
|
197
|
+
url: 'undefined' != typeof window ? window.location.href : void 0
|
|
198
|
+
};
|
|
199
|
+
if (!this.shouldProcessEvent()) return;
|
|
200
|
+
const processedEvent = this.processEvent(syntheticContext);
|
|
201
|
+
this.config.onNewEvent?.(processedEvent);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.warn('Event processing error in AfterTrackHook:', error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
shouldProcessEvent() {
|
|
207
|
+
const filter = this.config.filter;
|
|
208
|
+
if (!filter) return true;
|
|
209
|
+
return !(filter.kinds && !filter.kinds.includes('custom')) && !(filter.categories && !filter.categories.includes('custom'));
|
|
210
|
+
}
|
|
211
|
+
processEvent(context) {
|
|
212
|
+
const timestamp = Date.now();
|
|
213
|
+
this.idCounter = (this.idCounter + 1) % 999999;
|
|
214
|
+
const randomPart = Math.random().toString(36).substring(2, 8);
|
|
215
|
+
const id = `${context.kind}-${timestamp}-${this.idCounter.toString().padStart(6, '0')}-${randomPart}`;
|
|
216
|
+
return {
|
|
217
|
+
id,
|
|
218
|
+
kind: context.kind,
|
|
219
|
+
key: context.key,
|
|
220
|
+
timestamp,
|
|
221
|
+
context,
|
|
222
|
+
displayName: `Custom: ${context.key || 'unknown'}`,
|
|
223
|
+
category: 'custom',
|
|
224
|
+
metadata: this.extractMetadata(context)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
extractMetadata(context) {
|
|
228
|
+
return {
|
|
229
|
+
data: context.data,
|
|
230
|
+
metricValue: context.metricValue,
|
|
231
|
+
url: context.url
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
class AfterIdentifyHook {
|
|
236
|
+
config;
|
|
237
|
+
idCounter = 0;
|
|
238
|
+
constructor(config = {}){
|
|
239
|
+
this.config = {
|
|
240
|
+
filter: config.filter,
|
|
241
|
+
onNewEvent: config.onNewEvent
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
getMetadata() {
|
|
245
|
+
return {
|
|
246
|
+
name: 'AfterIdentifyHook'
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
afterIdentify(hookContext, data, result) {
|
|
250
|
+
try {
|
|
251
|
+
if ('completed' !== result.status) return data;
|
|
252
|
+
const syntheticContext = {
|
|
253
|
+
kind: 'identify',
|
|
254
|
+
context: hookContext.context,
|
|
255
|
+
creationDate: Date.now(),
|
|
256
|
+
contextKind: this.determineContextKind(hookContext.context)
|
|
257
|
+
};
|
|
258
|
+
if (!this.shouldProcessEvent()) return data;
|
|
259
|
+
const processedEvent = this.processEvent(syntheticContext);
|
|
260
|
+
this.config.onNewEvent?.(processedEvent);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.warn('Event processing error in AfterIdentifyHook:', error);
|
|
263
|
+
}
|
|
264
|
+
return data;
|
|
265
|
+
}
|
|
266
|
+
determineContextKind(context) {
|
|
267
|
+
if (context && 'object' == typeof context) {
|
|
268
|
+
if ('kind' in context && context.kind) return context.kind;
|
|
269
|
+
if (context.anonymous) return 'anonymousUser';
|
|
270
|
+
}
|
|
271
|
+
return 'user';
|
|
272
|
+
}
|
|
273
|
+
shouldProcessEvent() {
|
|
274
|
+
const filter = this.config.filter;
|
|
275
|
+
if (!filter) return true;
|
|
276
|
+
return !(filter.kinds && !filter.kinds.includes('identify')) && !(filter.categories && !filter.categories.includes('identify'));
|
|
277
|
+
}
|
|
278
|
+
processEvent(context) {
|
|
279
|
+
const timestamp = Date.now();
|
|
280
|
+
this.idCounter = (this.idCounter + 1) % 999999;
|
|
281
|
+
const randomPart = Math.random().toString(36).substring(2, 8);
|
|
282
|
+
const id = `${context.kind}-${timestamp}-${this.idCounter.toString().padStart(6, '0')}-${randomPart}`;
|
|
283
|
+
return {
|
|
284
|
+
id,
|
|
285
|
+
kind: context.kind,
|
|
286
|
+
key: context.key,
|
|
287
|
+
timestamp,
|
|
288
|
+
context,
|
|
289
|
+
displayName: `Identify: ${context.context?.key || 'anonymous'}`,
|
|
290
|
+
category: 'identify',
|
|
291
|
+
metadata: this.extractMetadata(context)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
extractMetadata(context) {
|
|
295
|
+
return {
|
|
296
|
+
contextKind: context.contextKind
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
class AfterEvaluationHook {
|
|
301
|
+
config;
|
|
302
|
+
idCounter = 0;
|
|
303
|
+
constructor(config = {}){
|
|
304
|
+
this.config = {
|
|
305
|
+
filter: config.filter,
|
|
306
|
+
onNewEvent: config.onNewEvent
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
getMetadata() {
|
|
310
|
+
return {
|
|
311
|
+
name: 'AfterEvaluationHook'
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
afterEvaluation(hookContext, data, detail) {
|
|
315
|
+
try {
|
|
316
|
+
const syntheticContext = {
|
|
317
|
+
kind: 'feature',
|
|
318
|
+
key: hookContext.flagKey,
|
|
319
|
+
context: hookContext.context,
|
|
320
|
+
value: detail.value,
|
|
321
|
+
variation: detail.variationIndex,
|
|
322
|
+
default: hookContext.defaultValue,
|
|
323
|
+
reason: detail.reason,
|
|
324
|
+
creationDate: Date.now()
|
|
325
|
+
};
|
|
326
|
+
if (!this.shouldProcessEvent(syntheticContext)) return data;
|
|
327
|
+
const processedEvent = this.processEvent(syntheticContext);
|
|
328
|
+
this.config.onNewEvent?.(processedEvent);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.warn('Event processing error in AfterEvaluationHook:', error);
|
|
331
|
+
}
|
|
332
|
+
return data;
|
|
333
|
+
}
|
|
334
|
+
shouldProcessEvent(context) {
|
|
335
|
+
const filter = this.config.filter;
|
|
336
|
+
if (!filter) return true;
|
|
337
|
+
return !(filter.kinds && !filter.kinds.includes('feature')) && !(filter.categories && !filter.categories.includes('flag')) && !(filter.flagKeys && context.key && !filter.flagKeys.includes(context.key));
|
|
338
|
+
}
|
|
339
|
+
processEvent(context) {
|
|
340
|
+
const timestamp = Date.now();
|
|
341
|
+
this.idCounter = (this.idCounter + 1) % 999999;
|
|
342
|
+
const randomPart = Math.random().toString(36).substring(2, 8);
|
|
343
|
+
const id = `${context.kind}-${timestamp}-${this.idCounter.toString().padStart(6, '0')}-${randomPart}`;
|
|
344
|
+
return {
|
|
345
|
+
id,
|
|
346
|
+
kind: context.kind,
|
|
347
|
+
key: context.key,
|
|
348
|
+
timestamp,
|
|
349
|
+
context,
|
|
350
|
+
displayName: `Flag: ${context.key || 'unknown'}`,
|
|
351
|
+
category: 'flag',
|
|
352
|
+
metadata: this.extractMetadata(context)
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
extractMetadata(context) {
|
|
356
|
+
return {
|
|
357
|
+
flagVersion: context.version,
|
|
358
|
+
variation: context.variation,
|
|
359
|
+
trackEvents: context.trackEvents,
|
|
360
|
+
reason: context.reason,
|
|
361
|
+
defaultValue: context.default
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
class EventInterceptionPlugin {
|
|
366
|
+
afterTrackHook;
|
|
367
|
+
afterIdentifyHook;
|
|
368
|
+
afterEvaluationHook;
|
|
369
|
+
eventStore;
|
|
370
|
+
config;
|
|
371
|
+
constructor(config = {}){
|
|
372
|
+
this.config = {
|
|
373
|
+
enableLogging: true,
|
|
374
|
+
...config
|
|
375
|
+
};
|
|
376
|
+
this.eventStore = new EventStore();
|
|
377
|
+
const onNewEvent = (event)=>{
|
|
378
|
+
if (this.config.enableLogging) console.log('🎯 Event intercepted:', {
|
|
379
|
+
kind: event.kind,
|
|
380
|
+
key: event.key,
|
|
381
|
+
category: event.category,
|
|
382
|
+
displayName: event.displayName
|
|
383
|
+
});
|
|
384
|
+
this.eventStore.addEvent(event);
|
|
385
|
+
};
|
|
386
|
+
this.afterTrackHook = new AfterTrackHook({
|
|
387
|
+
filter: config.filter,
|
|
388
|
+
onNewEvent
|
|
389
|
+
});
|
|
390
|
+
this.afterIdentifyHook = new AfterIdentifyHook({
|
|
391
|
+
filter: config.filter,
|
|
392
|
+
onNewEvent
|
|
393
|
+
});
|
|
394
|
+
this.afterEvaluationHook = new AfterEvaluationHook({
|
|
395
|
+
filter: config.filter,
|
|
396
|
+
onNewEvent
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
getMetadata() {
|
|
400
|
+
return {
|
|
401
|
+
name: 'EventInterceptionPlugin'
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
getHooks(_metadata) {
|
|
405
|
+
return [
|
|
406
|
+
this.afterTrackHook,
|
|
407
|
+
this.afterIdentifyHook,
|
|
408
|
+
this.afterEvaluationHook
|
|
409
|
+
];
|
|
410
|
+
}
|
|
411
|
+
register(_client) {}
|
|
412
|
+
getEvents() {
|
|
413
|
+
return this.eventStore.getEvents();
|
|
414
|
+
}
|
|
415
|
+
subscribe(listener) {
|
|
416
|
+
return this.eventStore.subscribe(listener);
|
|
417
|
+
}
|
|
418
|
+
clearEvents() {
|
|
419
|
+
this.eventStore.clear();
|
|
420
|
+
}
|
|
421
|
+
destroy() {
|
|
422
|
+
this.eventStore.destroy();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
export { EventInterceptionPlugin, FlagOverridePlugin };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Hook, LDClient, LDPluginEnvironmentMetadata, LDPluginMetadata } from 'launchdarkly-js-client-sdk';
|
|
2
|
+
import type { EventFilter, ProcessedEvent } from '../types/events';
|
|
3
|
+
import type { IEventInterceptionPlugin } from '../types/plugin';
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for the EventInterceptionPlugin
|
|
6
|
+
*/
|
|
7
|
+
export interface EventInterceptionPluginConfig {
|
|
8
|
+
/** Configuration for event filtering */
|
|
9
|
+
filter?: EventFilter;
|
|
10
|
+
/** Enable console logging for debugging */
|
|
11
|
+
enableLogging?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Plugin dedicated to intercepting and processing LaunchDarkly events
|
|
15
|
+
*/
|
|
16
|
+
export declare class EventInterceptionPlugin implements IEventInterceptionPlugin {
|
|
17
|
+
private afterTrackHook;
|
|
18
|
+
private afterIdentifyHook;
|
|
19
|
+
private afterEvaluationHook;
|
|
20
|
+
private eventStore;
|
|
21
|
+
private config;
|
|
22
|
+
constructor(config?: EventInterceptionPluginConfig);
|
|
23
|
+
getMetadata(): LDPluginMetadata;
|
|
24
|
+
getHooks(_metadata: LDPluginEnvironmentMetadata): Hook[];
|
|
25
|
+
register(_client: LDClient): void;
|
|
26
|
+
getEvents(): ProcessedEvent[];
|
|
27
|
+
subscribe(listener: () => void): () => void;
|
|
28
|
+
clearEvents(): void;
|
|
29
|
+
destroy(): void;
|
|
30
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { LDClient,
|
|
1
|
+
import type { LDClient, LDDebugOverride, LDPluginMetadata, LDFlagSet, Hook, LDPluginEnvironmentMetadata } from 'launchdarkly-js-client-sdk';
|
|
2
|
+
import type { IFlagOverridePlugin } from '../types/plugin';
|
|
2
3
|
/**
|
|
3
4
|
* Configuration options for the FlagOverridePlugin
|
|
4
5
|
*/
|
|
@@ -6,7 +7,7 @@ export type FlagOverridePluginConfig = {
|
|
|
6
7
|
/** Namespace for localStorage keys. Defaults to 'ld-flag-override' */
|
|
7
8
|
storageNamespace?: string;
|
|
8
9
|
};
|
|
9
|
-
export declare class FlagOverridePlugin implements
|
|
10
|
+
export declare class FlagOverridePlugin implements IFlagOverridePlugin {
|
|
10
11
|
private debugOverride?;
|
|
11
12
|
private config;
|
|
12
13
|
private ldClient;
|
|
@@ -15,6 +16,10 @@ export declare class FlagOverridePlugin implements LDPlugin {
|
|
|
15
16
|
* Returns plugin metadata
|
|
16
17
|
*/
|
|
17
18
|
getMetadata(): LDPluginMetadata;
|
|
19
|
+
/**
|
|
20
|
+
* Returns the hooks for the plugin
|
|
21
|
+
*/
|
|
22
|
+
getHooks(_metadata: LDPluginEnvironmentMetadata): Hook[];
|
|
18
23
|
/**
|
|
19
24
|
* Called when the plugin is registered with the LaunchDarkly client
|
|
20
25
|
*/
|
|
@@ -44,7 +49,7 @@ export declare class FlagOverridePlugin implements LDPlugin {
|
|
|
44
49
|
* Returns all currently active feature flag overrides
|
|
45
50
|
* @returns Record of flag keys to their override values
|
|
46
51
|
*/
|
|
47
|
-
getAllOverrides():
|
|
52
|
+
getAllOverrides(): LDFlagSet;
|
|
48
53
|
/**
|
|
49
54
|
* Returns the LaunchDarkly client instance
|
|
50
55
|
* @returns The LaunchDarkly client
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { FlagOverridePlugin } from './FlagOverridePlugin';
|
|
2
2
|
export type { FlagOverridePluginConfig } from './FlagOverridePlugin';
|
|
3
|
+
export { EventInterceptionPlugin } from './EventInterceptionPlugin';
|
|
4
|
+
export type { EventInterceptionPluginConfig } from './EventInterceptionPlugin';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|