@memberjunction/core 2.128.0 → 2.130.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.
Files changed (58) hide show
  1. package/dist/__tests__/mocks/TestMetadataProvider.d.ts +4 -2
  2. package/dist/__tests__/mocks/TestMetadataProvider.d.ts.map +1 -1
  3. package/dist/__tests__/mocks/TestMetadataProvider.js +9 -3
  4. package/dist/__tests__/mocks/TestMetadataProvider.js.map +1 -1
  5. package/dist/generic/RegisterForStartup.d.ts +228 -0
  6. package/dist/generic/RegisterForStartup.d.ts.map +1 -0
  7. package/dist/generic/RegisterForStartup.js +233 -0
  8. package/dist/generic/RegisterForStartup.js.map +1 -0
  9. package/dist/generic/baseEngine.d.ts +191 -8
  10. package/dist/generic/baseEngine.d.ts.map +1 -1
  11. package/dist/generic/baseEngine.js +360 -14
  12. package/dist/generic/baseEngine.js.map +1 -1
  13. package/dist/generic/baseEngineRegistry.d.ts +247 -0
  14. package/dist/generic/baseEngineRegistry.d.ts.map +1 -0
  15. package/dist/generic/baseEngineRegistry.js +470 -0
  16. package/dist/generic/baseEngineRegistry.js.map +1 -0
  17. package/dist/generic/entityInfo.d.ts +50 -0
  18. package/dist/generic/entityInfo.d.ts.map +1 -1
  19. package/dist/generic/entityInfo.js +56 -0
  20. package/dist/generic/entityInfo.js.map +1 -1
  21. package/dist/generic/graphqlTypeNames.d.ts +90 -0
  22. package/dist/generic/graphqlTypeNames.d.ts.map +1 -0
  23. package/dist/generic/graphqlTypeNames.js +119 -0
  24. package/dist/generic/graphqlTypeNames.js.map +1 -0
  25. package/dist/generic/interfaces.d.ts +234 -3
  26. package/dist/generic/interfaces.d.ts.map +1 -1
  27. package/dist/generic/interfaces.js.map +1 -1
  28. package/dist/generic/localCacheManager.d.ts +388 -0
  29. package/dist/generic/localCacheManager.d.ts.map +1 -0
  30. package/dist/generic/localCacheManager.js +856 -0
  31. package/dist/generic/localCacheManager.js.map +1 -0
  32. package/dist/generic/providerBase.d.ts +227 -13
  33. package/dist/generic/providerBase.d.ts.map +1 -1
  34. package/dist/generic/providerBase.js +751 -6
  35. package/dist/generic/providerBase.js.map +1 -1
  36. package/dist/generic/queryInfo.d.ts +18 -0
  37. package/dist/generic/queryInfo.d.ts.map +1 -1
  38. package/dist/generic/queryInfo.js +18 -0
  39. package/dist/generic/queryInfo.js.map +1 -1
  40. package/dist/generic/queryInfoInterfaces.d.ts +17 -0
  41. package/dist/generic/queryInfoInterfaces.d.ts.map +1 -1
  42. package/dist/generic/runQuery.d.ts +30 -0
  43. package/dist/generic/runQuery.d.ts.map +1 -1
  44. package/dist/generic/runQuery.js +13 -0
  45. package/dist/generic/runQuery.js.map +1 -1
  46. package/dist/generic/telemetryManager.d.ts +628 -0
  47. package/dist/generic/telemetryManager.d.ts.map +1 -0
  48. package/dist/generic/telemetryManager.js +1011 -0
  49. package/dist/generic/telemetryManager.js.map +1 -0
  50. package/dist/index.d.ts +5 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +5 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/views/runView.d.ts +25 -0
  55. package/dist/views/runView.d.ts.map +1 -1
  56. package/dist/views/runView.js +4 -5
  57. package/dist/views/runView.js.map +1 -1
  58. 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