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