@uistate/core 2.0.1 → 3.0.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/README.md +39 -103
- package/package.json +1 -1
- package/src/cssState.js +53 -9
- package/src/index.js +282 -40
- package/src/stateInspector.js +140 -0
- package/src/stateSerializer.js +204 -0
- package/src/telemetryPlugin.js +638 -0
- package/src/templateManager.js +119 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UIstate Telemetry Plugin
|
|
3
|
+
*
|
|
4
|
+
* A plugin for tracking and analyzing usage patterns in UIstate applications.
|
|
5
|
+
* Provides insights into method calls, state changes, and performance metrics.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Automatic tracking of API method calls
|
|
9
|
+
* - State change monitoring
|
|
10
|
+
* - Performance timing
|
|
11
|
+
* - Usage statistics and reporting
|
|
12
|
+
* - Visual dashboard integration
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a telemetry plugin instance
|
|
17
|
+
* @param {Object} config - Configuration options
|
|
18
|
+
* @returns {Object} - Configured telemetry plugin
|
|
19
|
+
*/
|
|
20
|
+
const createTelemetryPlugin = (config = {}) => {
|
|
21
|
+
// Default configuration
|
|
22
|
+
const defaultConfig = {
|
|
23
|
+
enabled: true,
|
|
24
|
+
trackStateChanges: true,
|
|
25
|
+
trackMethodCalls: true,
|
|
26
|
+
trackPerformance: true,
|
|
27
|
+
maxEntries: 1000,
|
|
28
|
+
logToConsole: false,
|
|
29
|
+
samplingRate: 1.0, // 1.0 = track everything, 0.5 = track 50% of events
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Merge provided config with defaults
|
|
33
|
+
const options = { ...defaultConfig, ...config };
|
|
34
|
+
|
|
35
|
+
// Telemetry data storage
|
|
36
|
+
const data = {
|
|
37
|
+
startTime: Date.now(),
|
|
38
|
+
methodCalls: new Map(),
|
|
39
|
+
stateChanges: new Map(),
|
|
40
|
+
performanceMetrics: new Map(),
|
|
41
|
+
unusedMethods: new Set(),
|
|
42
|
+
activeTimers: new Map()
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Reference to the UIstate instance
|
|
46
|
+
let uistateRef = null;
|
|
47
|
+
|
|
48
|
+
// Original method references
|
|
49
|
+
const originalMethods = new Map();
|
|
50
|
+
|
|
51
|
+
// Methods to track (will be populated during initialization)
|
|
52
|
+
const methodsToTrack = [
|
|
53
|
+
'getState', 'setState', 'subscribe', 'observe',
|
|
54
|
+
'configureSerializer', 'getSerializerConfig', 'setSerializationMode',
|
|
55
|
+
'onAction', 'registerActions', 'attachDelegation', 'mount',
|
|
56
|
+
'renderTemplateFromCss', 'createComponent', 'applyClassesFromState'
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Helper function to check if we should sample this event
|
|
60
|
+
const shouldSample = () => {
|
|
61
|
+
return options.samplingRate >= 1.0 || Math.random() <= options.samplingRate;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Helper function to track a method call
|
|
65
|
+
const trackMethodCall = (methodName, args = [], result = undefined, error = null, duration = 0) => {
|
|
66
|
+
if (!options.enabled || !options.trackMethodCalls || !shouldSample()) return;
|
|
67
|
+
|
|
68
|
+
if (!data.methodCalls.has(methodName)) {
|
|
69
|
+
data.methodCalls.set(methodName, []);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const calls = data.methodCalls.get(methodName);
|
|
73
|
+
|
|
74
|
+
// Limit the number of entries to prevent memory issues
|
|
75
|
+
if (calls.length >= options.maxEntries) {
|
|
76
|
+
calls.shift(); // Remove oldest entry
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Create a safe copy of arguments (avoiding circular references)
|
|
80
|
+
const safeArgs = Array.from(args).map(arg => {
|
|
81
|
+
try {
|
|
82
|
+
// For simple types, return as is
|
|
83
|
+
if (arg === null || arg === undefined ||
|
|
84
|
+
typeof arg === 'string' ||
|
|
85
|
+
typeof arg === 'number' ||
|
|
86
|
+
typeof arg === 'boolean') {
|
|
87
|
+
return arg;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// For functions, just return the function name or "[Function]"
|
|
91
|
+
if (typeof arg === 'function') {
|
|
92
|
+
return arg.name || "[Function]";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// For objects, create a simplified representation
|
|
96
|
+
if (typeof arg === 'object') {
|
|
97
|
+
if (Array.isArray(arg)) {
|
|
98
|
+
return `[Array(${arg.length})]`;
|
|
99
|
+
}
|
|
100
|
+
return Object.keys(arg).length > 0
|
|
101
|
+
? `[Object: ${Object.keys(arg).join(', ')}]`
|
|
102
|
+
: '[Object]';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return String(arg);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return "[Complex Value]";
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Create a safe copy of the result
|
|
112
|
+
let safeResult;
|
|
113
|
+
try {
|
|
114
|
+
if (result === null || result === undefined ||
|
|
115
|
+
typeof result === 'string' ||
|
|
116
|
+
typeof result === 'number' ||
|
|
117
|
+
typeof result === 'boolean') {
|
|
118
|
+
safeResult = result;
|
|
119
|
+
} else if (typeof result === 'function') {
|
|
120
|
+
safeResult = result.name || "[Function]";
|
|
121
|
+
} else if (typeof result === 'object') {
|
|
122
|
+
if (Array.isArray(result)) {
|
|
123
|
+
safeResult = `[Array(${result.length})]`;
|
|
124
|
+
} else {
|
|
125
|
+
safeResult = Object.keys(result).length > 0
|
|
126
|
+
? `[Object: ${Object.keys(result).join(', ')}]`
|
|
127
|
+
: '[Object]';
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
safeResult = String(result);
|
|
131
|
+
}
|
|
132
|
+
} catch (e) {
|
|
133
|
+
safeResult = "[Complex Value]";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Record the call
|
|
137
|
+
calls.push({
|
|
138
|
+
timestamp: Date.now(),
|
|
139
|
+
relativeTime: Date.now() - data.startTime,
|
|
140
|
+
args: safeArgs,
|
|
141
|
+
result: safeResult,
|
|
142
|
+
error: error ? error.message : null,
|
|
143
|
+
duration
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Log to console if enabled
|
|
147
|
+
if (options.logToConsole) {
|
|
148
|
+
console.log(`[Telemetry] ${methodName}`, {
|
|
149
|
+
args: safeArgs,
|
|
150
|
+
result: safeResult,
|
|
151
|
+
error: error ? error.message : null,
|
|
152
|
+
duration
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Helper function to track state changes
|
|
158
|
+
const trackStateChange = (path, value, previousValue) => {
|
|
159
|
+
if (!options.enabled || !options.trackStateChanges || !shouldSample()) return;
|
|
160
|
+
|
|
161
|
+
if (!data.stateChanges.has(path)) {
|
|
162
|
+
data.stateChanges.set(path, []);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const changes = data.stateChanges.get(path);
|
|
166
|
+
|
|
167
|
+
// Limit the number of entries to prevent memory issues
|
|
168
|
+
if (changes.length >= options.maxEntries) {
|
|
169
|
+
changes.shift(); // Remove oldest entry
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Create safe copies of values
|
|
173
|
+
let safeValue, safePreviousValue;
|
|
174
|
+
try {
|
|
175
|
+
safeValue = JSON.stringify(value);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
safeValue = String(value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
safePreviousValue = JSON.stringify(previousValue);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
safePreviousValue = String(previousValue);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Record the change
|
|
187
|
+
changes.push({
|
|
188
|
+
timestamp: Date.now(),
|
|
189
|
+
relativeTime: Date.now() - data.startTime,
|
|
190
|
+
value: safeValue,
|
|
191
|
+
previousValue: safePreviousValue
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Helper function to start a performance timer
|
|
196
|
+
const startTimer = (label) => {
|
|
197
|
+
if (!options.enabled || !options.trackPerformance) return;
|
|
198
|
+
|
|
199
|
+
data.activeTimers.set(label, performance.now());
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Helper function to end a performance timer
|
|
203
|
+
const endTimer = (label) => {
|
|
204
|
+
if (!options.enabled || !options.trackPerformance) return 0;
|
|
205
|
+
|
|
206
|
+
if (!data.activeTimers.has(label)) return 0;
|
|
207
|
+
|
|
208
|
+
const startTime = data.activeTimers.get(label);
|
|
209
|
+
const duration = performance.now() - startTime;
|
|
210
|
+
|
|
211
|
+
data.activeTimers.delete(label);
|
|
212
|
+
|
|
213
|
+
if (!data.performanceMetrics.has(label)) {
|
|
214
|
+
data.performanceMetrics.set(label, {
|
|
215
|
+
count: 0,
|
|
216
|
+
totalDuration: 0,
|
|
217
|
+
minDuration: Infinity,
|
|
218
|
+
maxDuration: 0,
|
|
219
|
+
samples: []
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const metrics = data.performanceMetrics.get(label);
|
|
224
|
+
metrics.count++;
|
|
225
|
+
metrics.totalDuration += duration;
|
|
226
|
+
metrics.minDuration = Math.min(metrics.minDuration, duration);
|
|
227
|
+
metrics.maxDuration = Math.max(metrics.maxDuration, duration);
|
|
228
|
+
|
|
229
|
+
// Store sample data (limited to prevent memory issues)
|
|
230
|
+
if (metrics.samples.length >= options.maxEntries) {
|
|
231
|
+
metrics.samples.shift();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
metrics.samples.push({
|
|
235
|
+
timestamp: Date.now(),
|
|
236
|
+
relativeTime: Date.now() - data.startTime,
|
|
237
|
+
duration
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return duration;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Method proxying function
|
|
244
|
+
const createMethodProxy = (obj, methodName) => {
|
|
245
|
+
// Store original method
|
|
246
|
+
const originalMethod = obj[methodName];
|
|
247
|
+
originalMethods.set(methodName, originalMethod);
|
|
248
|
+
|
|
249
|
+
// Create proxy
|
|
250
|
+
return function(...args) {
|
|
251
|
+
if (!options.enabled) {
|
|
252
|
+
return originalMethod.apply(this, args);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let result, error;
|
|
256
|
+
|
|
257
|
+
startTimer(`method.${methodName}`);
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
// Call original method
|
|
261
|
+
result = originalMethod.apply(this, args);
|
|
262
|
+
|
|
263
|
+
// Handle promises
|
|
264
|
+
if (result instanceof Promise) {
|
|
265
|
+
return result
|
|
266
|
+
.then(asyncResult => {
|
|
267
|
+
const duration = endTimer(`method.${methodName}`);
|
|
268
|
+
trackMethodCall(methodName, args, asyncResult, null, duration);
|
|
269
|
+
return asyncResult;
|
|
270
|
+
})
|
|
271
|
+
.catch(asyncError => {
|
|
272
|
+
const duration = endTimer(`method.${methodName}`);
|
|
273
|
+
trackMethodCall(methodName, args, undefined, asyncError, duration);
|
|
274
|
+
throw asyncError;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const duration = endTimer(`method.${methodName}`);
|
|
279
|
+
trackMethodCall(methodName, args, result, null, duration);
|
|
280
|
+
return result;
|
|
281
|
+
|
|
282
|
+
} catch (e) {
|
|
283
|
+
error = e;
|
|
284
|
+
const duration = endTimer(`method.${methodName}`);
|
|
285
|
+
trackMethodCall(methodName, args, undefined, error, duration);
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// Create the telemetry plugin
|
|
292
|
+
const telemetryPlugin = {
|
|
293
|
+
name: 'telemetry',
|
|
294
|
+
|
|
295
|
+
// Initialize the plugin
|
|
296
|
+
init(api) {
|
|
297
|
+
if (!options.enabled) return this;
|
|
298
|
+
|
|
299
|
+
uistateRef = api;
|
|
300
|
+
|
|
301
|
+
// Proxy methods for tracking
|
|
302
|
+
if (options.trackMethodCalls) {
|
|
303
|
+
methodsToTrack.forEach(methodName => {
|
|
304
|
+
if (typeof api[methodName] === 'function') {
|
|
305
|
+
// Add to list of methods to check for usage
|
|
306
|
+
data.unusedMethods.add(methodName);
|
|
307
|
+
|
|
308
|
+
// Create proxy
|
|
309
|
+
api[methodName] = createMethodProxy(api, methodName);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return this;
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
// Middleware for state changes
|
|
318
|
+
middlewares: [
|
|
319
|
+
(path, value, getState) => {
|
|
320
|
+
if (options.enabled && options.trackStateChanges) {
|
|
321
|
+
const previousValue = getState(path);
|
|
322
|
+
startTimer(`stateChange.${path}`);
|
|
323
|
+
|
|
324
|
+
// Return a proxy that will track after the state change
|
|
325
|
+
return {
|
|
326
|
+
__telemetryValue: true,
|
|
327
|
+
value,
|
|
328
|
+
path,
|
|
329
|
+
previousValue,
|
|
330
|
+
finalize() {
|
|
331
|
+
const duration = endTimer(`stateChange.${path}`);
|
|
332
|
+
trackStateChange(path, value, previousValue);
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
339
|
+
],
|
|
340
|
+
|
|
341
|
+
// Lifecycle hooks
|
|
342
|
+
hooks: {
|
|
343
|
+
beforeStateChange: (path, value) => {
|
|
344
|
+
// If this is our proxy value, do nothing
|
|
345
|
+
if (value && value.__telemetryValue) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
afterStateChange: (path, value) => {
|
|
351
|
+
// If this is our proxy value, finalize it
|
|
352
|
+
if (value && value.__telemetryValue) {
|
|
353
|
+
return value.finalize();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
// Plugin methods
|
|
359
|
+
methods: {
|
|
360
|
+
// Enable or disable telemetry
|
|
361
|
+
setEnabled(enabled) {
|
|
362
|
+
options.enabled = enabled;
|
|
363
|
+
return this;
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
// Get current configuration
|
|
367
|
+
getConfig() {
|
|
368
|
+
return { ...options };
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
// Update configuration
|
|
372
|
+
configure(newConfig) {
|
|
373
|
+
Object.assign(options, newConfig);
|
|
374
|
+
return this;
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
// Reset telemetry data
|
|
378
|
+
reset() {
|
|
379
|
+
data.startTime = Date.now();
|
|
380
|
+
data.methodCalls.clear();
|
|
381
|
+
data.stateChanges.clear();
|
|
382
|
+
data.performanceMetrics.clear();
|
|
383
|
+
data.activeTimers.clear();
|
|
384
|
+
return this;
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
// Get method call statistics
|
|
388
|
+
getMethodStats() {
|
|
389
|
+
const stats = {};
|
|
390
|
+
|
|
391
|
+
data.methodCalls.forEach((calls, methodName) => {
|
|
392
|
+
// Mark method as used
|
|
393
|
+
data.unusedMethods.delete(methodName);
|
|
394
|
+
|
|
395
|
+
stats[methodName] = {
|
|
396
|
+
count: calls.length,
|
|
397
|
+
averageDuration: calls.reduce((sum, call) => sum + (call.duration || 0), 0) / calls.length,
|
|
398
|
+
lastCall: calls[calls.length - 1],
|
|
399
|
+
firstCall: calls[0]
|
|
400
|
+
};
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
return stats;
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
// Get detailed method call data
|
|
407
|
+
getMethodCalls(methodName) {
|
|
408
|
+
if (!methodName) {
|
|
409
|
+
const result = {};
|
|
410
|
+
data.methodCalls.forEach((calls, method) => {
|
|
411
|
+
result[method] = [...calls];
|
|
412
|
+
});
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return data.methodCalls.has(methodName)
|
|
417
|
+
? [...data.methodCalls.get(methodName)]
|
|
418
|
+
: [];
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
// Get state change statistics
|
|
422
|
+
getStateStats() {
|
|
423
|
+
const stats = {};
|
|
424
|
+
|
|
425
|
+
data.stateChanges.forEach((changes, path) => {
|
|
426
|
+
stats[path] = {
|
|
427
|
+
count: changes.length,
|
|
428
|
+
lastChange: changes[changes.length - 1],
|
|
429
|
+
firstChange: changes[0]
|
|
430
|
+
};
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return stats;
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
// Get detailed state change data
|
|
437
|
+
getStateChanges(path) {
|
|
438
|
+
if (!path) {
|
|
439
|
+
const result = {};
|
|
440
|
+
data.stateChanges.forEach((changes, statePath) => {
|
|
441
|
+
result[statePath] = [...changes];
|
|
442
|
+
});
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return data.stateChanges.has(path)
|
|
447
|
+
? [...data.stateChanges.get(path)]
|
|
448
|
+
: [];
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
// Get performance metrics
|
|
452
|
+
getPerformanceMetrics() {
|
|
453
|
+
const metrics = {};
|
|
454
|
+
|
|
455
|
+
data.performanceMetrics.forEach((data, label) => {
|
|
456
|
+
metrics[label] = {
|
|
457
|
+
count: data.count,
|
|
458
|
+
totalDuration: data.totalDuration,
|
|
459
|
+
averageDuration: data.totalDuration / data.count,
|
|
460
|
+
minDuration: data.minDuration,
|
|
461
|
+
maxDuration: data.maxDuration
|
|
462
|
+
};
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
return metrics;
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
// Get unused methods
|
|
469
|
+
getUnusedMethods() {
|
|
470
|
+
return [...data.unusedMethods];
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
// Get comprehensive telemetry report
|
|
474
|
+
getReport() {
|
|
475
|
+
return {
|
|
476
|
+
startTime: data.startTime,
|
|
477
|
+
duration: Date.now() - data.startTime,
|
|
478
|
+
methodStats: this.getMethodStats(),
|
|
479
|
+
stateStats: this.getStateStats(),
|
|
480
|
+
performanceMetrics: this.getPerformanceMetrics(),
|
|
481
|
+
unusedMethods: this.getUnusedMethods(),
|
|
482
|
+
config: this.getConfig(),
|
|
483
|
+
// Add detailed logs for download
|
|
484
|
+
detailedLogs: {
|
|
485
|
+
methodCalls: this.getMethodCalls(),
|
|
486
|
+
stateChanges: this.getStateChanges()
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
// Download telemetry logs as a JSON file
|
|
492
|
+
downloadLogs() {
|
|
493
|
+
const report = this.getReport();
|
|
494
|
+
const data = JSON.stringify(report, null, 2);
|
|
495
|
+
const blob = new Blob([data], { type: 'application/json' });
|
|
496
|
+
const url = URL.createObjectURL(blob);
|
|
497
|
+
|
|
498
|
+
const a = document.createElement('a');
|
|
499
|
+
a.href = url;
|
|
500
|
+
a.download = `uistate-telemetry-${new Date().toISOString().replace(/:/g, '-')}.json`;
|
|
501
|
+
document.body.appendChild(a);
|
|
502
|
+
a.click();
|
|
503
|
+
document.body.removeChild(a);
|
|
504
|
+
URL.revokeObjectURL(url);
|
|
505
|
+
|
|
506
|
+
return true;
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
// Create a visual dashboard
|
|
510
|
+
createDashboard(container = document.body) {
|
|
511
|
+
// Create dashboard element
|
|
512
|
+
const dashboard = document.createElement('div');
|
|
513
|
+
dashboard.className = 'uistate-telemetry-dashboard';
|
|
514
|
+
dashboard.style.cssText = `
|
|
515
|
+
position: fixed;
|
|
516
|
+
bottom: 10px;
|
|
517
|
+
right: 10px;
|
|
518
|
+
width: 300px;
|
|
519
|
+
max-height: 400px;
|
|
520
|
+
overflow: auto;
|
|
521
|
+
background: #fff;
|
|
522
|
+
border: 1px solid #ccc;
|
|
523
|
+
border-radius: 4px;
|
|
524
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
525
|
+
padding: 10px;
|
|
526
|
+
font-family: monospace;
|
|
527
|
+
font-size: 12px;
|
|
528
|
+
z-index: 9999;
|
|
529
|
+
`;
|
|
530
|
+
|
|
531
|
+
// Update function
|
|
532
|
+
const updateDashboard = () => {
|
|
533
|
+
const report = this.getReport();
|
|
534
|
+
|
|
535
|
+
dashboard.innerHTML = `
|
|
536
|
+
<h3 style="margin: 0 0 10px; font-size: 14px;">UIstate Telemetry</h3>
|
|
537
|
+
<div style="margin-bottom: 5px;">
|
|
538
|
+
<button id="telemetry-refresh">Refresh</button>
|
|
539
|
+
<button id="telemetry-reset">Reset</button>
|
|
540
|
+
<button id="telemetry-close">Close</button>
|
|
541
|
+
</div>
|
|
542
|
+
<div style="margin: 10px 0;">
|
|
543
|
+
<strong>Duration:</strong> ${Math.round((Date.now() - report.startTime) / 1000)}s
|
|
544
|
+
</div>
|
|
545
|
+
|
|
546
|
+
<h4 style="margin: 10px 0 5px; font-size: 13px;">Method Calls</h4>
|
|
547
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
548
|
+
<tr>
|
|
549
|
+
<th style="text-align: left; border-bottom: 1px solid #eee;">Method</th>
|
|
550
|
+
<th style="text-align: right; border-bottom: 1px solid #eee;">Count</th>
|
|
551
|
+
<th style="text-align: right; border-bottom: 1px solid #eee;">Avg Time</th>
|
|
552
|
+
</tr>
|
|
553
|
+
${Object.entries(report.methodStats)
|
|
554
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
555
|
+
.map(([method, stats]) => `
|
|
556
|
+
<tr>
|
|
557
|
+
<td style="padding: 2px 0; border-bottom: 1px solid #eee;">${method}</td>
|
|
558
|
+
<td style="text-align: right; padding: 2px 0; border-bottom: 1px solid #eee;">${stats.count}</td>
|
|
559
|
+
<td style="text-align: right; padding: 2px 0; border-bottom: 1px solid #eee;">${stats.averageDuration ? stats.averageDuration.toFixed(2) + 'ms' : 'n/a'}</td>
|
|
560
|
+
</tr>
|
|
561
|
+
`).join('')}
|
|
562
|
+
</table>
|
|
563
|
+
|
|
564
|
+
<h4 style="margin: 10px 0 5px; font-size: 13px;">State Changes</h4>
|
|
565
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
566
|
+
<tr>
|
|
567
|
+
<th style="text-align: left; border-bottom: 1px solid #eee;">Path</th>
|
|
568
|
+
<th style="text-align: right; border-bottom: 1px solid #eee;">Count</th>
|
|
569
|
+
</tr>
|
|
570
|
+
${Object.entries(report.stateStats)
|
|
571
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
572
|
+
.map(([path, stats]) => `
|
|
573
|
+
<tr>
|
|
574
|
+
<td style="padding: 2px 0; border-bottom: 1px solid #eee;">${path}</td>
|
|
575
|
+
<td style="text-align: right; padding: 2px 0; border-bottom: 1px solid #eee;">${stats.count}</td>
|
|
576
|
+
</tr>
|
|
577
|
+
`).join('')}
|
|
578
|
+
</table>
|
|
579
|
+
|
|
580
|
+
${report.unusedMethods.length > 0 ? `
|
|
581
|
+
<h4 style="margin: 10px 0 5px; font-size: 13px;">Unused Methods</h4>
|
|
582
|
+
<div style="color: #666;">
|
|
583
|
+
${report.unusedMethods.join(', ')}
|
|
584
|
+
</div>
|
|
585
|
+
` : ''}
|
|
586
|
+
`;
|
|
587
|
+
|
|
588
|
+
// Add event listeners
|
|
589
|
+
dashboard.querySelector('#telemetry-refresh').addEventListener('click', updateDashboard);
|
|
590
|
+
dashboard.querySelector('#telemetry-reset').addEventListener('click', () => {
|
|
591
|
+
this.reset();
|
|
592
|
+
updateDashboard();
|
|
593
|
+
});
|
|
594
|
+
dashboard.querySelector('#telemetry-close').addEventListener('click', () => {
|
|
595
|
+
dashboard.remove();
|
|
596
|
+
});
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// Initial update
|
|
600
|
+
updateDashboard();
|
|
601
|
+
|
|
602
|
+
// Add to container
|
|
603
|
+
container.appendChild(dashboard);
|
|
604
|
+
|
|
605
|
+
return dashboard;
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
|
|
609
|
+
// Clean up
|
|
610
|
+
destroy() {
|
|
611
|
+
// Restore original methods
|
|
612
|
+
if (uistateRef) {
|
|
613
|
+
originalMethods.forEach((originalMethod, methodName) => {
|
|
614
|
+
if (uistateRef[methodName]) {
|
|
615
|
+
uistateRef[methodName] = originalMethod;
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Clear data
|
|
621
|
+
data.methodCalls.clear();
|
|
622
|
+
data.stateChanges.clear();
|
|
623
|
+
data.performanceMetrics.clear();
|
|
624
|
+
data.activeTimers.clear();
|
|
625
|
+
data.unusedMethods.clear();
|
|
626
|
+
|
|
627
|
+
uistateRef = null;
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
return telemetryPlugin;
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
// Create a default instance with standard configuration
|
|
635
|
+
const telemetryPlugin = createTelemetryPlugin();
|
|
636
|
+
|
|
637
|
+
export default telemetryPlugin;
|
|
638
|
+
export { createTelemetryPlugin };
|