@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/LICENSE +21 -0
- package/README.md +465 -0
- package/dist/index.d.mts +625 -0
- package/dist/index.d.ts +625 -0
- package/dist/index.js +1182 -0
- package/dist/index.mjs +1143 -0
- package/package.json +70 -0
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
|
+
});
|