@narumitw/pi-analytics 0.46.0 → 0.48.1
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/README.md +43 -68
- package/package.json +3 -4
- package/src/analytics.ts +102 -109
- package/src/collector.ts +4 -0
- package/src/menu.ts +28 -13
- package/src/skills.ts +6 -7
- package/src/storage/files.ts +434 -0
- package/src/storage/format.ts +281 -0
- package/src/storage/queries.ts +99 -151
- package/src/storage/store.ts +32 -227
- package/src/storage/database.ts +0 -126
- package/src/storage/migrations.ts +0 -257
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GenerationOutcome,
|
|
3
|
+
GenerationRecord,
|
|
4
|
+
ProviderErrorCategory,
|
|
5
|
+
ProviderErrorRecord,
|
|
6
|
+
ProviderResponseRecord,
|
|
7
|
+
RunOutcome,
|
|
8
|
+
SettledRun,
|
|
9
|
+
SkillActivationRecord,
|
|
10
|
+
ToolCallRecord,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
|
|
13
|
+
export const MAX_STORED_RUN_BYTES = 1024 * 1024;
|
|
14
|
+
const MAX_STRING_LENGTH = 4096;
|
|
15
|
+
const MAX_NESTED_RECORDS = 20_000;
|
|
16
|
+
const TRIGGER_SOURCES = ["interactive", "rpc", "extension", "unknown"] as const;
|
|
17
|
+
const RUN_OUTCOMES = [
|
|
18
|
+
"success",
|
|
19
|
+
"recovered_success",
|
|
20
|
+
"error",
|
|
21
|
+
"aborted",
|
|
22
|
+
"length",
|
|
23
|
+
"interrupted",
|
|
24
|
+
] as const;
|
|
25
|
+
const GENERATION_OUTCOMES = [
|
|
26
|
+
"pending",
|
|
27
|
+
"stop",
|
|
28
|
+
"tool_use",
|
|
29
|
+
"error",
|
|
30
|
+
"aborted",
|
|
31
|
+
"length",
|
|
32
|
+
"interrupted",
|
|
33
|
+
] as const;
|
|
34
|
+
const ERROR_CATEGORIES = [
|
|
35
|
+
"dns",
|
|
36
|
+
"timeout",
|
|
37
|
+
"connection_refused",
|
|
38
|
+
"connection_reset",
|
|
39
|
+
"tls",
|
|
40
|
+
"network_other",
|
|
41
|
+
"provider_other",
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
export class AnalyticsStorageFormatError extends Error {
|
|
45
|
+
constructor(message: string, options: { cause?: unknown } = {}) {
|
|
46
|
+
super(message, options);
|
|
47
|
+
this.name = "AnalyticsStorageFormatError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function encodeStoredRun(run: SettledRun): string {
|
|
52
|
+
const encoded = `${JSON.stringify({
|
|
53
|
+
formatVersion: 1,
|
|
54
|
+
run: parseRun(run, { remaining: MAX_NESTED_RECORDS }),
|
|
55
|
+
})}\n`;
|
|
56
|
+
if (Buffer.byteLength(encoded) > MAX_STORED_RUN_BYTES) {
|
|
57
|
+
throw new AnalyticsStorageFormatError("Analytics record is too large to store safely.");
|
|
58
|
+
}
|
|
59
|
+
return encoded;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function decodeStoredRun(line: string): SettledRun {
|
|
63
|
+
if (Buffer.byteLength(line) > MAX_STORED_RUN_BYTES) {
|
|
64
|
+
throw new AnalyticsStorageFormatError("Analytics record is too large to read safely.");
|
|
65
|
+
}
|
|
66
|
+
let value: unknown;
|
|
67
|
+
try {
|
|
68
|
+
value = JSON.parse(line);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new AnalyticsStorageFormatError("Analytics record contains invalid JSON.", {
|
|
71
|
+
cause: error,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const envelope = asRecord(value, "analytics record");
|
|
75
|
+
if (envelope.formatVersion !== 1) {
|
|
76
|
+
throw new AnalyticsStorageFormatError("Analytics record uses an unsupported format version.");
|
|
77
|
+
}
|
|
78
|
+
return parseRun(envelope.run, { remaining: MAX_NESTED_RECORDS });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface ParseBudget {
|
|
82
|
+
remaining: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseRun(value: unknown, budget: ParseBudget): SettledRun {
|
|
86
|
+
const run = asRecord(value, "run");
|
|
87
|
+
return {
|
|
88
|
+
id: requiredString(run.id, "run.id"),
|
|
89
|
+
startedAtMs: timestampValue(run.startedAtMs, "run.startedAtMs"),
|
|
90
|
+
finishedAtMs: timestampValue(run.finishedAtMs, "run.finishedAtMs"),
|
|
91
|
+
durationMs: durationValue(run.durationMs, "run.durationMs"),
|
|
92
|
+
triggerSource: enumValue(run.triggerSource, TRIGGER_SOURCES, "run.triggerSource"),
|
|
93
|
+
...optionalProperty(
|
|
94
|
+
"initialProvider",
|
|
95
|
+
optionalString(run.initialProvider, "run.initialProvider"),
|
|
96
|
+
),
|
|
97
|
+
...optionalProperty("initialModel", optionalString(run.initialModel, "run.initialModel")),
|
|
98
|
+
outcome: enumValue(run.outcome, RUN_OUTCOMES, "run.outcome") as RunOutcome,
|
|
99
|
+
attemptCount: boundedCount(run.attemptCount, "run.attemptCount"),
|
|
100
|
+
generations: boundedArray(run.generations, "run.generations", budget).map((item, index) =>
|
|
101
|
+
parseGeneration(item, index, budget),
|
|
102
|
+
),
|
|
103
|
+
tools: boundedArray(run.tools, "run.tools", budget).map(parseTool),
|
|
104
|
+
skills: boundedArray(run.skills, "run.skills", budget).map(parseSkill),
|
|
105
|
+
providerErrors: boundedArray(run.providerErrors, "run.providerErrors", budget).map(
|
|
106
|
+
parseProviderError,
|
|
107
|
+
),
|
|
108
|
+
toolErrorCount: boundedCount(run.toolErrorCount, "run.toolErrorCount"),
|
|
109
|
+
providerErrorCount: boundedCount(run.providerErrorCount, "run.providerErrorCount"),
|
|
110
|
+
recoveredErrorCount: boundedCount(run.recoveredErrorCount, "run.recoveredErrorCount"),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseGeneration(value: unknown, index: number, budget: ParseBudget): GenerationRecord {
|
|
115
|
+
const item = asRecord(value, `run.generations[${index}]`);
|
|
116
|
+
const prefix = `run.generations[${index}]`;
|
|
117
|
+
return {
|
|
118
|
+
id: requiredString(item.id, `${prefix}.id`),
|
|
119
|
+
ordinal: boundedCount(item.ordinal, `${prefix}.ordinal`),
|
|
120
|
+
...optionalProperty("provider", optionalString(item.provider, `${prefix}.provider`)),
|
|
121
|
+
...optionalProperty("model", optionalString(item.model, `${prefix}.model`)),
|
|
122
|
+
...optionalProperty(
|
|
123
|
+
"thinkingLevel",
|
|
124
|
+
optionalString(item.thinkingLevel, `${prefix}.thinkingLevel`),
|
|
125
|
+
),
|
|
126
|
+
startedAtMs: timestampValue(item.startedAtMs, `${prefix}.startedAtMs`),
|
|
127
|
+
...optionalProperty(
|
|
128
|
+
"finishedAtMs",
|
|
129
|
+
optionalTimestamp(item.finishedAtMs, `${prefix}.finishedAtMs`),
|
|
130
|
+
),
|
|
131
|
+
...optionalProperty("durationMs", optionalDuration(item.durationMs, `${prefix}.durationMs`)),
|
|
132
|
+
...optionalProperty("stopReason", optionalString(item.stopReason, `${prefix}.stopReason`)),
|
|
133
|
+
outcome: enumValue(item.outcome, GENERATION_OUTCOMES, `${prefix}.outcome`) as GenerationOutcome,
|
|
134
|
+
responses: boundedArray(item.responses, `${prefix}.responses`, budget).map(
|
|
135
|
+
parseProviderResponse,
|
|
136
|
+
),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseProviderResponse(value: unknown, index: number): ProviderResponseRecord {
|
|
141
|
+
const item = asRecord(value, `provider response ${index}`);
|
|
142
|
+
return {
|
|
143
|
+
ordinal: boundedCount(item.ordinal, "providerResponse.ordinal"),
|
|
144
|
+
occurredAtMs: timestampValue(item.occurredAtMs, "providerResponse.occurredAtMs"),
|
|
145
|
+
status: boundedInteger(item.status, "providerResponse.status", 999),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parseTool(value: unknown, index: number): ToolCallRecord {
|
|
150
|
+
const item = asRecord(value, `run.tools[${index}]`);
|
|
151
|
+
const prefix = `run.tools[${index}]`;
|
|
152
|
+
const ordinal = boundedCount(item.ordinal, `${prefix}.ordinal`);
|
|
153
|
+
return {
|
|
154
|
+
id: `tool-${ordinal}`,
|
|
155
|
+
ordinal,
|
|
156
|
+
name: requiredString(item.name, `${prefix}.name`),
|
|
157
|
+
...optionalProperty("provider", optionalString(item.provider, `${prefix}.provider`)),
|
|
158
|
+
...optionalProperty("model", optionalString(item.model, `${prefix}.model`)),
|
|
159
|
+
startedAtMs: timestampValue(item.startedAtMs, `${prefix}.startedAtMs`),
|
|
160
|
+
...optionalProperty(
|
|
161
|
+
"finishedAtMs",
|
|
162
|
+
optionalTimestamp(item.finishedAtMs, `${prefix}.finishedAtMs`),
|
|
163
|
+
),
|
|
164
|
+
...optionalProperty("durationMs", optionalDuration(item.durationMs, `${prefix}.durationMs`)),
|
|
165
|
+
isError: booleanValue(item.isError, `${prefix}.isError`),
|
|
166
|
+
completionState: enumValue(
|
|
167
|
+
item.completionState,
|
|
168
|
+
["running", "finished", "interrupted"] as const,
|
|
169
|
+
`${prefix}.completionState`,
|
|
170
|
+
),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseSkill(value: unknown, index: number): SkillActivationRecord {
|
|
175
|
+
const item = asRecord(value, `run.skills[${index}]`);
|
|
176
|
+
const prefix = `run.skills[${index}]`;
|
|
177
|
+
return {
|
|
178
|
+
id: requiredString(item.id, `${prefix}.id`),
|
|
179
|
+
name: requiredString(item.name, `${prefix}.name`),
|
|
180
|
+
initiatedBy: enumValue(item.initiatedBy, ["user", "model"] as const, `${prefix}.initiatedBy`),
|
|
181
|
+
occurredAtMs: timestampValue(item.occurredAtMs, `${prefix}.occurredAtMs`),
|
|
182
|
+
...optionalProperty("provider", optionalString(item.provider, `${prefix}.provider`)),
|
|
183
|
+
...optionalProperty("model", optionalString(item.model, `${prefix}.model`)),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function parseProviderError(value: unknown, index: number): ProviderErrorRecord {
|
|
188
|
+
const item = asRecord(value, `run.providerErrors[${index}]`);
|
|
189
|
+
const prefix = `run.providerErrors[${index}]`;
|
|
190
|
+
return {
|
|
191
|
+
id: requiredString(item.id, `${prefix}.id`),
|
|
192
|
+
...optionalProperty(
|
|
193
|
+
"generationId",
|
|
194
|
+
optionalString(item.generationId, `${prefix}.generationId`),
|
|
195
|
+
),
|
|
196
|
+
occurredAtMs: timestampValue(item.occurredAtMs, `${prefix}.occurredAtMs`),
|
|
197
|
+
...optionalProperty("provider", optionalString(item.provider, `${prefix}.provider`)),
|
|
198
|
+
...optionalProperty("model", optionalString(item.model, `${prefix}.model`)),
|
|
199
|
+
category: enumValue(
|
|
200
|
+
item.category,
|
|
201
|
+
ERROR_CATEGORIES,
|
|
202
|
+
`${prefix}.category`,
|
|
203
|
+
) as ProviderErrorCategory,
|
|
204
|
+
recovered: booleanValue(item.recovered, `${prefix}.recovered`),
|
|
205
|
+
terminal: booleanValue(item.terminal, `${prefix}.terminal`),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function asRecord(value: unknown, name: string): Record<string, unknown> {
|
|
210
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) invalid(name);
|
|
211
|
+
return value as Record<string, unknown>;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function boundedArray(value: unknown, name: string, budget: ParseBudget): unknown[] {
|
|
215
|
+
if (!Array.isArray(value)) invalid(name);
|
|
216
|
+
budget.remaining -= value.length;
|
|
217
|
+
if (budget.remaining < 0) {
|
|
218
|
+
throw new AnalyticsStorageFormatError("Analytics record is too large to process safely.");
|
|
219
|
+
}
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function requiredString(value: unknown, name: string): string {
|
|
224
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_STRING_LENGTH) {
|
|
225
|
+
invalid(name);
|
|
226
|
+
}
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function optionalString(value: unknown, name: string): string | undefined {
|
|
231
|
+
return value === undefined ? undefined : requiredString(value, name);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function boundedInteger(value: unknown, name: string, maximum: number): number {
|
|
235
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > maximum) {
|
|
236
|
+
invalid(name);
|
|
237
|
+
}
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function timestampValue(value: unknown, name: string): number {
|
|
242
|
+
return boundedInteger(value, name, Number.MAX_SAFE_INTEGER);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function optionalTimestamp(value: unknown, name: string): number | undefined {
|
|
246
|
+
return value === undefined ? undefined : timestampValue(value, name);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function durationValue(value: unknown, name: string): number {
|
|
250
|
+
return boundedInteger(value, name, Math.floor(Number.MAX_SAFE_INTEGER / MAX_NESTED_RECORDS));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function optionalDuration(value: unknown, name: string): number | undefined {
|
|
254
|
+
return value === undefined ? undefined : durationValue(value, name);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function boundedCount(value: unknown, name: string): number {
|
|
258
|
+
return boundedInteger(value, name, MAX_NESTED_RECORDS);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function booleanValue(value: unknown, name: string): boolean {
|
|
262
|
+
if (typeof value !== "boolean") invalid(name);
|
|
263
|
+
return value;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function enumValue<const T extends readonly string[]>(
|
|
267
|
+
value: unknown,
|
|
268
|
+
values: T,
|
|
269
|
+
name: string,
|
|
270
|
+
): T[number] {
|
|
271
|
+
if (typeof value !== "string" || !values.includes(value)) invalid(name);
|
|
272
|
+
return value as T[number];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function optionalProperty<K extends string, T>(key: K, value: T | undefined): { [P in K]?: T } {
|
|
276
|
+
return value === undefined ? {} : ({ [key]: value } as { [P in K]?: T });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function invalid(name: string): never {
|
|
280
|
+
throw new AnalyticsStorageFormatError(`Analytics record has an invalid ${name}.`);
|
|
281
|
+
}
|
package/src/storage/queries.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { ProviderErrorCategory } from "../types.js";
|
|
1
|
+
import type { ProviderErrorCategory, SettledRun } from "../types.js";
|
|
3
2
|
|
|
4
3
|
export type TimeRangeId = "today" | "7d" | "30d" | "all";
|
|
5
4
|
export interface TimeRange {
|
|
@@ -92,168 +91,120 @@ export function resolveTimeRange(id: TimeRangeId, now = Date.now()): TimeRange {
|
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
export async function querySnapshot(
|
|
95
|
-
|
|
94
|
+
runs: AsyncIterable<SettledRun> | Iterable<SettledRun>,
|
|
96
95
|
range: TimeRange,
|
|
96
|
+
signal?: AbortSignal,
|
|
97
97
|
): Promise<AnalyticsSnapshot> {
|
|
98
|
-
const [
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
FROM response_runs
|
|
103
|
-
WHERE started_at_ms >= ? AND started_at_ms < ?
|
|
104
|
-
ORDER BY generation_count`,
|
|
105
|
-
range.fromMs,
|
|
106
|
-
range.toMs,
|
|
107
|
-
),
|
|
108
|
-
database.all(
|
|
109
|
-
`SELECT s.skill_name, s.initiated_by, s.provider, s.model,
|
|
110
|
-
COUNT(*) AS count, MAX(s.occurred_at_ms) AS last_at
|
|
111
|
-
FROM skill_activations s
|
|
112
|
-
JOIN response_runs r ON r.id = s.run_id
|
|
113
|
-
WHERE r.started_at_ms >= ? AND r.started_at_ms < ?
|
|
114
|
-
GROUP BY s.skill_name, s.initiated_by, s.provider, s.model`,
|
|
115
|
-
range.fromMs,
|
|
116
|
-
range.toMs,
|
|
117
|
-
),
|
|
118
|
-
database.all(
|
|
119
|
-
`SELECT t.tool_name, t.provider, t.model, COUNT(*) AS count,
|
|
120
|
-
SUM(t.is_error) AS errors, AVG(COALESCE(t.duration_ms, 0)) AS average_duration,
|
|
121
|
-
MAX(t.started_at_ms) AS last_at
|
|
122
|
-
FROM tool_calls t
|
|
123
|
-
JOIN response_runs r ON r.id = t.run_id
|
|
124
|
-
WHERE r.started_at_ms >= ? AND r.started_at_ms < ?
|
|
125
|
-
GROUP BY t.tool_name, t.provider, t.model`,
|
|
126
|
-
range.fromMs,
|
|
127
|
-
range.toMs,
|
|
128
|
-
),
|
|
129
|
-
database.all(
|
|
130
|
-
`SELECT e.category, COUNT(*) AS count, SUM(e.terminal) AS terminal
|
|
131
|
-
FROM provider_errors e
|
|
132
|
-
JOIN response_runs r ON r.id = e.run_id
|
|
133
|
-
WHERE r.started_at_ms >= ? AND r.started_at_ms < ?
|
|
134
|
-
GROUP BY e.category`,
|
|
135
|
-
range.fromMs,
|
|
136
|
-
range.toMs,
|
|
137
|
-
),
|
|
138
|
-
database.all(
|
|
139
|
-
`SELECT p.status, COUNT(*) AS count
|
|
140
|
-
FROM provider_responses p
|
|
141
|
-
JOIN model_generations g ON g.id = p.generation_id
|
|
142
|
-
JOIN response_runs r ON r.id = g.run_id
|
|
143
|
-
WHERE r.started_at_ms >= ? AND r.started_at_ms < ?
|
|
144
|
-
GROUP BY p.status`,
|
|
145
|
-
range.fromMs,
|
|
146
|
-
range.toMs,
|
|
147
|
-
),
|
|
148
|
-
]);
|
|
149
|
-
|
|
150
|
-
const generationCounts = runs.map((row) => numberValue(row.generation_count));
|
|
151
|
-
const responseStats = responseStatistics(generationCounts);
|
|
152
|
-
const overview: OverviewStats = {
|
|
153
|
-
responseCycles: runs.length,
|
|
154
|
-
llmCalls: sum(generationCounts),
|
|
155
|
-
callsPerResponse: responseStats.average,
|
|
156
|
-
p95CallsPerResponse: responseStats.p95,
|
|
157
|
-
toolCalls: sum(runs.map((row) => numberValue(row.tool_call_count))),
|
|
158
|
-
toolErrors: sum(runs.map((row) => numberValue(row.tool_error_count))),
|
|
159
|
-
skillActivations: sum(runs.map((row) => numberValue(row.skill_activation_count))),
|
|
160
|
-
providerErrors: sum(runs.map((row) => numberValue(row.provider_error_count))),
|
|
161
|
-
recoveredErrors: sum(runs.map((row) => numberValue(row.recovered_error_count))),
|
|
162
|
-
};
|
|
163
|
-
|
|
98
|
+
const generationCounts: number[] = [];
|
|
99
|
+
const seenRunIds = new Set<string>();
|
|
100
|
+
const skills = new Map<string, SkillStats>();
|
|
101
|
+
const tools = new Map<string, ToolStats & { totalDurationMs: number }>();
|
|
164
102
|
const categories = Object.fromEntries(
|
|
165
103
|
ERROR_CATEGORIES.map((category) => [category, 0]),
|
|
166
104
|
) as Record<ProviderErrorCategory, number>;
|
|
167
|
-
let
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (category in categories) categories[category] = numberValue(row.count);
|
|
171
|
-
terminal += numberValue(row.terminal);
|
|
172
|
-
}
|
|
105
|
+
let toolErrors = 0;
|
|
106
|
+
let providerErrors = 0;
|
|
107
|
+
let recoveredErrors = 0;
|
|
173
108
|
let http429 = 0;
|
|
174
109
|
let http5xx = 0;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
110
|
+
let terminal = 0;
|
|
111
|
+
|
|
112
|
+
for await (const run of runs) {
|
|
113
|
+
throwIfAborted(signal);
|
|
114
|
+
if (seenRunIds.has(run.id)) continue;
|
|
115
|
+
seenRunIds.add(run.id);
|
|
116
|
+
if (run.startedAtMs < range.fromMs || run.startedAtMs >= range.toMs) continue;
|
|
117
|
+
generationCounts.push(run.generations.length);
|
|
118
|
+
toolErrors += run.toolErrorCount;
|
|
119
|
+
providerErrors += run.providerErrorCount;
|
|
120
|
+
recoveredErrors += run.recoveredErrorCount;
|
|
121
|
+
|
|
122
|
+
for (const skill of run.skills) {
|
|
123
|
+
const item = skills.get(skill.name) ?? {
|
|
124
|
+
name: skill.name,
|
|
125
|
+
count: 0,
|
|
126
|
+
modelInitiated: 0,
|
|
127
|
+
userInitiated: 0,
|
|
128
|
+
lastOccurredAtMs: 0,
|
|
129
|
+
models: [],
|
|
130
|
+
};
|
|
131
|
+
item.count += 1;
|
|
132
|
+
if (skill.initiatedBy === "user") item.userInitiated += 1;
|
|
133
|
+
else item.modelInitiated += 1;
|
|
134
|
+
item.lastOccurredAtMs = Math.max(item.lastOccurredAtMs, skill.occurredAtMs);
|
|
135
|
+
mergeModelCount(item.models, {
|
|
136
|
+
provider: skill.provider,
|
|
137
|
+
model: skill.model,
|
|
138
|
+
count: 1,
|
|
139
|
+
});
|
|
140
|
+
skills.set(skill.name, item);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const tool of run.tools) {
|
|
144
|
+
const item = tools.get(tool.name) ?? {
|
|
145
|
+
name: tool.name,
|
|
146
|
+
count: 0,
|
|
147
|
+
errors: 0,
|
|
148
|
+
averageDurationMs: 0,
|
|
149
|
+
totalDurationMs: 0,
|
|
150
|
+
lastOccurredAtMs: 0,
|
|
151
|
+
models: [],
|
|
152
|
+
};
|
|
153
|
+
item.count += 1;
|
|
154
|
+
item.errors += tool.isError ? 1 : 0;
|
|
155
|
+
item.totalDurationMs += tool.durationMs ?? 0;
|
|
156
|
+
item.averageDurationMs = item.totalDurationMs / item.count;
|
|
157
|
+
item.lastOccurredAtMs = Math.max(item.lastOccurredAtMs, tool.startedAtMs);
|
|
158
|
+
mergeModelCount(item.models, {
|
|
159
|
+
provider: tool.provider,
|
|
160
|
+
model: tool.model,
|
|
161
|
+
count: 1,
|
|
162
|
+
});
|
|
163
|
+
tools.set(tool.name, item);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const error of run.providerErrors) {
|
|
167
|
+
categories[error.category] += 1;
|
|
168
|
+
terminal += error.terminal ? 1 : 0;
|
|
169
|
+
}
|
|
170
|
+
for (const generation of run.generations) {
|
|
171
|
+
for (const response of generation.responses) {
|
|
172
|
+
if (response.status === 429) http429 += 1;
|
|
173
|
+
if (response.status >= 500 && response.status < 600) http5xx += 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
179
176
|
}
|
|
180
177
|
|
|
178
|
+
const responses = responseStatistics(generationCounts);
|
|
181
179
|
return {
|
|
182
|
-
overview
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
overview: {
|
|
181
|
+
responseCycles: responses.count,
|
|
182
|
+
llmCalls: responses.llmCalls,
|
|
183
|
+
callsPerResponse: responses.average,
|
|
184
|
+
p95CallsPerResponse: responses.p95,
|
|
185
|
+
toolCalls: sum([...tools.values()].map(({ count }) => count)),
|
|
186
|
+
toolErrors,
|
|
187
|
+
skillActivations: sum([...skills.values()].map(({ count }) => count)),
|
|
188
|
+
providerErrors,
|
|
189
|
+
recoveredErrors,
|
|
190
|
+
},
|
|
191
|
+
skills: [...skills.values()]
|
|
192
|
+
.map((item) => ({ ...item, models: sortModels(item.models) }))
|
|
193
|
+
.sort((left, right) => right.count - left.count || left.name.localeCompare(right.name)),
|
|
194
|
+
tools: [...tools.values()]
|
|
195
|
+
.map(({ totalDurationMs: _, ...item }) => ({ ...item, models: sortModels(item.models) }))
|
|
196
|
+
.sort((left, right) => right.count - left.count || left.name.localeCompare(right.name)),
|
|
185
197
|
reliability: {
|
|
186
198
|
http429,
|
|
187
199
|
http5xx,
|
|
188
|
-
recovered:
|
|
200
|
+
recovered: recoveredErrors,
|
|
189
201
|
terminal,
|
|
190
202
|
categories,
|
|
191
203
|
},
|
|
192
|
-
responses
|
|
204
|
+
responses,
|
|
193
205
|
};
|
|
194
206
|
}
|
|
195
207
|
|
|
196
|
-
function foldSkills(rows: Array<Record<string, unknown>>): SkillStats[] {
|
|
197
|
-
const result = new Map<string, SkillStats>();
|
|
198
|
-
for (const row of rows) {
|
|
199
|
-
const name = String(row.skill_name);
|
|
200
|
-
const count = numberValue(row.count);
|
|
201
|
-
const item = result.get(name) ?? {
|
|
202
|
-
name,
|
|
203
|
-
count: 0,
|
|
204
|
-
modelInitiated: 0,
|
|
205
|
-
userInitiated: 0,
|
|
206
|
-
lastOccurredAtMs: 0,
|
|
207
|
-
models: [],
|
|
208
|
-
};
|
|
209
|
-
item.count += count;
|
|
210
|
-
if (row.initiated_by === "user") item.userInitiated += count;
|
|
211
|
-
else item.modelInitiated += count;
|
|
212
|
-
item.lastOccurredAtMs = Math.max(item.lastOccurredAtMs, numberValue(row.last_at));
|
|
213
|
-
mergeModelCount(item.models, {
|
|
214
|
-
provider: optionalString(row.provider),
|
|
215
|
-
model: optionalString(row.model),
|
|
216
|
-
count,
|
|
217
|
-
});
|
|
218
|
-
result.set(name, item);
|
|
219
|
-
}
|
|
220
|
-
return [...result.values()]
|
|
221
|
-
.map((item) => ({ ...item, models: sortModels(item.models) }))
|
|
222
|
-
.sort((left, right) => right.count - left.count || left.name.localeCompare(right.name));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function foldTools(rows: Array<Record<string, unknown>>): ToolStats[] {
|
|
226
|
-
const result = new Map<string, ToolStats & { weightedDuration: number }>();
|
|
227
|
-
for (const row of rows) {
|
|
228
|
-
const name = String(row.tool_name);
|
|
229
|
-
const count = numberValue(row.count);
|
|
230
|
-
const average = numberValue(row.average_duration);
|
|
231
|
-
const item = result.get(name) ?? {
|
|
232
|
-
name,
|
|
233
|
-
count: 0,
|
|
234
|
-
errors: 0,
|
|
235
|
-
averageDurationMs: 0,
|
|
236
|
-
weightedDuration: 0,
|
|
237
|
-
lastOccurredAtMs: 0,
|
|
238
|
-
models: [],
|
|
239
|
-
};
|
|
240
|
-
item.count += count;
|
|
241
|
-
item.errors += numberValue(row.errors);
|
|
242
|
-
item.weightedDuration += average * count;
|
|
243
|
-
item.averageDurationMs = item.count > 0 ? item.weightedDuration / item.count : 0;
|
|
244
|
-
item.lastOccurredAtMs = Math.max(item.lastOccurredAtMs, numberValue(row.last_at));
|
|
245
|
-
mergeModelCount(item.models, {
|
|
246
|
-
provider: optionalString(row.provider),
|
|
247
|
-
model: optionalString(row.model),
|
|
248
|
-
count,
|
|
249
|
-
});
|
|
250
|
-
result.set(name, item);
|
|
251
|
-
}
|
|
252
|
-
return [...result.values()]
|
|
253
|
-
.map(({ weightedDuration: _, ...item }) => ({ ...item, models: sortModels(item.models) }))
|
|
254
|
-
.sort((left, right) => right.count - left.count || left.name.localeCompare(right.name));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
208
|
function responseStatistics(generationCounts: number[]): ResponseStats {
|
|
258
209
|
const sorted = [...generationCounts].sort((left, right) => left - right);
|
|
259
210
|
const count = sorted.length;
|
|
@@ -300,12 +251,9 @@ function sortModels(models: ModelCount[]): ModelCount[] {
|
|
|
300
251
|
);
|
|
301
252
|
}
|
|
302
253
|
|
|
303
|
-
function
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
function numberValue(value: unknown): number {
|
|
308
|
-
return typeof value === "number" && Number.isFinite(value) ? value : Number(value) || 0;
|
|
254
|
+
function throwIfAborted(signal?: AbortSignal): void {
|
|
255
|
+
if (signal?.aborted)
|
|
256
|
+
throw signal.reason ?? new DOMException("Analytics query aborted", "AbortError");
|
|
309
257
|
}
|
|
310
258
|
|
|
311
259
|
function sum(values: readonly number[]): number {
|