@juspay/neurolink 9.94.6 → 9.95.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/CHANGELOG.md +12 -0
- package/dist/analytics/index.d.ts +7 -0
- package/dist/analytics/index.js +7 -0
- package/dist/analytics/parseQualityScore.d.ts +9 -0
- package/dist/analytics/parseQualityScore.js +29 -0
- package/dist/analytics/pricing.d.ts +24 -0
- package/dist/analytics/pricing.js +37 -0
- package/dist/analytics/service.d.ts +36 -0
- package/dist/analytics/service.js +393 -0
- package/dist/analytics/storage.d.ts +19 -0
- package/dist/analytics/storage.js +32 -0
- package/dist/browser/neurolink.min.js +389 -389
- package/dist/cli/factories/commandFactory.d.ts +0 -2
- package/dist/cli/factories/commandFactory.js +36 -45
- package/dist/cli/loop/session.js +21 -0
- package/dist/cli/utils/inputValidation.d.ts +38 -0
- package/dist/cli/utils/inputValidation.js +143 -0
- package/dist/lib/analytics/index.d.ts +7 -0
- package/dist/lib/analytics/index.js +8 -0
- package/dist/lib/analytics/parseQualityScore.d.ts +9 -0
- package/dist/lib/analytics/parseQualityScore.js +30 -0
- package/dist/lib/analytics/pricing.d.ts +24 -0
- package/dist/lib/analytics/pricing.js +38 -0
- package/dist/lib/analytics/service.d.ts +36 -0
- package/dist/lib/analytics/service.js +394 -0
- package/dist/lib/analytics/storage.d.ts +19 -0
- package/dist/lib/analytics/storage.js +33 -0
- package/dist/lib/neurolink.d.ts +54 -0
- package/dist/lib/neurolink.js +302 -204
- package/dist/lib/types/analytics.d.ts +127 -0
- package/dist/neurolink.d.ts +54 -0
- package/dist/neurolink.js +302 -204
- package/dist/types/analytics.d.ts +127 -0
- package/package.json +2 -1
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Analytics Service Layer
|
|
3
|
+
* Handles metrics collection, aggregation, cost analysis, and projections.
|
|
4
|
+
*/
|
|
5
|
+
import { calculateAdvancedCost } from "./pricing.js";
|
|
6
|
+
import { InMemoryAnalyticsStorage } from "./storage.js";
|
|
7
|
+
export class AnalyticsService {
|
|
8
|
+
storage;
|
|
9
|
+
constructor(storage) {
|
|
10
|
+
this.storage = storage || new InMemoryAnalyticsStorage();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Helper to parse dynamic time range formats safely
|
|
14
|
+
*/
|
|
15
|
+
parseTimeRange(timeRange) {
|
|
16
|
+
if (!timeRange) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (typeof timeRange === "object" &&
|
|
20
|
+
"start" in timeRange &&
|
|
21
|
+
"end" in timeRange) {
|
|
22
|
+
const startObj = timeRange.start;
|
|
23
|
+
const endObj = timeRange.end;
|
|
24
|
+
const start = startObj instanceof Date
|
|
25
|
+
? startObj.getTime()
|
|
26
|
+
: new Date(startObj).getTime();
|
|
27
|
+
const end = endObj instanceof Date
|
|
28
|
+
? endObj.getTime()
|
|
29
|
+
: new Date(endObj).getTime();
|
|
30
|
+
if (!Number.isNaN(start) && !Number.isNaN(end)) {
|
|
31
|
+
return { start, end };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (typeof timeRange === "string") {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
const dayMs = 24 * 3600 * 1000;
|
|
37
|
+
if (timeRange === "last_24_hours") {
|
|
38
|
+
return { start: now - dayMs, end: now };
|
|
39
|
+
}
|
|
40
|
+
if (timeRange === "last_7_days") {
|
|
41
|
+
return { start: now - 7 * dayMs, end: now };
|
|
42
|
+
}
|
|
43
|
+
if (timeRange === "last_30_days") {
|
|
44
|
+
return { start: now - 30 * dayMs, end: now };
|
|
45
|
+
}
|
|
46
|
+
// Month/quarter bounds use UTC midnight for timezone-stable windows
|
|
47
|
+
if (timeRange === "current_month") {
|
|
48
|
+
const d = new Date();
|
|
49
|
+
const startUtc = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
|
|
50
|
+
return { start: startUtc, end: now };
|
|
51
|
+
}
|
|
52
|
+
if (timeRange === "current_quarter") {
|
|
53
|
+
const d = new Date();
|
|
54
|
+
const qMonth = Math.floor(d.getUTCMonth() / 3) * 3;
|
|
55
|
+
const startUtc = Date.UTC(d.getUTCFullYear(), qMonth, 1);
|
|
56
|
+
return { start: startUtc, end: now };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Capture a request lifecycle record
|
|
63
|
+
*/
|
|
64
|
+
async trackRequest(record) {
|
|
65
|
+
// Preserve explicit costs (including 0). Only price when cost is omitted.
|
|
66
|
+
const cost = record.cost !== undefined
|
|
67
|
+
? record.cost
|
|
68
|
+
: calculateAdvancedCost(record.model, record.inputTokens, record.outputTokens, record.provider);
|
|
69
|
+
const fullRecord = {
|
|
70
|
+
...record,
|
|
71
|
+
id: crypto.randomUUID(),
|
|
72
|
+
cost,
|
|
73
|
+
};
|
|
74
|
+
await this.storage.saveRecord(fullRecord);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* getProviderMetrics implementation
|
|
78
|
+
*/
|
|
79
|
+
async getProviderMetrics(options = {}) {
|
|
80
|
+
const allRecords = await this.storage.getRecords();
|
|
81
|
+
const range = this.parseTimeRange(options.timeRange);
|
|
82
|
+
// Filter records
|
|
83
|
+
const records = allRecords.filter((r) => {
|
|
84
|
+
if (range && (r.timestamp < range.start || r.timestamp > range.end)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (options.providers && options.providers.length > 0) {
|
|
88
|
+
if (!options.providers.includes(r.provider)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
});
|
|
94
|
+
// Compute top-level aggregated metrics
|
|
95
|
+
let totalLatency = 0;
|
|
96
|
+
let totalTokens = 0;
|
|
97
|
+
let inputTokens = 0;
|
|
98
|
+
let outputTokens = 0;
|
|
99
|
+
let errors = 0;
|
|
100
|
+
let totalCost = 0;
|
|
101
|
+
const requestCount = records.length;
|
|
102
|
+
// Group by provider
|
|
103
|
+
const providerMap = {};
|
|
104
|
+
for (const r of records) {
|
|
105
|
+
totalLatency += r.latency;
|
|
106
|
+
totalTokens += r.totalTokens;
|
|
107
|
+
inputTokens += r.inputTokens;
|
|
108
|
+
outputTokens += r.outputTokens;
|
|
109
|
+
if (r.isError) {
|
|
110
|
+
errors++;
|
|
111
|
+
}
|
|
112
|
+
totalCost += r.cost;
|
|
113
|
+
if (!providerMap[r.provider]) {
|
|
114
|
+
providerMap[r.provider] = [];
|
|
115
|
+
}
|
|
116
|
+
providerMap[r.provider].push(r);
|
|
117
|
+
}
|
|
118
|
+
const averageLatency = requestCount > 0 ? totalLatency / requestCount : 0;
|
|
119
|
+
const errorRate = requestCount > 0 ? errors / requestCount : 0;
|
|
120
|
+
// Empty datasets must not report a fabricated 100% success rate
|
|
121
|
+
const successRate = requestCount > 0 ? 1 - errorRate : 0;
|
|
122
|
+
const costPerToken = totalTokens > 0 ? totalCost / totalTokens : 0;
|
|
123
|
+
// Build per-provider results
|
|
124
|
+
const providers = [];
|
|
125
|
+
for (const [name, pRecords] of Object.entries(providerMap)) {
|
|
126
|
+
const pCount = pRecords.length;
|
|
127
|
+
let pLatency = 0;
|
|
128
|
+
let pTotalTokens = 0;
|
|
129
|
+
let pInputTokens = 0;
|
|
130
|
+
let pOutputTokens = 0;
|
|
131
|
+
let pErrors = 0;
|
|
132
|
+
let pCost = 0;
|
|
133
|
+
for (const r of pRecords) {
|
|
134
|
+
pLatency += r.latency;
|
|
135
|
+
pTotalTokens += r.totalTokens;
|
|
136
|
+
pInputTokens += r.inputTokens;
|
|
137
|
+
pOutputTokens += r.outputTokens;
|
|
138
|
+
if (r.isError) {
|
|
139
|
+
pErrors++;
|
|
140
|
+
}
|
|
141
|
+
pCost += r.cost;
|
|
142
|
+
}
|
|
143
|
+
const pAvgLatency = pCount > 0 ? pLatency / pCount : 0;
|
|
144
|
+
const pErrorRate = pCount > 0 ? pErrors / pCount : 0;
|
|
145
|
+
const pSuccessRate = 1 - pErrorRate;
|
|
146
|
+
const pCostPerToken = pTotalTokens > 0 ? pCost / pTotalTokens : 0;
|
|
147
|
+
providers.push({
|
|
148
|
+
name,
|
|
149
|
+
averageLatency: pAvgLatency,
|
|
150
|
+
averageResponseTime: pAvgLatency, // docs compatibility
|
|
151
|
+
totalTokens: pTotalTokens,
|
|
152
|
+
inputTokens: pInputTokens,
|
|
153
|
+
outputTokens: pOutputTokens,
|
|
154
|
+
errorRate: pErrorRate,
|
|
155
|
+
successRate: pSuccessRate,
|
|
156
|
+
costPerToken: pCostPerToken,
|
|
157
|
+
totalCost: pCost,
|
|
158
|
+
requestCount: pCount,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
averageLatency,
|
|
163
|
+
averageResponseTime: averageLatency,
|
|
164
|
+
totalTokens,
|
|
165
|
+
inputTokens,
|
|
166
|
+
outputTokens,
|
|
167
|
+
errorRate,
|
|
168
|
+
successRate,
|
|
169
|
+
costPerToken,
|
|
170
|
+
totalCost,
|
|
171
|
+
requestCount,
|
|
172
|
+
providers,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Helper to format timestamps for date groupings.
|
|
177
|
+
* Clones the Date so week arithmetic never mutates the caller's value.
|
|
178
|
+
*/
|
|
179
|
+
formatGroupDate(timestamp, type) {
|
|
180
|
+
const d = new Date(timestamp);
|
|
181
|
+
if (type === "day") {
|
|
182
|
+
return d.toISOString().split("T")[0];
|
|
183
|
+
}
|
|
184
|
+
if (type === "month") {
|
|
185
|
+
return d.toISOString().substring(0, 7);
|
|
186
|
+
}
|
|
187
|
+
if (type === "week") {
|
|
188
|
+
// UTC Monday of the containing ISO week — never mutate caller input
|
|
189
|
+
// and avoid local/UTC drift from mixing getDay()/toISOString().
|
|
190
|
+
const clone = new Date(timestamp);
|
|
191
|
+
const day = clone.getUTCDay();
|
|
192
|
+
const diff = clone.getUTCDate() - day + (day === 0 ? -6 : 1);
|
|
193
|
+
clone.setUTCDate(diff);
|
|
194
|
+
return clone.toISOString().split("T")[0];
|
|
195
|
+
}
|
|
196
|
+
return d.toISOString().split("T")[0];
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* getCostAnalysis implementation
|
|
200
|
+
*/
|
|
201
|
+
async getCostAnalysis(options = {}) {
|
|
202
|
+
const allRecords = await this.storage.getRecords();
|
|
203
|
+
const range = this.parseTimeRange(options.timeRange);
|
|
204
|
+
const records = allRecords.filter((r) => {
|
|
205
|
+
if (range && (r.timestamp < range.start || r.timestamp > range.end)) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
});
|
|
210
|
+
let totalCost = 0;
|
|
211
|
+
let totalTokens = 0;
|
|
212
|
+
let inputTokens = 0;
|
|
213
|
+
let outputTokens = 0;
|
|
214
|
+
const requestCount = records.length;
|
|
215
|
+
const groups = {};
|
|
216
|
+
const providerGroupsMap = {};
|
|
217
|
+
const groupByArr = Array.isArray(options.groupBy)
|
|
218
|
+
? options.groupBy
|
|
219
|
+
: typeof options.groupBy === "string"
|
|
220
|
+
? [options.groupBy]
|
|
221
|
+
: ["provider"];
|
|
222
|
+
for (const r of records) {
|
|
223
|
+
totalCost += r.cost;
|
|
224
|
+
totalTokens += r.totalTokens;
|
|
225
|
+
inputTokens += r.inputTokens;
|
|
226
|
+
outputTokens += r.outputTokens;
|
|
227
|
+
// Ensure per-provider items are always built for documentation support
|
|
228
|
+
if (!providerGroupsMap[r.provider]) {
|
|
229
|
+
providerGroupsMap[r.provider] = {
|
|
230
|
+
groupKey: r.provider,
|
|
231
|
+
provider: r.provider,
|
|
232
|
+
totalCost: 0,
|
|
233
|
+
costPerToken: 0,
|
|
234
|
+
inputTokens: 0,
|
|
235
|
+
outputTokens: 0,
|
|
236
|
+
totalTokens: 0,
|
|
237
|
+
requestCount: 0,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const pg = providerGroupsMap[r.provider];
|
|
241
|
+
pg.totalCost += r.cost;
|
|
242
|
+
pg.inputTokens += r.inputTokens;
|
|
243
|
+
pg.outputTokens += r.outputTokens;
|
|
244
|
+
pg.totalTokens += r.totalTokens;
|
|
245
|
+
pg.requestCount += 1;
|
|
246
|
+
pg.costPerToken = pg.totalTokens > 0 ? pg.totalCost / pg.totalTokens : 0;
|
|
247
|
+
// Build specific requested group key
|
|
248
|
+
const keyParts = [];
|
|
249
|
+
for (const g of groupByArr) {
|
|
250
|
+
if (g === "provider") {
|
|
251
|
+
keyParts.push(r.provider);
|
|
252
|
+
}
|
|
253
|
+
else if (g === "model") {
|
|
254
|
+
keyParts.push(r.model);
|
|
255
|
+
}
|
|
256
|
+
else if (g === "user_id" || g === "userId") {
|
|
257
|
+
keyParts.push(r.userId || "unknown");
|
|
258
|
+
}
|
|
259
|
+
else if (g === "day" || g === "week" || g === "month") {
|
|
260
|
+
keyParts.push(this.formatGroupDate(r.timestamp, g));
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
keyParts.push(r.provider);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const groupKey = keyParts.join("#") || r.provider;
|
|
267
|
+
if (!groups[groupKey]) {
|
|
268
|
+
groups[groupKey] = {
|
|
269
|
+
groupKey,
|
|
270
|
+
provider: r.provider,
|
|
271
|
+
model: r.model,
|
|
272
|
+
userId: r.userId,
|
|
273
|
+
totalCost: 0,
|
|
274
|
+
costPerToken: 0,
|
|
275
|
+
inputTokens: 0,
|
|
276
|
+
outputTokens: 0,
|
|
277
|
+
totalTokens: 0,
|
|
278
|
+
requestCount: 0,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const gItem = groups[groupKey];
|
|
282
|
+
gItem.totalCost += r.cost;
|
|
283
|
+
gItem.inputTokens += r.inputTokens;
|
|
284
|
+
gItem.outputTokens += r.outputTokens;
|
|
285
|
+
gItem.totalTokens += r.totalTokens;
|
|
286
|
+
gItem.requestCount += 1;
|
|
287
|
+
gItem.costPerToken =
|
|
288
|
+
gItem.totalTokens > 0 ? gItem.totalCost / gItem.totalTokens : 0;
|
|
289
|
+
}
|
|
290
|
+
const providers = Object.values(providerGroupsMap);
|
|
291
|
+
// Optional future projections using simple average-based estimation
|
|
292
|
+
let projections;
|
|
293
|
+
if (options.includeProjections) {
|
|
294
|
+
if (records.length === 0) {
|
|
295
|
+
projections = { nextMonth: 0, nextQuarter: 0 };
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
// Find span of records in days
|
|
299
|
+
const timestamps = records.map((r) => r.timestamp);
|
|
300
|
+
const minTime = Math.min(...timestamps);
|
|
301
|
+
const maxTime = Math.max(...timestamps);
|
|
302
|
+
const diffDays = Math.max(1, (maxTime - minTime) / (24 * 3600 * 1000));
|
|
303
|
+
const dailyAverage = totalCost / diffDays;
|
|
304
|
+
projections = {
|
|
305
|
+
nextMonth: dailyAverage * 30,
|
|
306
|
+
nextQuarter: dailyAverage * 90,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
totalCost,
|
|
312
|
+
totalTokens,
|
|
313
|
+
inputTokens,
|
|
314
|
+
outputTokens,
|
|
315
|
+
requestCount,
|
|
316
|
+
groups,
|
|
317
|
+
providers,
|
|
318
|
+
projections,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* getTeamAnalytics implementation
|
|
323
|
+
*/
|
|
324
|
+
async getTeamAnalytics(options = {}) {
|
|
325
|
+
const allRecords = await this.storage.getRecords();
|
|
326
|
+
const range = this.parseTimeRange(options.timeRange);
|
|
327
|
+
// teamId and departments are independent AND filters (docs: both supported).
|
|
328
|
+
const records = allRecords.filter((r) => {
|
|
329
|
+
if (range && (r.timestamp < range.start || r.timestamp > range.end)) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
if (options.teamId) {
|
|
333
|
+
const recTeam = r.teamId || "default";
|
|
334
|
+
if (recTeam !== options.teamId) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (options.departments && options.departments.length > 0) {
|
|
339
|
+
const dept = r.department;
|
|
340
|
+
if (!dept || !options.departments.includes(dept)) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return true;
|
|
345
|
+
});
|
|
346
|
+
const totalRequests = records.length;
|
|
347
|
+
const uniqueUsersSet = new Set();
|
|
348
|
+
const providersSet = new Set();
|
|
349
|
+
const costBreakdownByProvider = {};
|
|
350
|
+
const costBreakdownByUser = {};
|
|
351
|
+
let totalQualityScore = 0;
|
|
352
|
+
let totalRelevance = 0;
|
|
353
|
+
let totalAccuracy = 0;
|
|
354
|
+
let totalCompleteness = 0;
|
|
355
|
+
let scoredCount = 0;
|
|
356
|
+
for (const r of records) {
|
|
357
|
+
if (r.userId) {
|
|
358
|
+
uniqueUsersSet.add(r.userId);
|
|
359
|
+
}
|
|
360
|
+
providersSet.add(r.provider);
|
|
361
|
+
costBreakdownByProvider[r.provider] =
|
|
362
|
+
(costBreakdownByProvider[r.provider] || 0) + r.cost;
|
|
363
|
+
const uKey = r.userId || "anonymous";
|
|
364
|
+
costBreakdownByUser[uKey] = (costBreakdownByUser[uKey] || 0) + r.cost;
|
|
365
|
+
if (r.qualityScore) {
|
|
366
|
+
scoredCount++;
|
|
367
|
+
totalQualityScore += r.qualityScore.overall;
|
|
368
|
+
totalRelevance += r.qualityScore.relevance;
|
|
369
|
+
totalAccuracy += r.qualityScore.accuracy;
|
|
370
|
+
totalCompleteness += r.qualityScore.completeness;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Omit fabricated scores when no requests were evaluated
|
|
374
|
+
const qualityScores = scoredCount > 0
|
|
375
|
+
? {
|
|
376
|
+
overall: totalQualityScore / scoredCount,
|
|
377
|
+
relevance: totalRelevance / scoredCount,
|
|
378
|
+
accuracy: totalAccuracy / scoredCount,
|
|
379
|
+
completeness: totalCompleteness / scoredCount,
|
|
380
|
+
reasoning: "Aggregated automated response evaluations",
|
|
381
|
+
}
|
|
382
|
+
: undefined;
|
|
383
|
+
return {
|
|
384
|
+
totalRequests,
|
|
385
|
+
// Count only attributed users — never fabricate a user when none present
|
|
386
|
+
uniqueUsers: uniqueUsersSet.size,
|
|
387
|
+
providersUsed: Array.from(providersSet),
|
|
388
|
+
costBreakdownByProvider,
|
|
389
|
+
costBreakdownByUser,
|
|
390
|
+
qualityScores,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics storage — in-memory implementation with bounded FIFO capacity.
|
|
3
|
+
* Designed modularly so Redis or a database can later replace the backend.
|
|
4
|
+
*/
|
|
5
|
+
import type { AnalyticsStorage, InMemoryAnalyticsStorageOptions, TelemetryRecord } from "../types/index.js";
|
|
6
|
+
/** Default max records retained in memory (FIFO eviction beyond this). */
|
|
7
|
+
export declare const DEFAULT_ANALYTICS_STORAGE_MAX_RECORDS = 10000;
|
|
8
|
+
/**
|
|
9
|
+
* Lightweight bounded in-memory storage implementation.
|
|
10
|
+
* Public surface of AnalyticsStorage is unchanged; maxRecords is optional.
|
|
11
|
+
*/
|
|
12
|
+
export declare class InMemoryAnalyticsStorage implements AnalyticsStorage {
|
|
13
|
+
private records;
|
|
14
|
+
private readonly maxRecords;
|
|
15
|
+
constructor(options?: InMemoryAnalyticsStorageOptions);
|
|
16
|
+
saveRecord(record: TelemetryRecord): Promise<void>;
|
|
17
|
+
getRecords(): Promise<TelemetryRecord[]>;
|
|
18
|
+
clear(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics storage — in-memory implementation with bounded FIFO capacity.
|
|
3
|
+
* Designed modularly so Redis or a database can later replace the backend.
|
|
4
|
+
*/
|
|
5
|
+
/** Default max records retained in memory (FIFO eviction beyond this). */
|
|
6
|
+
export const DEFAULT_ANALYTICS_STORAGE_MAX_RECORDS = 10_000;
|
|
7
|
+
/**
|
|
8
|
+
* Lightweight bounded in-memory storage implementation.
|
|
9
|
+
* Public surface of AnalyticsStorage is unchanged; maxRecords is optional.
|
|
10
|
+
*/
|
|
11
|
+
export class InMemoryAnalyticsStorage {
|
|
12
|
+
records = [];
|
|
13
|
+
maxRecords;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
const max = options?.maxRecords ?? DEFAULT_ANALYTICS_STORAGE_MAX_RECORDS;
|
|
16
|
+
this.maxRecords =
|
|
17
|
+
max > 0 ? Math.floor(max) : DEFAULT_ANALYTICS_STORAGE_MAX_RECORDS;
|
|
18
|
+
}
|
|
19
|
+
async saveRecord(record) {
|
|
20
|
+
this.records.push(record);
|
|
21
|
+
// Slice once when over capacity — avoids O(n²) repeated shift() on burst.
|
|
22
|
+
if (this.records.length > this.maxRecords) {
|
|
23
|
+
this.records = this.records.slice(-this.maxRecords);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async getRecords() {
|
|
27
|
+
return [...this.records];
|
|
28
|
+
}
|
|
29
|
+
async clear() {
|
|
30
|
+
this.records = [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=storage.js.map
|
package/dist/lib/neurolink.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { ExternalServerManager } from "./mcp/externalServerManager.js";
|
|
|
13
13
|
import { MCPToolRegistry } from "./mcp/toolRegistry.js";
|
|
14
14
|
import type { DynamicOptions } from "./types/index.js";
|
|
15
15
|
import { SkillsManager } from "./skills/skillsManager.js";
|
|
16
|
+
import type { CostAnalysisOptions, CostAnalysisResult, ProviderMetricsOptions, ProviderMetricsResult, TeamAnalyticsOptions, TeamAnalyticsResult } from "./types/index.js";
|
|
16
17
|
import { TaskManager } from "./tasks/taskManager.js";
|
|
17
18
|
import type { SessionExport, SessionListItem } from "./types/index.js";
|
|
18
19
|
/**
|
|
@@ -220,6 +221,7 @@ export declare class NeuroLink {
|
|
|
220
221
|
*/
|
|
221
222
|
private observabilityConfig?;
|
|
222
223
|
private metricsAggregator;
|
|
224
|
+
private analyticsService;
|
|
223
225
|
/**
|
|
224
226
|
* Per-request metrics trace context backed by AsyncLocalStorage.
|
|
225
227
|
* Safe for concurrent requests on the same SDK instance.
|
|
@@ -532,6 +534,30 @@ export declare class NeuroLink {
|
|
|
532
534
|
* Record a span for metrics tracking
|
|
533
535
|
*/
|
|
534
536
|
recordMetricsSpan(span: SpanData): void;
|
|
537
|
+
/**
|
|
538
|
+
* Get provider metrics analysis
|
|
539
|
+
* Retrieves aggregated performance, token usage, latency, and success rates per provider.
|
|
540
|
+
*
|
|
541
|
+
* @param options - Filtering options
|
|
542
|
+
* @returns Comprehensive provider metrics result
|
|
543
|
+
*/
|
|
544
|
+
getProviderMetrics(options?: ProviderMetricsOptions): Promise<ProviderMetricsResult>;
|
|
545
|
+
/**
|
|
546
|
+
* Get cost analysis breakdown
|
|
547
|
+
* Analyzes AI generation costs across requested groups and provides future projections.
|
|
548
|
+
*
|
|
549
|
+
* @param options - Cost configuration options
|
|
550
|
+
* @returns Detailed cost analysis breakdown
|
|
551
|
+
*/
|
|
552
|
+
getCostAnalysis(options?: CostAnalysisOptions): Promise<CostAnalysisResult>;
|
|
553
|
+
/**
|
|
554
|
+
* Get team-wide usage analytics
|
|
555
|
+
* Retrieves request counts, unique active users, provider breakdown, and quality scoring.
|
|
556
|
+
*
|
|
557
|
+
* @param options - Team query options
|
|
558
|
+
* @returns Comprehensive team analytics report
|
|
559
|
+
*/
|
|
560
|
+
getTeamAnalytics(options?: TeamAnalyticsOptions): Promise<TeamAnalyticsResult>;
|
|
535
561
|
/**
|
|
536
562
|
* Record a memory operation span to both instance and global metrics aggregators.
|
|
537
563
|
* This ensures memory spans are visible via sdk.getSpans() and getMetricsAggregator().getSpans().
|
|
@@ -546,6 +572,34 @@ export declare class NeuroLink {
|
|
|
546
572
|
* Gracefully shutdown NeuroLink and all MCP connections
|
|
547
573
|
*/
|
|
548
574
|
shutdown(): Promise<void>;
|
|
575
|
+
/**
|
|
576
|
+
* Fire-and-forget analytics tracking with rejection logging.
|
|
577
|
+
*/
|
|
578
|
+
private enqueueAnalyticsTrackRequest;
|
|
579
|
+
/**
|
|
580
|
+
* Estimate cost using the canonical SDK pricing table (same source as
|
|
581
|
+
* AnalyticsService.trackRequest) — avoids diverging TokenTracker rates.
|
|
582
|
+
*/
|
|
583
|
+
private estimateCostFromUsage;
|
|
584
|
+
/**
|
|
585
|
+
* Handle generation:end for Pipeline B spans + advanced analytics.
|
|
586
|
+
* When pipelineAHandled is set, skips Pipeline B spans but still tracks
|
|
587
|
+
* analytics so successful AI-SDK generate/stream calls are recorded.
|
|
588
|
+
* Stream analytics are owned here (not stream:complete) to avoid duplicates.
|
|
589
|
+
*/
|
|
590
|
+
private handleGenerationEndMetrics;
|
|
591
|
+
/**
|
|
592
|
+
* Pipeline B span recording for stream:complete.
|
|
593
|
+
* Advanced analytics are recorded from generation:end only (avoids duplicates
|
|
594
|
+
* when both stream:complete and generation:end fire for the same request).
|
|
595
|
+
*/
|
|
596
|
+
private handleStreamCompleteMetrics;
|
|
597
|
+
private handleToolEndMetrics;
|
|
598
|
+
/**
|
|
599
|
+
* Pipeline B span recording for stream:error.
|
|
600
|
+
* Analytics for failed streams come from generation:end (success: false).
|
|
601
|
+
*/
|
|
602
|
+
private handleStreamErrorMetrics;
|
|
549
603
|
/**
|
|
550
604
|
* Initialize event listeners that feed span data to MetricsAggregator.
|
|
551
605
|
* Listens to generation:end, stream:complete, and tool:end events.
|