@memberjunction/core 2.128.0 → 2.129.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/dist/__tests__/mocks/TestMetadataProvider.d.ts +4 -2
- package/dist/__tests__/mocks/TestMetadataProvider.d.ts.map +1 -1
- package/dist/__tests__/mocks/TestMetadataProvider.js +9 -3
- package/dist/__tests__/mocks/TestMetadataProvider.js.map +1 -1
- package/dist/generic/RegisterForStartup.d.ts +228 -0
- package/dist/generic/RegisterForStartup.d.ts.map +1 -0
- package/dist/generic/RegisterForStartup.js +233 -0
- package/dist/generic/RegisterForStartup.js.map +1 -0
- package/dist/generic/baseEngine.d.ts +191 -8
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +360 -14
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEngineRegistry.d.ts +247 -0
- package/dist/generic/baseEngineRegistry.d.ts.map +1 -0
- package/dist/generic/baseEngineRegistry.js +470 -0
- package/dist/generic/baseEngineRegistry.js.map +1 -0
- package/dist/generic/entityInfo.d.ts +50 -0
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +56 -0
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/graphqlTypeNames.d.ts +90 -0
- package/dist/generic/graphqlTypeNames.d.ts.map +1 -0
- package/dist/generic/graphqlTypeNames.js +119 -0
- package/dist/generic/graphqlTypeNames.js.map +1 -0
- package/dist/generic/interfaces.d.ts +234 -3
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/localCacheManager.d.ts +388 -0
- package/dist/generic/localCacheManager.d.ts.map +1 -0
- package/dist/generic/localCacheManager.js +856 -0
- package/dist/generic/localCacheManager.js.map +1 -0
- package/dist/generic/providerBase.d.ts +227 -13
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +751 -6
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/queryInfo.d.ts +18 -0
- package/dist/generic/queryInfo.d.ts.map +1 -1
- package/dist/generic/queryInfo.js +18 -0
- package/dist/generic/queryInfo.js.map +1 -1
- package/dist/generic/queryInfoInterfaces.d.ts +17 -0
- package/dist/generic/queryInfoInterfaces.d.ts.map +1 -1
- package/dist/generic/runQuery.d.ts +30 -0
- package/dist/generic/runQuery.d.ts.map +1 -1
- package/dist/generic/runQuery.js +13 -0
- package/dist/generic/runQuery.js.map +1 -1
- package/dist/generic/telemetryManager.d.ts +628 -0
- package/dist/generic/telemetryManager.d.ts.map +1 -0
- package/dist/generic/telemetryManager.js +1011 -0
- package/dist/generic/telemetryManager.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/views/runView.d.ts +25 -0
- package/dist/views/runView.d.ts.map +1 -1
- package/dist/views/runView.js +4 -5
- package/dist/views/runView.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TelemetryManager - Lightweight instrumentation system for tracking operation performance,
|
|
4
|
+
* detecting patterns, and surfacing optimization opportunities.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Session-level event tracking for RunView, RunQuery, Engine, AI, Cache operations
|
|
8
|
+
* - Pattern detection for identifying duplicate calls and optimization opportunities
|
|
9
|
+
* - Pluggable analyzer system for custom analysis rules
|
|
10
|
+
* - WarningManager integration for debounced console output
|
|
11
|
+
* - Configurable via local storage for per-client settings
|
|
12
|
+
* - Strongly-typed parameter interfaces for each telemetry category
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const tm = TelemetryManager.Instance;
|
|
17
|
+
*
|
|
18
|
+
* // Enable telemetry
|
|
19
|
+
* tm.SetEnabled(true);
|
|
20
|
+
*
|
|
21
|
+
* // Track a RunView operation with typed params
|
|
22
|
+
* const eventId = tm.StartEvent('RunView', 'ProviderBase.RunView', {
|
|
23
|
+
* EntityName: 'Users',
|
|
24
|
+
* ExtraFilter: 'IsActive = 1',
|
|
25
|
+
* ResultType: 'entity_object'
|
|
26
|
+
* });
|
|
27
|
+
* // ... perform operation
|
|
28
|
+
* tm.EndEvent(eventId, { cacheHit: false, resultCount: 50 });
|
|
29
|
+
*
|
|
30
|
+
* // Get patterns for analysis
|
|
31
|
+
* const patterns = tm.GetPatterns({ category: 'RunView', minCount: 2 });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.TelemetryManager = exports.isNetworkParams = exports.isCacheParams = exports.isAIParams = exports.isEngineParams = exports.isBatchRunQueryParams = exports.isSingleRunQueryParams = exports.isSingleRunViewParams = exports.isBatchRunViewParams = exports.TelemetryLevelValue = void 0;
|
|
36
|
+
const global_1 = require("@memberjunction/global");
|
|
37
|
+
/**
|
|
38
|
+
* Numeric mapping for level comparisons
|
|
39
|
+
*/
|
|
40
|
+
exports.TelemetryLevelValue = {
|
|
41
|
+
'off': 0,
|
|
42
|
+
'basic': 1, // Timing only (operation, elapsed ms)
|
|
43
|
+
'standard': 2, // Timing + key params (entity, filter, etc.)
|
|
44
|
+
'verbose': 3, // + stack traces (cleaned)
|
|
45
|
+
'debug': 4 // + memory snapshots before/after
|
|
46
|
+
};
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// TYPE GUARDS
|
|
49
|
+
// ============================================================================
|
|
50
|
+
/**
|
|
51
|
+
* Type guard to check if params represent a batch RunViews operation
|
|
52
|
+
*/
|
|
53
|
+
function isBatchRunViewParams(params) {
|
|
54
|
+
return typeof params === 'object' &&
|
|
55
|
+
params !== null &&
|
|
56
|
+
'Entities' in params &&
|
|
57
|
+
Array.isArray(params.Entities);
|
|
58
|
+
}
|
|
59
|
+
exports.isBatchRunViewParams = isBatchRunViewParams;
|
|
60
|
+
/**
|
|
61
|
+
* Type guard to check if params represent a single RunView operation
|
|
62
|
+
*/
|
|
63
|
+
function isSingleRunViewParams(params) {
|
|
64
|
+
return typeof params === 'object' &&
|
|
65
|
+
params !== null &&
|
|
66
|
+
!isBatchRunViewParams(params) &&
|
|
67
|
+
('EntityName' in params || 'ViewID' in params || 'ViewName' in params);
|
|
68
|
+
}
|
|
69
|
+
exports.isSingleRunViewParams = isSingleRunViewParams;
|
|
70
|
+
/**
|
|
71
|
+
* Type guard to check if params represent a single RunQuery operation
|
|
72
|
+
*/
|
|
73
|
+
function isSingleRunQueryParams(params) {
|
|
74
|
+
return typeof params === 'object' &&
|
|
75
|
+
params !== null &&
|
|
76
|
+
!isBatchRunQueryParams(params) &&
|
|
77
|
+
('QueryID' in params || 'QueryName' in params);
|
|
78
|
+
}
|
|
79
|
+
exports.isSingleRunQueryParams = isSingleRunQueryParams;
|
|
80
|
+
/**
|
|
81
|
+
* Type guard to check if params represent a batch RunQueries operation
|
|
82
|
+
*/
|
|
83
|
+
function isBatchRunQueryParams(params) {
|
|
84
|
+
return typeof params === 'object' &&
|
|
85
|
+
params !== null &&
|
|
86
|
+
'Queries' in params &&
|
|
87
|
+
Array.isArray(params.Queries);
|
|
88
|
+
}
|
|
89
|
+
exports.isBatchRunQueryParams = isBatchRunQueryParams;
|
|
90
|
+
/**
|
|
91
|
+
* Type guard to check if params represent an Engine operation
|
|
92
|
+
*/
|
|
93
|
+
function isEngineParams(params) {
|
|
94
|
+
return typeof params === 'object' &&
|
|
95
|
+
params !== null &&
|
|
96
|
+
'engineClass' in params &&
|
|
97
|
+
'operation' in params;
|
|
98
|
+
}
|
|
99
|
+
exports.isEngineParams = isEngineParams;
|
|
100
|
+
/**
|
|
101
|
+
* Type guard to check if params represent an AI operation
|
|
102
|
+
*/
|
|
103
|
+
function isAIParams(params) {
|
|
104
|
+
return typeof params === 'object' &&
|
|
105
|
+
params !== null &&
|
|
106
|
+
('modelID' in params || 'modelName' in params || 'promptID' in params);
|
|
107
|
+
}
|
|
108
|
+
exports.isAIParams = isAIParams;
|
|
109
|
+
/**
|
|
110
|
+
* Type guard to check if params represent a Cache operation
|
|
111
|
+
*/
|
|
112
|
+
function isCacheParams(params) {
|
|
113
|
+
return typeof params === 'object' &&
|
|
114
|
+
params !== null &&
|
|
115
|
+
'cacheType' in params &&
|
|
116
|
+
'operation' in params;
|
|
117
|
+
}
|
|
118
|
+
exports.isCacheParams = isCacheParams;
|
|
119
|
+
/**
|
|
120
|
+
* Type guard to check if params represent a Network operation
|
|
121
|
+
*/
|
|
122
|
+
function isNetworkParams(params) {
|
|
123
|
+
return typeof params === 'object' &&
|
|
124
|
+
params !== null &&
|
|
125
|
+
('method' in params || 'url' in params || 'statusCode' in params);
|
|
126
|
+
}
|
|
127
|
+
exports.isNetworkParams = isNetworkParams;
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// BUILT-IN ANALYZERS
|
|
130
|
+
// ============================================================================
|
|
131
|
+
/**
|
|
132
|
+
* Detects when RunView is called for an entity already loaded by an engine.
|
|
133
|
+
* Suggests using the engine's cached data instead.
|
|
134
|
+
*
|
|
135
|
+
* Note: Skips RunView calls marked with _fromEngine=true, as these are
|
|
136
|
+
* engine-initiated calls (e.g., BaseEngine loading its config entities).
|
|
137
|
+
*/
|
|
138
|
+
class EngineOverlapAnalyzer {
|
|
139
|
+
constructor() {
|
|
140
|
+
this.name = 'EngineOverlapAnalyzer';
|
|
141
|
+
this.category = 'Optimization';
|
|
142
|
+
}
|
|
143
|
+
analyze(event, context) {
|
|
144
|
+
if (event.category !== 'RunView')
|
|
145
|
+
return null;
|
|
146
|
+
const params = event.params;
|
|
147
|
+
// Skip engine-initiated RunView calls to avoid false positives
|
|
148
|
+
if (params._fromEngine)
|
|
149
|
+
return null;
|
|
150
|
+
// Only check single RunView operations, not batches
|
|
151
|
+
if (!isSingleRunViewParams(params))
|
|
152
|
+
return null;
|
|
153
|
+
const entityName = params.EntityName;
|
|
154
|
+
if (!entityName)
|
|
155
|
+
return null;
|
|
156
|
+
const loadedEntities = context.getEngineLoadedEntities();
|
|
157
|
+
const engines = loadedEntities.get(entityName);
|
|
158
|
+
if (engines && engines.length > 0) {
|
|
159
|
+
return {
|
|
160
|
+
id: `engine-overlap-${event.id}`,
|
|
161
|
+
severity: 'optimization',
|
|
162
|
+
analyzerName: this.name,
|
|
163
|
+
category: this.category,
|
|
164
|
+
title: 'Entity Already in Engine',
|
|
165
|
+
entityName,
|
|
166
|
+
message: `RunView for "${entityName}" called, but this entity is already loaded by: ${engines.join(', ')}`,
|
|
167
|
+
suggestion: `Consider using ${engines[0]} cached data instead of a separate RunView call`,
|
|
168
|
+
relatedEventIds: [event.id],
|
|
169
|
+
timestamp: Date.now()
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Detects when the same entity is queried multiple times with different filters.
|
|
177
|
+
* Suggests creating an engine to centralize access.
|
|
178
|
+
*/
|
|
179
|
+
class SameEntityMultipleCallsAnalyzer {
|
|
180
|
+
constructor() {
|
|
181
|
+
this.name = 'SameEntityMultipleCallsAnalyzer';
|
|
182
|
+
this.category = 'Optimization';
|
|
183
|
+
}
|
|
184
|
+
analyze(event, context) {
|
|
185
|
+
if (event.category !== 'RunView')
|
|
186
|
+
return null;
|
|
187
|
+
const params = event.params;
|
|
188
|
+
if (!isSingleRunViewParams(params))
|
|
189
|
+
return null;
|
|
190
|
+
const entityName = params.EntityName;
|
|
191
|
+
if (!entityName)
|
|
192
|
+
return null;
|
|
193
|
+
// Count distinct RunViews for same entity in recent events
|
|
194
|
+
const entityEvents = context.recentEvents.filter(e => {
|
|
195
|
+
if (e.category !== 'RunView')
|
|
196
|
+
return false;
|
|
197
|
+
const p = e.params;
|
|
198
|
+
return isSingleRunViewParams(p) && p.EntityName === entityName;
|
|
199
|
+
});
|
|
200
|
+
// Get unique fingerprints (different filter/orderBy combinations)
|
|
201
|
+
const uniqueFingerprints = new Set(entityEvents.map(e => e.fingerprint));
|
|
202
|
+
if (uniqueFingerprints.size >= 3) {
|
|
203
|
+
return {
|
|
204
|
+
id: `multi-call-${entityName}-${Date.now()}`,
|
|
205
|
+
severity: 'optimization',
|
|
206
|
+
analyzerName: this.name,
|
|
207
|
+
category: this.category,
|
|
208
|
+
title: 'Multiple Queries for Same Entity',
|
|
209
|
+
entityName,
|
|
210
|
+
message: `Entity "${entityName}" queried ${entityEvents.length} times with ${uniqueFingerprints.size} different filter combinations`,
|
|
211
|
+
suggestion: `Consider creating a dedicated engine to load and cache ${entityName} data centrally`,
|
|
212
|
+
relatedEventIds: entityEvents.map(e => e.id),
|
|
213
|
+
metadata: {
|
|
214
|
+
totalCalls: entityEvents.length,
|
|
215
|
+
uniqueVariations: uniqueFingerprints.size
|
|
216
|
+
},
|
|
217
|
+
timestamp: Date.now()
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Detects sequential RunView calls that could be batched with RunViews.
|
|
225
|
+
*/
|
|
226
|
+
class ParallelizationOpportunityAnalyzer {
|
|
227
|
+
constructor() {
|
|
228
|
+
this.name = 'ParallelizationOpportunityAnalyzer';
|
|
229
|
+
this.category = 'Performance';
|
|
230
|
+
this.SEQUENCE_THRESHOLD_MS = 100;
|
|
231
|
+
}
|
|
232
|
+
analyze(event, context) {
|
|
233
|
+
if (event.category !== 'RunView')
|
|
234
|
+
return null;
|
|
235
|
+
// Find RunView events that completed just before this one started
|
|
236
|
+
const recentSequential = context.recentEvents.filter(e => {
|
|
237
|
+
if (e.category !== 'RunView')
|
|
238
|
+
return false;
|
|
239
|
+
if (e.id === event.id)
|
|
240
|
+
return false;
|
|
241
|
+
if (!e.endTime)
|
|
242
|
+
return false;
|
|
243
|
+
// Check if previous event ended shortly before this one started
|
|
244
|
+
const gap = event.startTime - e.endTime;
|
|
245
|
+
return gap >= 0 && gap < this.SEQUENCE_THRESHOLD_MS;
|
|
246
|
+
});
|
|
247
|
+
if (recentSequential.length >= 2) {
|
|
248
|
+
const allEvents = [...recentSequential, event];
|
|
249
|
+
const entities = allEvents.map(e => {
|
|
250
|
+
const p = e.params;
|
|
251
|
+
return isSingleRunViewParams(p) ? p.EntityName : 'batch';
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
id: `parallel-${event.id}`,
|
|
255
|
+
severity: 'optimization',
|
|
256
|
+
analyzerName: this.name,
|
|
257
|
+
category: this.category,
|
|
258
|
+
title: 'Sequential Queries Could Be Parallelized',
|
|
259
|
+
message: `${allEvents.length} RunView calls executed sequentially`,
|
|
260
|
+
suggestion: `Use RunViews (batch) to execute these queries in parallel for better performance`,
|
|
261
|
+
relatedEventIds: allEvents.map(e => e.id),
|
|
262
|
+
metadata: { entities },
|
|
263
|
+
timestamp: Date.now()
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Detects exact duplicate RunView calls (same fingerprint).
|
|
271
|
+
*/
|
|
272
|
+
class DuplicateRunViewAnalyzer {
|
|
273
|
+
constructor() {
|
|
274
|
+
this.name = 'DuplicateRunViewAnalyzer';
|
|
275
|
+
this.category = 'Redundancy';
|
|
276
|
+
}
|
|
277
|
+
analyze(event, context) {
|
|
278
|
+
if (event.category !== 'RunView')
|
|
279
|
+
return null;
|
|
280
|
+
const pattern = context.patterns.get(event.fingerprint);
|
|
281
|
+
if (pattern && pattern.count >= 2) {
|
|
282
|
+
const params = event.params;
|
|
283
|
+
// Handle both single RunView (EntityName) and batch RunViews (Entities array)
|
|
284
|
+
const entityName = isSingleRunViewParams(params)
|
|
285
|
+
? params.EntityName || 'Unknown'
|
|
286
|
+
: isBatchRunViewParams(params)
|
|
287
|
+
? params.Entities.join(', ')
|
|
288
|
+
: 'Unknown';
|
|
289
|
+
return {
|
|
290
|
+
id: `duplicate-${event.fingerprint}-${Date.now()}`,
|
|
291
|
+
severity: 'warning',
|
|
292
|
+
analyzerName: this.name,
|
|
293
|
+
category: this.category,
|
|
294
|
+
title: 'Duplicate RunView Detected',
|
|
295
|
+
entityName,
|
|
296
|
+
message: `Identical RunView (${entityName}, same filter/orderBy) called ${pattern.count} times`,
|
|
297
|
+
suggestion: `Cache the result or use an engine to avoid redundant database queries`,
|
|
298
|
+
relatedEventIds: [event.id],
|
|
299
|
+
metadata: {
|
|
300
|
+
callCount: pattern.count,
|
|
301
|
+
totalTimeMs: pattern.totalElapsedMs,
|
|
302
|
+
avgTimeMs: pattern.avgElapsedMs
|
|
303
|
+
},
|
|
304
|
+
timestamp: Date.now()
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// ============================================================================
|
|
311
|
+
// CONSTANTS
|
|
312
|
+
// ============================================================================
|
|
313
|
+
const TELEMETRY_SETTINGS_KEY = '__MJ_TELEMETRY_SETTINGS__';
|
|
314
|
+
const DEFAULT_SETTINGS = {
|
|
315
|
+
enabled: false,
|
|
316
|
+
level: 'standard',
|
|
317
|
+
categoryOverrides: {},
|
|
318
|
+
autoTrim: {
|
|
319
|
+
enabled: true,
|
|
320
|
+
maxEvents: 10000,
|
|
321
|
+
maxAgeMs: 30 * 60 * 1000 // 30 minutes
|
|
322
|
+
},
|
|
323
|
+
duplicateDetection: {
|
|
324
|
+
enabled: true,
|
|
325
|
+
windowMs: 60 * 1000 // 1 minute
|
|
326
|
+
},
|
|
327
|
+
analyzers: {
|
|
328
|
+
enabled: true,
|
|
329
|
+
dedupeWindowMs: 30000 // 30 seconds
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
// ============================================================================
|
|
333
|
+
// TELEMETRY MANAGER
|
|
334
|
+
// ============================================================================
|
|
335
|
+
/**
|
|
336
|
+
* Singleton manager for telemetry tracking and analysis.
|
|
337
|
+
*
|
|
338
|
+
* Provides:
|
|
339
|
+
* - Event recording for various operation types with strongly-typed parameters
|
|
340
|
+
* - Pattern detection for identifying optimization opportunities
|
|
341
|
+
* - Pluggable analyzer system for custom rules
|
|
342
|
+
* - Integration with WarningManager for console output
|
|
343
|
+
*/
|
|
344
|
+
class TelemetryManager extends global_1.BaseSingleton {
|
|
345
|
+
/**
|
|
346
|
+
* Returns the singleton instance of TelemetryManager
|
|
347
|
+
*/
|
|
348
|
+
static get Instance() {
|
|
349
|
+
return super.getInstance();
|
|
350
|
+
}
|
|
351
|
+
constructor() {
|
|
352
|
+
super();
|
|
353
|
+
this._events = [];
|
|
354
|
+
this._patterns = new Map();
|
|
355
|
+
this._activeEvents = new Map();
|
|
356
|
+
// Analyzer infrastructure
|
|
357
|
+
this._analyzers = [];
|
|
358
|
+
this._insights = [];
|
|
359
|
+
this._insightDedupeWindow = new Map();
|
|
360
|
+
this._settings = this.loadSettings();
|
|
361
|
+
this.registerBuiltInAnalyzers();
|
|
362
|
+
}
|
|
363
|
+
// ========== CONFIGURATION ==========
|
|
364
|
+
/**
|
|
365
|
+
* Get a copy of current settings
|
|
366
|
+
*/
|
|
367
|
+
get Settings() {
|
|
368
|
+
return { ...this._settings };
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Update telemetry settings
|
|
372
|
+
*/
|
|
373
|
+
UpdateSettings(settings) {
|
|
374
|
+
this._settings = { ...this._settings, ...settings };
|
|
375
|
+
this.saveSettings();
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Check if telemetry is globally enabled
|
|
379
|
+
*/
|
|
380
|
+
get IsEnabled() {
|
|
381
|
+
return this._settings.enabled;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Enable or disable telemetry globally
|
|
385
|
+
*/
|
|
386
|
+
SetEnabled(enabled) {
|
|
387
|
+
this._settings.enabled = enabled;
|
|
388
|
+
this.saveSettings();
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Check if a specific category is enabled
|
|
392
|
+
*/
|
|
393
|
+
IsCategoryEnabled(category) {
|
|
394
|
+
if (!this._settings.enabled)
|
|
395
|
+
return false;
|
|
396
|
+
const override = this._settings.categoryOverrides[category];
|
|
397
|
+
return override?.enabled ?? true;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Get the telemetry level for a specific category
|
|
401
|
+
*/
|
|
402
|
+
GetLevelForCategory(category) {
|
|
403
|
+
const override = this._settings.categoryOverrides[category];
|
|
404
|
+
return override?.level ?? this._settings.level;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Get the numeric value for a telemetry level
|
|
408
|
+
*/
|
|
409
|
+
GetLevelValue(level) {
|
|
410
|
+
return exports.TelemetryLevelValue[level];
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Start tracking an event. Returns event ID for later completion, or null if disabled.
|
|
414
|
+
* Uses strongly-typed params based on the category.
|
|
415
|
+
*/
|
|
416
|
+
StartEvent(category, operation, params, userId) {
|
|
417
|
+
if (!this.IsCategoryEnabled(category))
|
|
418
|
+
return null;
|
|
419
|
+
const level = this.GetLevelForCategory(category);
|
|
420
|
+
const levelValue = this.GetLevelValue(level);
|
|
421
|
+
const event = {
|
|
422
|
+
id: this.generateId(),
|
|
423
|
+
category,
|
|
424
|
+
operation,
|
|
425
|
+
fingerprint: this.generateFingerprint(category, params),
|
|
426
|
+
startTime: this.getTimestamp(),
|
|
427
|
+
userId,
|
|
428
|
+
params: levelValue >= exports.TelemetryLevelValue['standard'] ? params : {},
|
|
429
|
+
stackTrace: levelValue >= exports.TelemetryLevelValue['verbose'] ? this.captureStackTrace() : undefined,
|
|
430
|
+
memoryBefore: levelValue >= exports.TelemetryLevelValue['debug'] ? this.captureMemory() : undefined
|
|
431
|
+
};
|
|
432
|
+
this._activeEvents.set(event.id, event);
|
|
433
|
+
return event.id;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Complete an event that was started with StartEvent
|
|
437
|
+
* @param eventId - The event ID returned from StartEvent
|
|
438
|
+
* @param additionalParams - Optional additional parameters to merge into the event's params
|
|
439
|
+
* Useful for adding context like cacheHit, resultCount, etc.
|
|
440
|
+
*/
|
|
441
|
+
EndEvent(eventId, additionalParams) {
|
|
442
|
+
if (!eventId)
|
|
443
|
+
return null;
|
|
444
|
+
const event = this._activeEvents.get(eventId);
|
|
445
|
+
if (!event)
|
|
446
|
+
return null;
|
|
447
|
+
this._activeEvents.delete(eventId);
|
|
448
|
+
// Merge additional params if provided
|
|
449
|
+
if (additionalParams) {
|
|
450
|
+
event.params = { ...event.params, ...additionalParams };
|
|
451
|
+
}
|
|
452
|
+
event.endTime = this.getTimestamp();
|
|
453
|
+
event.elapsedMs = event.endTime - event.startTime;
|
|
454
|
+
const level = this.GetLevelForCategory(event.category);
|
|
455
|
+
if (this.GetLevelValue(level) >= exports.TelemetryLevelValue['debug']) {
|
|
456
|
+
event.memoryAfter = this.captureMemory();
|
|
457
|
+
}
|
|
458
|
+
this._events.push(event);
|
|
459
|
+
this.updatePattern(event);
|
|
460
|
+
this.runAnalyzers(event);
|
|
461
|
+
this.trimIfNeeded();
|
|
462
|
+
return event;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Convenience method for recording a completed event directly with strong typing
|
|
466
|
+
*/
|
|
467
|
+
RecordEvent(category, operation, params, elapsedMs, userId) {
|
|
468
|
+
if (!this.IsCategoryEnabled(category))
|
|
469
|
+
return;
|
|
470
|
+
const level = this.GetLevelForCategory(category);
|
|
471
|
+
const levelValue = this.GetLevelValue(level);
|
|
472
|
+
const now = this.getTimestamp();
|
|
473
|
+
const event = {
|
|
474
|
+
id: this.generateId(),
|
|
475
|
+
category,
|
|
476
|
+
operation,
|
|
477
|
+
fingerprint: this.generateFingerprint(category, params),
|
|
478
|
+
startTime: now - elapsedMs,
|
|
479
|
+
endTime: now,
|
|
480
|
+
elapsedMs,
|
|
481
|
+
userId,
|
|
482
|
+
params: levelValue >= exports.TelemetryLevelValue['standard'] ? params : {},
|
|
483
|
+
stackTrace: levelValue >= exports.TelemetryLevelValue['verbose'] ? this.captureStackTrace() : undefined
|
|
484
|
+
};
|
|
485
|
+
this._events.push(event);
|
|
486
|
+
this.updatePattern(event);
|
|
487
|
+
this.runAnalyzers(event);
|
|
488
|
+
this.trimIfNeeded();
|
|
489
|
+
}
|
|
490
|
+
// ========== PATTERN DETECTION ==========
|
|
491
|
+
updatePattern(event) {
|
|
492
|
+
if (!this._settings.duplicateDetection.enabled)
|
|
493
|
+
return;
|
|
494
|
+
let pattern = this._patterns.get(event.fingerprint);
|
|
495
|
+
const now = this.getTimestamp();
|
|
496
|
+
if (!pattern) {
|
|
497
|
+
pattern = {
|
|
498
|
+
fingerprint: event.fingerprint,
|
|
499
|
+
category: event.category,
|
|
500
|
+
operation: event.operation,
|
|
501
|
+
sampleParams: event.params,
|
|
502
|
+
count: 0,
|
|
503
|
+
totalElapsedMs: 0,
|
|
504
|
+
avgElapsedMs: 0,
|
|
505
|
+
minElapsedMs: Infinity,
|
|
506
|
+
maxElapsedMs: 0,
|
|
507
|
+
callerLocations: new Map(),
|
|
508
|
+
firstSeen: now,
|
|
509
|
+
lastSeen: now,
|
|
510
|
+
windowStartTime: now
|
|
511
|
+
};
|
|
512
|
+
this._patterns.set(event.fingerprint, pattern);
|
|
513
|
+
}
|
|
514
|
+
// Reset window if expired
|
|
515
|
+
if (now - pattern.windowStartTime > this._settings.duplicateDetection.windowMs) {
|
|
516
|
+
pattern.count = 0;
|
|
517
|
+
pattern.totalElapsedMs = 0;
|
|
518
|
+
pattern.callerLocations.clear();
|
|
519
|
+
pattern.windowStartTime = now;
|
|
520
|
+
}
|
|
521
|
+
// Update stats
|
|
522
|
+
pattern.count++;
|
|
523
|
+
pattern.lastSeen = now;
|
|
524
|
+
if (event.elapsedMs != null) {
|
|
525
|
+
pattern.totalElapsedMs += event.elapsedMs;
|
|
526
|
+
pattern.avgElapsedMs = pattern.totalElapsedMs / pattern.count;
|
|
527
|
+
pattern.minElapsedMs = Math.min(pattern.minElapsedMs, event.elapsedMs);
|
|
528
|
+
pattern.maxElapsedMs = Math.max(pattern.maxElapsedMs, event.elapsedMs);
|
|
529
|
+
}
|
|
530
|
+
// Track call sites
|
|
531
|
+
if (event.stackTrace) {
|
|
532
|
+
const location = this.extractCallerLocation(event.stackTrace);
|
|
533
|
+
pattern.callerLocations.set(location, (pattern.callerLocations.get(location) || 0) + 1);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// ========== QUERIES ==========
|
|
537
|
+
/**
|
|
538
|
+
* Get events matching the filter criteria
|
|
539
|
+
*/
|
|
540
|
+
GetEvents(filter) {
|
|
541
|
+
let results = [...this._events];
|
|
542
|
+
if (filter?.category) {
|
|
543
|
+
results = results.filter(e => e.category === filter.category);
|
|
544
|
+
}
|
|
545
|
+
if (filter?.operation) {
|
|
546
|
+
results = results.filter(e => e.operation === filter.operation);
|
|
547
|
+
}
|
|
548
|
+
if (filter?.minElapsedMs != null) {
|
|
549
|
+
results = results.filter(e => (e.elapsedMs ?? 0) >= filter.minElapsedMs);
|
|
550
|
+
}
|
|
551
|
+
if (filter?.since != null) {
|
|
552
|
+
results = results.filter(e => e.startTime >= filter.since);
|
|
553
|
+
}
|
|
554
|
+
if (filter?.limit) {
|
|
555
|
+
results = results.slice(-filter.limit);
|
|
556
|
+
}
|
|
557
|
+
return results;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Get patterns matching the filter criteria
|
|
561
|
+
*/
|
|
562
|
+
GetPatterns(filter) {
|
|
563
|
+
let results = [...this._patterns.values()];
|
|
564
|
+
if (filter?.category) {
|
|
565
|
+
results = results.filter(p => p.category === filter.category);
|
|
566
|
+
}
|
|
567
|
+
if (filter?.minCount != null) {
|
|
568
|
+
results = results.filter(p => p.count >= filter.minCount);
|
|
569
|
+
}
|
|
570
|
+
const sortBy = filter?.sortBy ?? 'count';
|
|
571
|
+
results.sort((a, b) => {
|
|
572
|
+
switch (sortBy) {
|
|
573
|
+
case 'count': return b.count - a.count;
|
|
574
|
+
case 'totalTime': return b.totalElapsedMs - a.totalElapsedMs;
|
|
575
|
+
case 'avgTime': return b.avgElapsedMs - a.avgElapsedMs;
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
return results;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Get patterns with duplicate calls (count >= minCount)
|
|
582
|
+
*/
|
|
583
|
+
GetDuplicates(minCount = 2) {
|
|
584
|
+
return this.GetPatterns({ minCount, sortBy: 'count' });
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Get active events that haven't completed yet
|
|
588
|
+
*/
|
|
589
|
+
GetActiveEvents() {
|
|
590
|
+
return [...this._activeEvents.values()];
|
|
591
|
+
}
|
|
592
|
+
// ========== ANALYZER SYSTEM ==========
|
|
593
|
+
registerBuiltInAnalyzers() {
|
|
594
|
+
this.RegisterAnalyzer(new EngineOverlapAnalyzer());
|
|
595
|
+
this.RegisterAnalyzer(new SameEntityMultipleCallsAnalyzer());
|
|
596
|
+
this.RegisterAnalyzer(new ParallelizationOpportunityAnalyzer());
|
|
597
|
+
this.RegisterAnalyzer(new DuplicateRunViewAnalyzer());
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Register a custom analyzer
|
|
601
|
+
*/
|
|
602
|
+
RegisterAnalyzer(analyzer) {
|
|
603
|
+
this._analyzers.push(analyzer);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Unregister an analyzer by name
|
|
607
|
+
*/
|
|
608
|
+
UnregisterAnalyzer(name) {
|
|
609
|
+
this._analyzers = this._analyzers.filter(a => a.name !== name);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Get all registered analyzers
|
|
613
|
+
*/
|
|
614
|
+
GetAnalyzers() {
|
|
615
|
+
return [...this._analyzers];
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Get insights matching the filter criteria
|
|
619
|
+
*/
|
|
620
|
+
GetInsights(filter) {
|
|
621
|
+
let results = [...this._insights];
|
|
622
|
+
if (filter?.severity) {
|
|
623
|
+
results = results.filter(i => i.severity === filter.severity);
|
|
624
|
+
}
|
|
625
|
+
if (filter?.category) {
|
|
626
|
+
results = results.filter(i => i.category === filter.category);
|
|
627
|
+
}
|
|
628
|
+
if (filter?.entityName) {
|
|
629
|
+
results = results.filter(i => i.entityName === filter.entityName);
|
|
630
|
+
}
|
|
631
|
+
if (filter?.limit) {
|
|
632
|
+
results = results.slice(-filter.limit);
|
|
633
|
+
}
|
|
634
|
+
return results;
|
|
635
|
+
}
|
|
636
|
+
runAnalyzers(event) {
|
|
637
|
+
if (!this._settings.analyzers.enabled)
|
|
638
|
+
return;
|
|
639
|
+
const context = this.buildAnalyzerContext();
|
|
640
|
+
for (const analyzer of this._analyzers) {
|
|
641
|
+
try {
|
|
642
|
+
const insight = analyzer.analyze(event, context);
|
|
643
|
+
if (insight && this.shouldEmitInsight(insight)) {
|
|
644
|
+
this._insights.push(insight);
|
|
645
|
+
this.emitInsightWarning(insight);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
// Don't let analyzer errors break telemetry
|
|
650
|
+
console.warn(`Telemetry analyzer ${analyzer.name} error:`, error);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
buildAnalyzerContext() {
|
|
655
|
+
return {
|
|
656
|
+
recentEvents: this._events.slice(-1000),
|
|
657
|
+
patterns: this._patterns,
|
|
658
|
+
getEngineLoadedEntities: () => {
|
|
659
|
+
// Integration with BaseEngineRegistry
|
|
660
|
+
const g = (0, global_1.GetGlobalObjectStore)();
|
|
661
|
+
if (g && g.__MJ_ENGINE_REGISTRY__) {
|
|
662
|
+
return g.__MJ_ENGINE_REGISTRY__.GetEntityLoadTracking?.() || new Map();
|
|
663
|
+
}
|
|
664
|
+
return new Map();
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
shouldEmitInsight(insight) {
|
|
669
|
+
// Dedupe similar insights within a time window
|
|
670
|
+
const dedupeKey = `${insight.analyzerName}:${insight.entityName || ''}:${insight.title}`;
|
|
671
|
+
const lastEmit = this._insightDedupeWindow.get(dedupeKey);
|
|
672
|
+
const now = Date.now();
|
|
673
|
+
if (lastEmit && now - lastEmit < this._settings.analyzers.dedupeWindowMs) {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
this._insightDedupeWindow.set(dedupeKey, now);
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
emitInsightWarning(insight) {
|
|
680
|
+
// Emit through WarningManager for console output
|
|
681
|
+
this.recordTelemetryInsightToWarningManager(insight);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Record a telemetry insight to the warning manager for debounced output
|
|
685
|
+
*/
|
|
686
|
+
recordTelemetryInsightToWarningManager(insight) {
|
|
687
|
+
const config = global_1.WarningManager.Instance.GetConfig();
|
|
688
|
+
if (config.DisableWarnings)
|
|
689
|
+
return;
|
|
690
|
+
const severityIcon = {
|
|
691
|
+
'info': 'ℹ️',
|
|
692
|
+
'warning': '⚠️',
|
|
693
|
+
'optimization': '💡'
|
|
694
|
+
}[insight.severity];
|
|
695
|
+
const message = `${severityIcon} [Telemetry/${insight.category}] ${insight.title}\n` +
|
|
696
|
+
` ${insight.message}\n` +
|
|
697
|
+
` 💡 ${insight.suggestion}`;
|
|
698
|
+
// Use console.info for telemetry insights to distinguish from warnings
|
|
699
|
+
console.info(message);
|
|
700
|
+
}
|
|
701
|
+
// ========== FINGERPRINT GENERATION WITH TYPE GUARDS ==========
|
|
702
|
+
generateId() {
|
|
703
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Generate a fingerprint for duplicate detection using type guards
|
|
707
|
+
*/
|
|
708
|
+
generateFingerprint(category, params) {
|
|
709
|
+
let keyParams;
|
|
710
|
+
switch (category) {
|
|
711
|
+
case 'RunView':
|
|
712
|
+
keyParams = this.generateRunViewFingerprint(params);
|
|
713
|
+
break;
|
|
714
|
+
case 'RunQuery':
|
|
715
|
+
keyParams = this.generateRunQueryFingerprint(params);
|
|
716
|
+
break;
|
|
717
|
+
case 'Engine':
|
|
718
|
+
keyParams = this.generateEngineFingerprint(params);
|
|
719
|
+
break;
|
|
720
|
+
case 'AI':
|
|
721
|
+
keyParams = this.generateAIFingerprint(params);
|
|
722
|
+
break;
|
|
723
|
+
case 'Cache':
|
|
724
|
+
keyParams = this.generateCacheFingerprint(params);
|
|
725
|
+
break;
|
|
726
|
+
case 'Network':
|
|
727
|
+
keyParams = this.generateNetworkFingerprint(params);
|
|
728
|
+
break;
|
|
729
|
+
default:
|
|
730
|
+
keyParams = params;
|
|
731
|
+
}
|
|
732
|
+
return `${category}:${JSON.stringify(keyParams)}`;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Generate fingerprint for RunView operations
|
|
736
|
+
*/
|
|
737
|
+
generateRunViewFingerprint(params) {
|
|
738
|
+
if (isBatchRunViewParams(params)) {
|
|
739
|
+
// Batch operation - create fingerprint from sorted entity list
|
|
740
|
+
const sortedEntities = [...params.Entities]
|
|
741
|
+
.map(e => e?.toLowerCase().trim())
|
|
742
|
+
.filter(Boolean)
|
|
743
|
+
.sort();
|
|
744
|
+
// Use a hash for long entity lists to keep fingerprint manageable
|
|
745
|
+
const entityKey = sortedEntities.length > 5
|
|
746
|
+
? this.simpleHash(sortedEntities.join('|'))
|
|
747
|
+
: sortedEntities.join('|');
|
|
748
|
+
return {
|
|
749
|
+
batch: true,
|
|
750
|
+
batchSize: params.BatchSize,
|
|
751
|
+
entities: entityKey
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
// Single operation
|
|
756
|
+
return {
|
|
757
|
+
entity: params.EntityName?.toLowerCase().trim(),
|
|
758
|
+
filter: params.ExtraFilter?.toLowerCase().trim(),
|
|
759
|
+
orderBy: params.OrderBy?.toLowerCase().trim(),
|
|
760
|
+
resultType: params.ResultType
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Generate fingerprint for RunQuery operations
|
|
766
|
+
*/
|
|
767
|
+
generateRunQueryFingerprint(params) {
|
|
768
|
+
if (isBatchRunQueryParams(params)) {
|
|
769
|
+
// Batch operation - create fingerprint from sorted query list
|
|
770
|
+
const sortedQueries = [...params.Queries]
|
|
771
|
+
.map(q => q?.toLowerCase().trim())
|
|
772
|
+
.filter(Boolean)
|
|
773
|
+
.sort();
|
|
774
|
+
const queryKey = sortedQueries.length > 5
|
|
775
|
+
? this.simpleHash(sortedQueries.join('|'))
|
|
776
|
+
: sortedQueries.join('|');
|
|
777
|
+
return {
|
|
778
|
+
batch: true,
|
|
779
|
+
batchSize: params.BatchSize,
|
|
780
|
+
queries: queryKey
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
return {
|
|
785
|
+
queryId: params.QueryID,
|
|
786
|
+
queryName: params.QueryName?.toLowerCase().trim(),
|
|
787
|
+
categoryPath: params.CategoryPath?.toLowerCase().trim()
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Generate fingerprint for Engine operations
|
|
793
|
+
*/
|
|
794
|
+
generateEngineFingerprint(params) {
|
|
795
|
+
return {
|
|
796
|
+
engine: params.engineClass,
|
|
797
|
+
operation: params.operation
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Generate fingerprint for AI operations
|
|
802
|
+
*/
|
|
803
|
+
generateAIFingerprint(params) {
|
|
804
|
+
return {
|
|
805
|
+
modelId: params.modelID,
|
|
806
|
+
modelName: params.modelName?.toLowerCase().trim(),
|
|
807
|
+
promptId: params.promptID,
|
|
808
|
+
promptName: params.promptName?.toLowerCase().trim(),
|
|
809
|
+
operationType: params.operationType
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Generate fingerprint for Cache operations
|
|
814
|
+
*/
|
|
815
|
+
generateCacheFingerprint(params) {
|
|
816
|
+
return {
|
|
817
|
+
cacheType: params.cacheType,
|
|
818
|
+
operation: params.operation,
|
|
819
|
+
entityName: params.entityName?.toLowerCase().trim(),
|
|
820
|
+
fingerprint: params.fingerprint
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Generate fingerprint for Network operations
|
|
825
|
+
*/
|
|
826
|
+
generateNetworkFingerprint(params) {
|
|
827
|
+
return {
|
|
828
|
+
method: params.method,
|
|
829
|
+
url: params.url
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Simple hash function for creating short fingerprints from long strings.
|
|
834
|
+
* Not cryptographic, just for deduplication purposes.
|
|
835
|
+
*/
|
|
836
|
+
simpleHash(str) {
|
|
837
|
+
let hash = 0;
|
|
838
|
+
for (let i = 0; i < str.length; i++) {
|
|
839
|
+
const char = str.charCodeAt(i);
|
|
840
|
+
hash = ((hash << 5) - hash) + char;
|
|
841
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
842
|
+
}
|
|
843
|
+
// Convert to hex and ensure positive
|
|
844
|
+
return (hash >>> 0).toString(16);
|
|
845
|
+
}
|
|
846
|
+
captureStackTrace() {
|
|
847
|
+
const stack = new Error().stack || '';
|
|
848
|
+
return this.cleanStackTrace(stack);
|
|
849
|
+
}
|
|
850
|
+
cleanStackTrace(stack) {
|
|
851
|
+
const lines = stack.split('\n');
|
|
852
|
+
return lines
|
|
853
|
+
.filter(line => {
|
|
854
|
+
// Filter out noise
|
|
855
|
+
if (line.includes('TelemetryManager'))
|
|
856
|
+
return false;
|
|
857
|
+
if (line.includes('node_modules'))
|
|
858
|
+
return false;
|
|
859
|
+
if (line.includes('webpack'))
|
|
860
|
+
return false;
|
|
861
|
+
if (line.includes('zone.js'))
|
|
862
|
+
return false;
|
|
863
|
+
if (line.includes('<anonymous>'))
|
|
864
|
+
return false;
|
|
865
|
+
return true;
|
|
866
|
+
})
|
|
867
|
+
.slice(0, 10) // Limit depth
|
|
868
|
+
.join('\n');
|
|
869
|
+
}
|
|
870
|
+
extractCallerLocation(stack) {
|
|
871
|
+
const lines = stack.split('\n');
|
|
872
|
+
// Return first meaningful line
|
|
873
|
+
return lines[0] || 'unknown';
|
|
874
|
+
}
|
|
875
|
+
captureMemory() {
|
|
876
|
+
// Node.js
|
|
877
|
+
if (typeof process !== 'undefined' && process.memoryUsage) {
|
|
878
|
+
const mem = process.memoryUsage();
|
|
879
|
+
return {
|
|
880
|
+
heapUsed: mem.heapUsed,
|
|
881
|
+
heapTotal: mem.heapTotal,
|
|
882
|
+
timestamp: Date.now()
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
// Browser (Chrome only)
|
|
886
|
+
if (typeof performance !== 'undefined') {
|
|
887
|
+
const perfWithMemory = performance;
|
|
888
|
+
if (perfWithMemory.memory) {
|
|
889
|
+
return {
|
|
890
|
+
heapUsed: perfWithMemory.memory.usedJSHeapSize,
|
|
891
|
+
heapTotal: perfWithMemory.memory.totalJSHeapSize,
|
|
892
|
+
timestamp: Date.now()
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return { heapUsed: 0, heapTotal: 0, timestamp: Date.now() };
|
|
897
|
+
}
|
|
898
|
+
getTimestamp() {
|
|
899
|
+
// Use performance.now() if available for better precision
|
|
900
|
+
if (typeof performance !== 'undefined' && performance.now) {
|
|
901
|
+
return performance.now();
|
|
902
|
+
}
|
|
903
|
+
return Date.now();
|
|
904
|
+
}
|
|
905
|
+
trimIfNeeded() {
|
|
906
|
+
if (!this._settings.autoTrim.enabled)
|
|
907
|
+
return;
|
|
908
|
+
const { maxEvents, maxAgeMs } = this._settings.autoTrim;
|
|
909
|
+
const now = this.getTimestamp();
|
|
910
|
+
// Trim by age
|
|
911
|
+
if (maxAgeMs) {
|
|
912
|
+
this._events = this._events.filter(e => now - e.startTime < maxAgeMs);
|
|
913
|
+
}
|
|
914
|
+
// Trim by count
|
|
915
|
+
if (maxEvents && this._events.length > maxEvents) {
|
|
916
|
+
this._events = this._events.slice(-maxEvents);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
loadSettings() {
|
|
920
|
+
try {
|
|
921
|
+
// Use localStorage if available (browser), otherwise use global store
|
|
922
|
+
if (typeof localStorage !== 'undefined') {
|
|
923
|
+
const stored = localStorage.getItem(TELEMETRY_SETTINGS_KEY);
|
|
924
|
+
if (stored) {
|
|
925
|
+
return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) };
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
catch {
|
|
930
|
+
// Ignore storage errors
|
|
931
|
+
}
|
|
932
|
+
return { ...DEFAULT_SETTINGS };
|
|
933
|
+
}
|
|
934
|
+
saveSettings() {
|
|
935
|
+
try {
|
|
936
|
+
if (typeof localStorage !== 'undefined') {
|
|
937
|
+
localStorage.setItem(TELEMETRY_SETTINGS_KEY, JSON.stringify(this._settings));
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
catch {
|
|
941
|
+
// Ignore storage errors
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
// ========== CLEAR / RESET ==========
|
|
945
|
+
/**
|
|
946
|
+
* Clear all recorded events and patterns
|
|
947
|
+
*/
|
|
948
|
+
Clear() {
|
|
949
|
+
this._events = [];
|
|
950
|
+
this._patterns.clear();
|
|
951
|
+
this._activeEvents.clear();
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Clear only patterns (keeps events)
|
|
955
|
+
*/
|
|
956
|
+
ClearPatterns() {
|
|
957
|
+
this._patterns.clear();
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Clear insights
|
|
961
|
+
*/
|
|
962
|
+
ClearInsights() {
|
|
963
|
+
this._insights = [];
|
|
964
|
+
this._insightDedupeWindow.clear();
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Reset everything including settings
|
|
968
|
+
*/
|
|
969
|
+
Reset() {
|
|
970
|
+
this.Clear();
|
|
971
|
+
this.ClearInsights();
|
|
972
|
+
this._settings = { ...DEFAULT_SETTINGS };
|
|
973
|
+
this.saveSettings();
|
|
974
|
+
}
|
|
975
|
+
// ========== STATISTICS ==========
|
|
976
|
+
/**
|
|
977
|
+
* Get summary statistics
|
|
978
|
+
*/
|
|
979
|
+
GetStats() {
|
|
980
|
+
const byCategory = {
|
|
981
|
+
RunView: { events: 0, totalMs: 0 },
|
|
982
|
+
RunQuery: { events: 0, totalMs: 0 },
|
|
983
|
+
Engine: { events: 0, totalMs: 0 },
|
|
984
|
+
Network: { events: 0, totalMs: 0 },
|
|
985
|
+
AI: { events: 0, totalMs: 0 },
|
|
986
|
+
Cache: { events: 0, totalMs: 0 },
|
|
987
|
+
Custom: { events: 0, totalMs: 0 }
|
|
988
|
+
};
|
|
989
|
+
for (const event of this._events) {
|
|
990
|
+
byCategory[event.category].events++;
|
|
991
|
+
byCategory[event.category].totalMs += event.elapsedMs || 0;
|
|
992
|
+
}
|
|
993
|
+
const byCategoryWithAvg = {};
|
|
994
|
+
for (const cat of Object.keys(byCategory)) {
|
|
995
|
+
const data = byCategory[cat];
|
|
996
|
+
byCategoryWithAvg[cat] = {
|
|
997
|
+
events: data.events,
|
|
998
|
+
avgMs: data.events > 0 ? data.totalMs / data.events : 0
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
return {
|
|
1002
|
+
totalEvents: this._events.length,
|
|
1003
|
+
totalPatterns: this._patterns.size,
|
|
1004
|
+
totalInsights: this._insights.length,
|
|
1005
|
+
activeEvents: this._activeEvents.size,
|
|
1006
|
+
byCategory: byCategoryWithAvg
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
exports.TelemetryManager = TelemetryManager;
|
|
1011
|
+
//# sourceMappingURL=telemetryManager.js.map
|