@snap-agent/analytics 0.1.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/index.mjs ADDED
@@ -0,0 +1,1143 @@
1
+ // src/storage/MemoryAnalyticsStorage.ts
2
+ var MemoryAnalyticsStorage = class {
3
+ constructor() {
4
+ this.requests = [];
5
+ this.responses = [];
6
+ this.errors = [];
7
+ }
8
+ async saveRequests(requests) {
9
+ this.requests.push(...requests);
10
+ }
11
+ async saveResponses(responses) {
12
+ this.responses.push(...responses);
13
+ }
14
+ async saveErrors(errors) {
15
+ this.errors.push(...errors);
16
+ }
17
+ async getRequests(options) {
18
+ return this.filterRecords(this.requests, options);
19
+ }
20
+ async getResponses(options) {
21
+ return this.filterRecords(this.responses, options);
22
+ }
23
+ async getErrors(options) {
24
+ return this.filterRecords(this.errors, options);
25
+ }
26
+ async getRequestCount(options) {
27
+ return this.filterRecords(this.requests, options).length;
28
+ }
29
+ async getResponseCount(options) {
30
+ return this.filterRecords(this.responses, options).length;
31
+ }
32
+ async getErrorCount(options) {
33
+ return this.filterRecords(this.errors, options).length;
34
+ }
35
+ async deleteOlderThan(date) {
36
+ const timestamp = date.getTime();
37
+ const requestsBefore = this.requests.length;
38
+ this.requests = this.requests.filter((r) => r.timestamp.getTime() >= timestamp);
39
+ const requestsDeleted = requestsBefore - this.requests.length;
40
+ const responsesBefore = this.responses.length;
41
+ this.responses = this.responses.filter((r) => r.timestamp.getTime() >= timestamp);
42
+ const responsesDeleted = responsesBefore - this.responses.length;
43
+ const errorsBefore = this.errors.length;
44
+ this.errors = this.errors.filter((e) => e.timestamp.getTime() >= timestamp);
45
+ const errorsDeleted = errorsBefore - this.errors.length;
46
+ return {
47
+ requests: requestsDeleted,
48
+ responses: responsesDeleted,
49
+ errors: errorsDeleted
50
+ };
51
+ }
52
+ async clear() {
53
+ this.requests = [];
54
+ this.responses = [];
55
+ this.errors = [];
56
+ }
57
+ async isReady() {
58
+ return true;
59
+ }
60
+ async close() {
61
+ }
62
+ /**
63
+ * Filter records based on query options
64
+ */
65
+ filterRecords(records, options) {
66
+ if (!options) {
67
+ return [...records];
68
+ }
69
+ let filtered = records;
70
+ if (options.agentId) {
71
+ filtered = filtered.filter((r) => r.agentId === options.agentId);
72
+ }
73
+ if (options.threadId) {
74
+ filtered = filtered.filter((r) => r.threadId === options.threadId);
75
+ }
76
+ if (options.userId) {
77
+ filtered = filtered.filter((r) => r.userId === options.userId);
78
+ }
79
+ if (options.startDate) {
80
+ const start = options.startDate.getTime();
81
+ filtered = filtered.filter((r) => r.timestamp.getTime() >= start);
82
+ }
83
+ if (options.endDate) {
84
+ const end = options.endDate.getTime();
85
+ filtered = filtered.filter((r) => r.timestamp.getTime() <= end);
86
+ }
87
+ if (options.offset) {
88
+ filtered = filtered.slice(options.offset);
89
+ }
90
+ if (options.limit) {
91
+ filtered = filtered.slice(0, options.limit);
92
+ }
93
+ return filtered;
94
+ }
95
+ /**
96
+ * Get current storage stats (for debugging)
97
+ */
98
+ getStats() {
99
+ return {
100
+ requests: this.requests.length,
101
+ responses: this.responses.length,
102
+ errors: this.errors.length
103
+ };
104
+ }
105
+ };
106
+
107
+ // src/storage/MongoAnalyticsStorage.ts
108
+ var MongoAnalyticsStorage = class {
109
+ constructor(config) {
110
+ this.client = null;
111
+ this.db = null;
112
+ this.requestsCollection = null;
113
+ this.responsesCollection = null;
114
+ this.errorsCollection = null;
115
+ this.connectionPromise = null;
116
+ this.config = {
117
+ uri: config.uri,
118
+ database: config.database || "snap-agent-analytics",
119
+ collectionPrefix: config.collectionPrefix || "analytics",
120
+ createIndexes: config.createIndexes !== false
121
+ };
122
+ }
123
+ /**
124
+ * Ensure connection to MongoDB
125
+ */
126
+ async ensureConnection() {
127
+ if (this.db) {
128
+ return;
129
+ }
130
+ if (this.connectionPromise) {
131
+ return this.connectionPromise;
132
+ }
133
+ this.connectionPromise = this.connect();
134
+ return this.connectionPromise;
135
+ }
136
+ async connect() {
137
+ const { MongoClient } = await import("mongodb");
138
+ this.client = new MongoClient(this.config.uri);
139
+ await this.client.connect();
140
+ this.db = this.client.db(this.config.database);
141
+ const prefix = this.config.collectionPrefix;
142
+ this.requestsCollection = this.db.collection(`${prefix}_requests`);
143
+ this.responsesCollection = this.db.collection(`${prefix}_responses`);
144
+ this.errorsCollection = this.db.collection(`${prefix}_errors`);
145
+ if (this.config.createIndexes) {
146
+ await this.createIndexes();
147
+ }
148
+ }
149
+ async createIndexes() {
150
+ if (!this.requestsCollection || !this.responsesCollection || !this.errorsCollection) {
151
+ return;
152
+ }
153
+ await this.requestsCollection.createIndexes([
154
+ { key: { agentId: 1, timestamp: -1 } },
155
+ { key: { threadId: 1, timestamp: -1 } },
156
+ { key: { userId: 1, timestamp: -1 } },
157
+ { key: { timestamp: -1 } }
158
+ ]);
159
+ await this.responsesCollection.createIndexes([
160
+ { key: { agentId: 1, timestamp: -1 } },
161
+ { key: { threadId: 1, timestamp: -1 } },
162
+ { key: { userId: 1, timestamp: -1 } },
163
+ { key: { timestamp: -1 } },
164
+ { key: { success: 1, timestamp: -1 } }
165
+ ]);
166
+ await this.errorsCollection.createIndexes([
167
+ { key: { agentId: 1, timestamp: -1 } },
168
+ { key: { errorType: 1, timestamp: -1 } },
169
+ { key: { timestamp: -1 } }
170
+ ]);
171
+ }
172
+ async saveRequests(requests) {
173
+ if (requests.length === 0) return;
174
+ await this.ensureConnection();
175
+ await this.requestsCollection.insertMany(requests);
176
+ }
177
+ async saveResponses(responses) {
178
+ if (responses.length === 0) return;
179
+ await this.ensureConnection();
180
+ await this.responsesCollection.insertMany(responses);
181
+ }
182
+ async saveErrors(errors) {
183
+ if (errors.length === 0) return;
184
+ await this.ensureConnection();
185
+ await this.errorsCollection.insertMany(errors);
186
+ }
187
+ async getRequests(options) {
188
+ await this.ensureConnection();
189
+ const query = this.buildQuery(options);
190
+ let cursor = this.requestsCollection.find(query).sort({ timestamp: -1 });
191
+ if (options?.offset) {
192
+ cursor = cursor.skip(options.offset);
193
+ }
194
+ if (options?.limit) {
195
+ cursor = cursor.limit(options.limit);
196
+ }
197
+ return cursor.toArray();
198
+ }
199
+ async getResponses(options) {
200
+ await this.ensureConnection();
201
+ const query = this.buildQuery(options);
202
+ let cursor = this.responsesCollection.find(query).sort({ timestamp: -1 });
203
+ if (options?.offset) {
204
+ cursor = cursor.skip(options.offset);
205
+ }
206
+ if (options?.limit) {
207
+ cursor = cursor.limit(options.limit);
208
+ }
209
+ return cursor.toArray();
210
+ }
211
+ async getErrors(options) {
212
+ await this.ensureConnection();
213
+ const query = this.buildQuery(options);
214
+ let cursor = this.errorsCollection.find(query).sort({ timestamp: -1 });
215
+ if (options?.offset) {
216
+ cursor = cursor.skip(options.offset);
217
+ }
218
+ if (options?.limit) {
219
+ cursor = cursor.limit(options.limit);
220
+ }
221
+ return cursor.toArray();
222
+ }
223
+ async getRequestCount(options) {
224
+ await this.ensureConnection();
225
+ const query = this.buildQuery(options);
226
+ return this.requestsCollection.countDocuments(query);
227
+ }
228
+ async getResponseCount(options) {
229
+ await this.ensureConnection();
230
+ const query = this.buildQuery(options);
231
+ return this.responsesCollection.countDocuments(query);
232
+ }
233
+ async getErrorCount(options) {
234
+ await this.ensureConnection();
235
+ const query = this.buildQuery(options);
236
+ return this.errorsCollection.countDocuments(query);
237
+ }
238
+ async deleteOlderThan(date) {
239
+ await this.ensureConnection();
240
+ const query = { timestamp: { $lt: date } };
241
+ const [requestsResult, responsesResult, errorsResult] = await Promise.all([
242
+ this.requestsCollection.deleteMany(query),
243
+ this.responsesCollection.deleteMany(query),
244
+ this.errorsCollection.deleteMany(query)
245
+ ]);
246
+ return {
247
+ requests: requestsResult.deletedCount,
248
+ responses: responsesResult.deletedCount,
249
+ errors: errorsResult.deletedCount
250
+ };
251
+ }
252
+ async clear() {
253
+ await this.ensureConnection();
254
+ await Promise.all([
255
+ this.requestsCollection.deleteMany({}),
256
+ this.responsesCollection.deleteMany({}),
257
+ this.errorsCollection.deleteMany({})
258
+ ]);
259
+ }
260
+ async isReady() {
261
+ try {
262
+ await this.ensureConnection();
263
+ await this.db.command({ ping: 1 });
264
+ return true;
265
+ } catch {
266
+ return false;
267
+ }
268
+ }
269
+ async close() {
270
+ if (this.client) {
271
+ await this.client.close();
272
+ this.client = null;
273
+ this.db = null;
274
+ this.requestsCollection = null;
275
+ this.responsesCollection = null;
276
+ this.errorsCollection = null;
277
+ this.connectionPromise = null;
278
+ }
279
+ }
280
+ /**
281
+ * Build MongoDB query from options
282
+ */
283
+ buildQuery(options) {
284
+ if (!options) {
285
+ return {};
286
+ }
287
+ const query = {};
288
+ if (options.agentId) {
289
+ query.agentId = options.agentId;
290
+ }
291
+ if (options.threadId) {
292
+ query.threadId = options.threadId;
293
+ }
294
+ if (options.userId) {
295
+ query.userId = options.userId;
296
+ }
297
+ if (options.startDate || options.endDate) {
298
+ query.timestamp = {};
299
+ if (options.startDate) {
300
+ query.timestamp.$gte = options.startDate;
301
+ }
302
+ if (options.endDate) {
303
+ query.timestamp.$lte = options.endDate;
304
+ }
305
+ }
306
+ return query;
307
+ }
308
+ /**
309
+ * Get aggregated metrics with grouping by time period
310
+ */
311
+ async getAggregatedMetrics(options) {
312
+ await this.ensureConnection();
313
+ const query = this.buildQuery(options);
314
+ const [requestCount, responseStats, errorCount] = await Promise.all([
315
+ this.requestsCollection.countDocuments(query),
316
+ this.responsesCollection.aggregate([
317
+ { $match: query },
318
+ {
319
+ $group: {
320
+ _id: null,
321
+ count: { $sum: 1 },
322
+ avgLatency: { $avg: "$timings.total" },
323
+ totalTokens: { $sum: "$tokens.totalTokens" }
324
+ }
325
+ }
326
+ ]).toArray(),
327
+ this.errorsCollection.countDocuments(query)
328
+ ]);
329
+ const stats = responseStats[0] || { count: 0, avgLatency: 0, totalTokens: 0 };
330
+ return {
331
+ totalRequests: requestCount,
332
+ totalResponses: stats.count,
333
+ totalErrors: errorCount,
334
+ avgLatency: stats.avgLatency || 0,
335
+ totalTokens: stats.totalTokens || 0,
336
+ errorRate: stats.count > 0 ? errorCount / stats.count : 0
337
+ };
338
+ }
339
+ };
340
+
341
+ // src/SnapAgentAnalytics.ts
342
+ var DEFAULT_MODEL_COSTS = {
343
+ // OpenAI
344
+ "gpt-4o": { input: 5e-3, output: 0.015 },
345
+ "gpt-4o-mini": { input: 15e-5, output: 6e-4 },
346
+ "gpt-4-turbo": { input: 0.01, output: 0.03 },
347
+ "gpt-4": { input: 0.03, output: 0.06 },
348
+ "gpt-3.5-turbo": { input: 5e-4, output: 15e-4 },
349
+ // Anthropic
350
+ "claude-3-5-sonnet-20241022": { input: 3e-3, output: 0.015 },
351
+ "claude-3-opus-20240229": { input: 0.015, output: 0.075 },
352
+ "claude-3-sonnet-20240229": { input: 3e-3, output: 0.015 },
353
+ "claude-3-haiku-20240307": { input: 25e-5, output: 125e-5 },
354
+ // Google
355
+ "gemini-2.0-flash-exp": { input: 0, output: 0 },
356
+ // Free during preview
357
+ "gemini-1.5-pro": { input: 125e-5, output: 5e-3 },
358
+ "gemini-1.5-flash": { input: 75e-6, output: 3e-4 }
359
+ };
360
+ var SnapAgentAnalytics = class {
361
+ constructor(config = {}) {
362
+ this.name = "snap-agent-analytics";
363
+ this.type = "analytics";
364
+ // In-memory caches (for fast access during aggregation)
365
+ this.requests = [];
366
+ this.responses = [];
367
+ this.errors = [];
368
+ this.threadStats = /* @__PURE__ */ new Map();
369
+ this.userSessions = /* @__PURE__ */ new Map();
370
+ // Counter for IDs
371
+ this.idCounter = 0;
372
+ this.pendingRequests = [];
373
+ this.pendingResponses = [];
374
+ this.pendingErrors = [];
375
+ this.isFlushing = false;
376
+ this.config = {
377
+ enablePerformance: config.enablePerformance !== false,
378
+ enableRAG: config.enableRAG !== false,
379
+ enableCost: config.enableCost !== false,
380
+ enableConversation: config.enableConversation !== false,
381
+ enableErrors: config.enableErrors !== false,
382
+ modelCosts: { ...DEFAULT_MODEL_COSTS, ...config.modelCosts },
383
+ embeddingCost: config.embeddingCost ?? 1e-4,
384
+ retentionDays: config.retentionDays ?? 30,
385
+ flushInterval: config.flushInterval ?? 5e3,
386
+ onMetric: config.onMetric || (() => {
387
+ }),
388
+ storage: config.storage
389
+ };
390
+ this.storage = config.storage || new MemoryAnalyticsStorage();
391
+ if (this.config.retentionDays > 0) {
392
+ setInterval(() => this.cleanup(), 60 * 60 * 1e3);
393
+ }
394
+ if (this.config.storage && this.config.flushInterval > 0) {
395
+ this.startFlushTimer();
396
+ }
397
+ }
398
+ // ============================================================================
399
+ // Basic Interface (Backwards Compatible)
400
+ // ============================================================================
401
+ /**
402
+ * Track incoming request (basic)
403
+ */
404
+ async trackRequest(data) {
405
+ await this.trackRequestExtended({
406
+ agentId: data.agentId,
407
+ threadId: data.threadId,
408
+ message: data.message,
409
+ messageLength: data.message.length,
410
+ timestamp: data.timestamp
411
+ });
412
+ }
413
+ /**
414
+ * Track response (basic)
415
+ */
416
+ async trackResponse(data) {
417
+ await this.trackResponseExtended({
418
+ agentId: data.agentId,
419
+ threadId: data.threadId,
420
+ response: data.response,
421
+ responseLength: data.response.length,
422
+ timestamp: data.timestamp,
423
+ timings: { total: data.latency },
424
+ tokens: {
425
+ promptTokens: 0,
426
+ completionTokens: data.tokensUsed || 0,
427
+ totalTokens: data.tokensUsed || 0
428
+ },
429
+ success: true
430
+ });
431
+ }
432
+ // ============================================================================
433
+ // Extended Interface
434
+ // ============================================================================
435
+ /**
436
+ * Track request with full metadata
437
+ */
438
+ async trackRequestExtended(data) {
439
+ const request = {
440
+ id: `req-${++this.idCounter}`,
441
+ agentId: data.agentId,
442
+ threadId: data.threadId,
443
+ userId: data.userId,
444
+ timestamp: data.timestamp,
445
+ messageLength: data.messageLength,
446
+ model: data.model,
447
+ provider: data.provider
448
+ };
449
+ this.requests.push(request);
450
+ this.pendingRequests.push(request);
451
+ if (this.config.enableConversation && data.threadId) {
452
+ this.updateThreadStats(data.threadId, data.agentId, data.userId, data.timestamp);
453
+ }
454
+ if (data.userId) {
455
+ this.trackUserSession(data.userId, data.timestamp);
456
+ }
457
+ this.config.onMetric({
458
+ type: "request",
459
+ timestamp: data.timestamp,
460
+ data
461
+ });
462
+ }
463
+ /**
464
+ * Track response with full metrics
465
+ */
466
+ async trackResponseExtended(data) {
467
+ let estimatedCost = 0;
468
+ if (this.config.enableCost && data.model) {
469
+ estimatedCost = this.calculateCost(
470
+ data.model,
471
+ data.tokens.promptTokens,
472
+ data.tokens.completionTokens,
473
+ data.rag?.contextTokens
474
+ );
475
+ }
476
+ const response = {
477
+ id: `res-${++this.idCounter}`,
478
+ requestId: `req-${this.idCounter}`,
479
+ agentId: data.agentId,
480
+ threadId: data.threadId,
481
+ userId: data.userId,
482
+ timestamp: data.timestamp,
483
+ responseLength: data.responseLength,
484
+ timings: data.timings,
485
+ tokens: {
486
+ ...data.tokens,
487
+ estimatedCost
488
+ },
489
+ rag: data.rag,
490
+ success: data.success,
491
+ errorType: data.errorType,
492
+ model: data.model,
493
+ provider: data.provider
494
+ };
495
+ this.responses.push(response);
496
+ this.pendingResponses.push(response);
497
+ if (this.config.enableConversation && data.threadId) {
498
+ this.updateThreadStats(data.threadId, data.agentId, data.userId, data.timestamp);
499
+ }
500
+ if (!data.success && data.errorType) {
501
+ await this.trackError({
502
+ agentId: data.agentId,
503
+ threadId: data.threadId,
504
+ timestamp: data.timestamp,
505
+ errorType: data.errorType,
506
+ errorMessage: data.errorMessage || "Unknown error",
507
+ component: "llm"
508
+ });
509
+ }
510
+ this.config.onMetric({
511
+ type: "response",
512
+ timestamp: data.timestamp,
513
+ data
514
+ });
515
+ }
516
+ /**
517
+ * Track errors
518
+ */
519
+ async trackError(data) {
520
+ if (!this.config.enableErrors) return;
521
+ const error = {
522
+ id: `err-${++this.idCounter}`,
523
+ agentId: data.agentId,
524
+ threadId: data.threadId,
525
+ timestamp: data.timestamp,
526
+ errorType: data.errorType,
527
+ errorMessage: data.errorMessage,
528
+ component: data.component
529
+ };
530
+ this.errors.push(error);
531
+ this.pendingErrors.push(error);
532
+ this.config.onMetric({
533
+ type: "error",
534
+ timestamp: data.timestamp,
535
+ data
536
+ });
537
+ }
538
+ // ============================================================================
539
+ // Metrics Retrieval
540
+ // ============================================================================
541
+ /**
542
+ * Get aggregated metrics
543
+ */
544
+ async getMetrics(options) {
545
+ const startDate = options?.startDate || new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
546
+ const endDate = options?.endDate || /* @__PURE__ */ new Date();
547
+ const filteredResponses = this.filterResponses(options?.agentId, startDate, endDate);
548
+ const filteredRequests = this.filterRequests(options?.agentId, startDate, endDate);
549
+ const filteredErrors = this.filterErrors(options?.agentId, startDate, endDate);
550
+ return {
551
+ period: { start: startDate, end: endDate },
552
+ performance: this.calculatePerformanceMetrics(filteredResponses),
553
+ rag: this.calculateRAGMetrics(filteredResponses),
554
+ cost: this.calculateCostMetrics(filteredResponses),
555
+ conversation: this.calculateConversationMetrics(filteredRequests, options?.agentId),
556
+ errors: this.calculateErrorMetrics(filteredErrors, filteredResponses.length)
557
+ };
558
+ }
559
+ /**
560
+ * Get performance metrics
561
+ */
562
+ getPerformanceMetrics(options) {
563
+ const responses = this.filterResponses(
564
+ options?.agentId,
565
+ options?.startDate,
566
+ options?.endDate
567
+ );
568
+ return this.calculatePerformanceMetrics(responses);
569
+ }
570
+ /**
571
+ * Get RAG metrics
572
+ */
573
+ getRAGMetrics(options) {
574
+ const responses = this.filterResponses(
575
+ options?.agentId,
576
+ options?.startDate,
577
+ options?.endDate
578
+ );
579
+ return this.calculateRAGMetrics(responses);
580
+ }
581
+ /**
582
+ * Get cost metrics
583
+ */
584
+ getCostMetrics(options) {
585
+ const responses = this.filterResponses(
586
+ options?.agentId,
587
+ options?.startDate,
588
+ options?.endDate
589
+ );
590
+ return this.calculateCostMetrics(responses);
591
+ }
592
+ /**
593
+ * Get conversation metrics
594
+ */
595
+ getConversationMetrics(options) {
596
+ const requests = this.filterRequests(
597
+ options?.agentId,
598
+ options?.startDate,
599
+ options?.endDate
600
+ );
601
+ return this.calculateConversationMetrics(requests, options?.agentId);
602
+ }
603
+ /**
604
+ * Get error metrics
605
+ */
606
+ getErrorMetrics(options) {
607
+ const errors = this.filterErrors(
608
+ options?.agentId,
609
+ options?.startDate,
610
+ options?.endDate
611
+ );
612
+ const responses = this.filterResponses(
613
+ options?.agentId,
614
+ options?.startDate,
615
+ options?.endDate
616
+ );
617
+ return this.calculateErrorMetrics(errors, responses.length);
618
+ }
619
+ /**
620
+ * Get time series data
621
+ */
622
+ getTimeSeries(metric, options) {
623
+ const groupBy = options?.groupBy || "day";
624
+ const responses = this.filterResponses(
625
+ options?.agentId,
626
+ options?.startDate,
627
+ options?.endDate
628
+ );
629
+ const groups = /* @__PURE__ */ new Map();
630
+ for (const response of responses) {
631
+ const key = this.getTimeKey(response.timestamp, groupBy);
632
+ if (!groups.has(key)) {
633
+ groups.set(key, []);
634
+ }
635
+ groups.get(key).push(response);
636
+ }
637
+ const series = [];
638
+ for (const [key, groupResponses] of groups.entries()) {
639
+ let value = 0;
640
+ switch (metric) {
641
+ case "latency":
642
+ value = this.avg(groupResponses.map((r) => r.timings.total));
643
+ break;
644
+ case "tokens":
645
+ value = groupResponses.reduce((sum, r) => sum + r.tokens.totalTokens, 0);
646
+ break;
647
+ case "cost":
648
+ value = groupResponses.reduce((sum, r) => sum + (r.tokens.estimatedCost || 0), 0);
649
+ break;
650
+ case "errors":
651
+ value = groupResponses.filter((r) => !r.success).length;
652
+ break;
653
+ case "requests":
654
+ value = groupResponses.length;
655
+ break;
656
+ }
657
+ series.push({
658
+ timestamp: new Date(key),
659
+ value,
660
+ metadata: { count: groupResponses.length }
661
+ });
662
+ }
663
+ return series.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
664
+ }
665
+ // ============================================================================
666
+ // Private: Calculation Methods
667
+ // ============================================================================
668
+ calculatePerformanceMetrics(responses) {
669
+ if (responses.length === 0) {
670
+ return this.emptyPerformanceMetrics();
671
+ }
672
+ const latencies = responses.map((r) => r.timings.total);
673
+ const sorted = [...latencies].sort((a, b) => a - b);
674
+ const llmTimes = responses.map((r) => r.timings.llmApiTime).filter(Boolean);
675
+ const ragTimes = responses.map((r) => r.timings.ragRetrievalTime).filter(Boolean);
676
+ const pluginTimes = responses.map((r) => r.timings.pluginExecutionTime).filter(Boolean);
677
+ const dbTimes = responses.map((r) => r.timings.dbQueryTime).filter(Boolean);
678
+ const ttft = responses.map((r) => r.timings.timeToFirstToken).filter(Boolean);
679
+ const ttlt = responses.map((r) => r.timings.timeToLastToken).filter(Boolean);
680
+ return {
681
+ totalRequests: responses.length,
682
+ avgLatency: this.avg(latencies),
683
+ p50Latency: this.percentile(sorted, 50),
684
+ p95Latency: this.percentile(sorted, 95),
685
+ p99Latency: this.percentile(sorted, 99),
686
+ minLatency: Math.min(...latencies),
687
+ maxLatency: Math.max(...latencies),
688
+ avgLLMTime: this.avg(llmTimes),
689
+ avgRAGTime: this.avg(ragTimes),
690
+ avgPluginTime: this.avg(pluginTimes),
691
+ avgDbTime: this.avg(dbTimes),
692
+ avgTimeToFirstToken: this.avg(ttft),
693
+ avgTimeToLastToken: this.avg(ttlt),
694
+ latencyDistribution: {
695
+ under100ms: latencies.filter((l) => l < 100).length,
696
+ under500ms: latencies.filter((l) => l >= 100 && l < 500).length,
697
+ under1s: latencies.filter((l) => l >= 500 && l < 1e3).length,
698
+ under5s: latencies.filter((l) => l >= 1e3 && l < 5e3).length,
699
+ over5s: latencies.filter((l) => l >= 5e3).length
700
+ }
701
+ };
702
+ }
703
+ calculateRAGMetrics(responses) {
704
+ const ragResponses = responses.filter((r) => r.rag?.enabled);
705
+ if (ragResponses.length === 0) {
706
+ return this.emptyRAGMetrics();
707
+ }
708
+ const docsRetrieved = ragResponses.map((r) => r.rag.documentsRetrieved || 0);
709
+ const vectorTimes = ragResponses.map((r) => r.rag.vectorSearchTime).filter(Boolean);
710
+ const embedTimes = ragResponses.map((r) => r.rag.embeddingTime).filter(Boolean);
711
+ const similarities = ragResponses.map((r) => r.rag.avgSimilarityScore).filter(Boolean);
712
+ const rerankTimes = ragResponses.map((r) => r.rag.rerankTime).filter(Boolean);
713
+ const contextLengths = ragResponses.map((r) => r.rag.contextLength).filter(Boolean);
714
+ const contextTokens = ragResponses.map((r) => r.rag.contextTokens).filter(Boolean);
715
+ const sourcesCounts = ragResponses.map((r) => r.rag.sourcesCount).filter(Boolean);
716
+ const cacheHits = ragResponses.filter((r) => r.rag.cacheHit === true).length;
717
+ const cacheMisses = ragResponses.filter((r) => r.rag.cacheHit === false).length;
718
+ const successfulRetrievals = ragResponses.filter((r) => (r.rag.documentsRetrieved || 0) > 0).length;
719
+ return {
720
+ totalQueries: ragResponses.length,
721
+ avgDocumentsRetrieved: this.avg(docsRetrieved),
722
+ avgVectorSearchTime: this.avg(vectorTimes),
723
+ avgEmbeddingTime: this.avg(embedTimes),
724
+ cacheHitRate: cacheHits / (cacheHits + cacheMisses) || 0,
725
+ cacheMissRate: cacheMisses / (cacheHits + cacheMisses) || 0,
726
+ avgSimilarityScore: this.avg(similarities),
727
+ avgRerankTime: this.avg(rerankTimes),
728
+ avgContextLength: this.avg(contextLengths),
729
+ avgContextTokens: this.avg(contextTokens),
730
+ avgSourcesCount: this.avg(sourcesCounts),
731
+ retrievalSuccessRate: successfulRetrievals / ragResponses.length
732
+ };
733
+ }
734
+ calculateCostMetrics(responses) {
735
+ if (responses.length === 0) {
736
+ return this.emptyCostMetrics();
737
+ }
738
+ const totalCost = responses.reduce((sum, r) => sum + (r.tokens.estimatedCost || 0), 0);
739
+ const totalTokens = responses.reduce((sum, r) => sum + r.tokens.totalTokens, 0);
740
+ const totalPromptTokens = responses.reduce((sum, r) => sum + r.tokens.promptTokens, 0);
741
+ const totalCompletionTokens = responses.reduce((sum, r) => sum + r.tokens.completionTokens, 0);
742
+ const totalEmbeddingTokens = responses.reduce((sum, r) => sum + (r.rag?.contextTokens || 0), 0);
743
+ const totalEmbeddingCost = totalEmbeddingTokens * this.config.embeddingCost / 1e3;
744
+ const costByModel = {};
745
+ const tokensByModel = {};
746
+ for (const r of responses) {
747
+ const model = r.model || "unknown";
748
+ costByModel[model] = (costByModel[model] || 0) + (r.tokens.estimatedCost || 0);
749
+ tokensByModel[model] = (tokensByModel[model] || 0) + r.tokens.totalTokens;
750
+ }
751
+ const costByAgent = {};
752
+ for (const r of responses) {
753
+ costByAgent[r.agentId] = (costByAgent[r.agentId] || 0) + (r.tokens.estimatedCost || 0);
754
+ }
755
+ const dailyCosts = {};
756
+ for (const r of responses) {
757
+ const day = r.timestamp.toISOString().split("T")[0];
758
+ dailyCosts[day] = (dailyCosts[day] || 0) + (r.tokens.estimatedCost || 0);
759
+ }
760
+ return {
761
+ totalCost,
762
+ totalTokens,
763
+ totalPromptTokens,
764
+ totalCompletionTokens,
765
+ avgTokensPerRequest: totalTokens / responses.length,
766
+ avgCostPerRequest: totalCost / responses.length,
767
+ tokenEfficiency: totalCompletionTokens / (totalPromptTokens || 1),
768
+ costByModel,
769
+ costByAgent,
770
+ tokensByModel,
771
+ totalEmbeddingTokens,
772
+ totalEmbeddingCost,
773
+ dailyCosts
774
+ };
775
+ }
776
+ calculateConversationMetrics(requests, agentId) {
777
+ const relevantThreads = agentId ? Array.from(this.threadStats.values()).filter((t) => t.agentId === agentId) : Array.from(this.threadStats.values());
778
+ if (relevantThreads.length === 0) {
779
+ return this.emptyConversationMetrics();
780
+ }
781
+ const messageCounts = relevantThreads.map((t) => t.messageCount);
782
+ const durations = relevantThreads.map(
783
+ (t) => t.lastMessage.getTime() - t.firstMessage.getTime()
784
+ );
785
+ const abandonedThreads = relevantThreads.filter((t) => t.messageCount === 1);
786
+ const inputLengths = requests.map((r) => r.messageLength);
787
+ const shortInputs = inputLengths.filter((l) => l < 50).length;
788
+ const mediumInputs = inputLengths.filter((l) => l >= 50 && l < 200).length;
789
+ const longInputs = inputLengths.filter((l) => l >= 200 && l < 500).length;
790
+ const veryLongInputs = inputLengths.filter((l) => l >= 500).length;
791
+ let returnRate = 0;
792
+ if (this.userSessions.size > 0) {
793
+ const returningUsers = Array.from(this.userSessions.values()).filter(
794
+ (sessions) => sessions.length > 1
795
+ ).length;
796
+ returnRate = returningUsers / this.userSessions.size;
797
+ }
798
+ return {
799
+ totalThreads: relevantThreads.length,
800
+ totalMessages: messageCounts.reduce((a, b) => a + b, 0),
801
+ avgMessagesPerThread: this.avg(messageCounts),
802
+ avgThreadDuration: this.avg(durations),
803
+ avgSessionLength: this.avg(durations),
804
+ // Same as thread duration for now
805
+ userReturnRate: returnRate,
806
+ threadAbandonmentRate: abandonedThreads.length / relevantThreads.length,
807
+ avgInputLength: this.avg(inputLengths),
808
+ avgOutputLength: 0,
809
+ // Would need to track from responses
810
+ inputLengthDistribution: {
811
+ short: shortInputs,
812
+ medium: mediumInputs,
813
+ long: longInputs,
814
+ veryLong: veryLongInputs
815
+ }
816
+ };
817
+ }
818
+ calculateErrorMetrics(errors, totalRequests) {
819
+ const errorsByType = {};
820
+ for (const e of errors) {
821
+ errorsByType[e.errorType] = (errorsByType[e.errorType] || 0) + 1;
822
+ }
823
+ const llmErrors = errors.filter((e) => e.component === "llm").length;
824
+ const ragErrors = errors.filter((e) => e.component === "rag").length;
825
+ const pluginErrors = errors.filter((e) => e.component === "plugin").length;
826
+ const dbErrors = errors.filter((e) => e.component === "database").length;
827
+ const networkErrors = errors.filter((e) => e.component === "network").length;
828
+ const timeoutErrors = errors.filter((e) => e.errorType === "timeout").length;
829
+ const rateLimitHits = errors.filter((e) => e.errorType === "rate_limit").length;
830
+ const recentErrors = errors.slice(-10).reverse().map((e) => ({
831
+ timestamp: e.timestamp,
832
+ type: e.errorType,
833
+ message: e.errorMessage,
834
+ agentId: e.agentId
835
+ }));
836
+ return {
837
+ totalErrors: errors.length,
838
+ errorRate: totalRequests > 0 ? errors.length / totalRequests : 0,
839
+ errorsByType,
840
+ llmErrors,
841
+ ragErrors,
842
+ pluginErrors,
843
+ dbErrors,
844
+ networkErrors,
845
+ timeoutErrors,
846
+ rateLimitHits,
847
+ successRate: totalRequests > 0 ? (totalRequests - errors.length) / totalRequests : 1,
848
+ retryCount: 0,
849
+ // Would need to track this separately
850
+ fallbackUsage: 0,
851
+ // Would need to track this separately
852
+ recentErrors
853
+ };
854
+ }
855
+ // ============================================================================
856
+ // Private: Helper Methods
857
+ // ============================================================================
858
+ calculateCost(model, promptTokens, completionTokens, embeddingTokens) {
859
+ const modelCost = this.config.modelCosts[model];
860
+ if (!modelCost) return 0;
861
+ const inputCost = promptTokens / 1e3 * modelCost.input;
862
+ const outputCost = completionTokens / 1e3 * modelCost.output;
863
+ const embedCost = embeddingTokens ? embeddingTokens / 1e3 * this.config.embeddingCost : 0;
864
+ return inputCost + outputCost + embedCost;
865
+ }
866
+ updateThreadStats(threadId, agentId, userId, timestamp) {
867
+ const existing = this.threadStats.get(threadId);
868
+ if (existing) {
869
+ existing.messageCount++;
870
+ existing.lastMessage = timestamp;
871
+ } else {
872
+ this.threadStats.set(threadId, {
873
+ threadId,
874
+ agentId,
875
+ userId,
876
+ messageCount: 1,
877
+ firstMessage: timestamp,
878
+ lastMessage: timestamp
879
+ });
880
+ }
881
+ }
882
+ trackUserSession(userId, timestamp) {
883
+ const sessions = this.userSessions.get(userId) || [];
884
+ sessions.push(timestamp);
885
+ this.userSessions.set(userId, sessions);
886
+ }
887
+ filterResponses(agentId, startDate, endDate) {
888
+ return this.responses.filter((r) => {
889
+ if (agentId && r.agentId !== agentId) return false;
890
+ if (startDate && r.timestamp < startDate) return false;
891
+ if (endDate && r.timestamp > endDate) return false;
892
+ return true;
893
+ });
894
+ }
895
+ filterRequests(agentId, startDate, endDate) {
896
+ return this.requests.filter((r) => {
897
+ if (agentId && r.agentId !== agentId) return false;
898
+ if (startDate && r.timestamp < startDate) return false;
899
+ if (endDate && r.timestamp > endDate) return false;
900
+ return true;
901
+ });
902
+ }
903
+ filterErrors(agentId, startDate, endDate) {
904
+ return this.errors.filter((e) => {
905
+ if (agentId && e.agentId !== agentId) return false;
906
+ if (startDate && e.timestamp < startDate) return false;
907
+ if (endDate && e.timestamp > endDate) return false;
908
+ return true;
909
+ });
910
+ }
911
+ getTimeKey(date, groupBy) {
912
+ const d = new Date(date);
913
+ switch (groupBy) {
914
+ case "hour":
915
+ d.setMinutes(0, 0, 0);
916
+ return d.toISOString();
917
+ case "day":
918
+ d.setHours(0, 0, 0, 0);
919
+ return d.toISOString();
920
+ case "week":
921
+ const day = d.getDay();
922
+ d.setDate(d.getDate() - day);
923
+ d.setHours(0, 0, 0, 0);
924
+ return d.toISOString();
925
+ default:
926
+ return d.toISOString();
927
+ }
928
+ }
929
+ avg(numbers) {
930
+ if (numbers.length === 0) return 0;
931
+ return numbers.reduce((a, b) => a + b, 0) / numbers.length;
932
+ }
933
+ percentile(sorted, p) {
934
+ if (sorted.length === 0) return 0;
935
+ const index = Math.ceil(p / 100 * sorted.length) - 1;
936
+ return sorted[Math.max(0, index)];
937
+ }
938
+ async cleanup() {
939
+ if (this.config.retentionDays <= 0) return;
940
+ const cutoff = new Date(Date.now() - this.config.retentionDays * 24 * 60 * 60 * 1e3);
941
+ this.requests = this.requests.filter((r) => r.timestamp > cutoff);
942
+ this.responses = this.responses.filter((r) => r.timestamp > cutoff);
943
+ this.errors = this.errors.filter((e) => e.timestamp > cutoff);
944
+ for (const [threadId, stats] of this.threadStats.entries()) {
945
+ if (stats.lastMessage < cutoff) {
946
+ this.threadStats.delete(threadId);
947
+ }
948
+ }
949
+ if (this.config.storage) {
950
+ await this.storage.deleteOlderThan(cutoff);
951
+ }
952
+ }
953
+ // Empty metrics for when there's no data
954
+ emptyPerformanceMetrics() {
955
+ return {
956
+ totalRequests: 0,
957
+ avgLatency: 0,
958
+ p50Latency: 0,
959
+ p95Latency: 0,
960
+ p99Latency: 0,
961
+ minLatency: 0,
962
+ maxLatency: 0,
963
+ avgLLMTime: 0,
964
+ avgRAGTime: 0,
965
+ avgPluginTime: 0,
966
+ avgDbTime: 0,
967
+ avgTimeToFirstToken: 0,
968
+ avgTimeToLastToken: 0,
969
+ latencyDistribution: { under100ms: 0, under500ms: 0, under1s: 0, under5s: 0, over5s: 0 }
970
+ };
971
+ }
972
+ emptyRAGMetrics() {
973
+ return {
974
+ totalQueries: 0,
975
+ avgDocumentsRetrieved: 0,
976
+ avgVectorSearchTime: 0,
977
+ avgEmbeddingTime: 0,
978
+ cacheHitRate: 0,
979
+ cacheMissRate: 0,
980
+ avgSimilarityScore: 0,
981
+ avgRerankTime: 0,
982
+ avgContextLength: 0,
983
+ avgContextTokens: 0,
984
+ avgSourcesCount: 0,
985
+ retrievalSuccessRate: 0
986
+ };
987
+ }
988
+ emptyCostMetrics() {
989
+ return {
990
+ totalCost: 0,
991
+ totalTokens: 0,
992
+ totalPromptTokens: 0,
993
+ totalCompletionTokens: 0,
994
+ avgTokensPerRequest: 0,
995
+ avgCostPerRequest: 0,
996
+ tokenEfficiency: 0,
997
+ costByModel: {},
998
+ costByAgent: {},
999
+ tokensByModel: {},
1000
+ totalEmbeddingTokens: 0,
1001
+ totalEmbeddingCost: 0,
1002
+ dailyCosts: {}
1003
+ };
1004
+ }
1005
+ emptyConversationMetrics() {
1006
+ return {
1007
+ totalThreads: 0,
1008
+ totalMessages: 0,
1009
+ avgMessagesPerThread: 0,
1010
+ avgThreadDuration: 0,
1011
+ avgSessionLength: 0,
1012
+ userReturnRate: 0,
1013
+ threadAbandonmentRate: 0,
1014
+ avgInputLength: 0,
1015
+ avgOutputLength: 0,
1016
+ inputLengthDistribution: { short: 0, medium: 0, long: 0, veryLong: 0 }
1017
+ };
1018
+ }
1019
+ // ============================================================================
1020
+ // Public: Utility Methods
1021
+ // ============================================================================
1022
+ /**
1023
+ * Get raw data for export
1024
+ */
1025
+ exportData() {
1026
+ return {
1027
+ requests: [...this.requests],
1028
+ responses: [...this.responses],
1029
+ errors: [...this.errors]
1030
+ };
1031
+ }
1032
+ /**
1033
+ * Clear all analytics data
1034
+ */
1035
+ clear() {
1036
+ this.requests = [];
1037
+ this.responses = [];
1038
+ this.errors = [];
1039
+ this.pendingRequests = [];
1040
+ this.pendingResponses = [];
1041
+ this.pendingErrors = [];
1042
+ this.threadStats.clear();
1043
+ this.userSessions.clear();
1044
+ }
1045
+ /**
1046
+ * Get summary statistics
1047
+ */
1048
+ getSummary() {
1049
+ return {
1050
+ totalRequests: this.requests.length,
1051
+ totalResponses: this.responses.length,
1052
+ totalErrors: this.errors.length,
1053
+ totalThreads: this.threadStats.size,
1054
+ totalUsers: this.userSessions.size,
1055
+ dataRange: {
1056
+ oldest: this.requests[0]?.timestamp,
1057
+ newest: this.requests[this.requests.length - 1]?.timestamp
1058
+ }
1059
+ };
1060
+ }
1061
+ // ============================================================================
1062
+ // Flush Mechanism
1063
+ // ============================================================================
1064
+ /**
1065
+ * Start the flush timer
1066
+ */
1067
+ startFlushTimer() {
1068
+ if (this.flushTimer) {
1069
+ return;
1070
+ }
1071
+ this.flushTimer = setInterval(() => {
1072
+ this.flush().catch((err) => {
1073
+ console.error("[SnapAgentAnalytics] Flush error:", err);
1074
+ });
1075
+ }, this.config.flushInterval);
1076
+ }
1077
+ /**
1078
+ * Stop the flush timer
1079
+ */
1080
+ stopFlushTimer() {
1081
+ if (this.flushTimer) {
1082
+ clearInterval(this.flushTimer);
1083
+ this.flushTimer = void 0;
1084
+ }
1085
+ }
1086
+ /**
1087
+ * Flush pending data to external storage via onFlush callback
1088
+ * @returns Promise that resolves when flush is complete
1089
+ */
1090
+ async flush() {
1091
+ if (this.isFlushing) {
1092
+ return;
1093
+ }
1094
+ if (this.pendingRequests.length === 0 && this.pendingResponses.length === 0 && this.pendingErrors.length === 0) {
1095
+ return;
1096
+ }
1097
+ this.isFlushing = true;
1098
+ try {
1099
+ const flushData = {
1100
+ requests: [...this.pendingRequests],
1101
+ responses: [...this.pendingResponses],
1102
+ errors: [...this.pendingErrors],
1103
+ timestamp: /* @__PURE__ */ new Date()
1104
+ };
1105
+ this.pendingRequests = [];
1106
+ this.pendingResponses = [];
1107
+ this.pendingErrors = [];
1108
+ if (this.config.storage) {
1109
+ await Promise.all([
1110
+ this.storage.saveRequests(flushData.requests),
1111
+ this.storage.saveResponses(flushData.responses),
1112
+ this.storage.saveErrors(flushData.errors)
1113
+ ]);
1114
+ }
1115
+ } finally {
1116
+ this.isFlushing = false;
1117
+ }
1118
+ }
1119
+ /**
1120
+ * Stop the analytics plugin and flush remaining data
1121
+ * Call this before shutting down to ensure all data is persisted
1122
+ */
1123
+ async stop() {
1124
+ this.stopFlushTimer();
1125
+ await this.flush();
1126
+ await this.storage.close();
1127
+ }
1128
+ /**
1129
+ * Get count of pending (unflushed) items
1130
+ */
1131
+ getPendingCount() {
1132
+ return {
1133
+ requests: this.pendingRequests.length,
1134
+ responses: this.pendingResponses.length,
1135
+ errors: this.pendingErrors.length
1136
+ };
1137
+ }
1138
+ };
1139
+ export {
1140
+ MemoryAnalyticsStorage,
1141
+ MongoAnalyticsStorage,
1142
+ SnapAgentAnalytics
1143
+ };