@obsunified/collector 2.0.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/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +9 -0
- package/dist/auth.js.map +1 -0
- package/dist/chunk-UGWDOGGH.js +319 -0
- package/dist/chunk-UGWDOGGH.js.map +1 -0
- package/dist/chunk-XKQTVIRE.js +671 -0
- package/dist/chunk-XKQTVIRE.js.map +1 -0
- package/dist/index.d.ts +1197 -0
- package/dist/index.js +17707 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest-auth-HFVO1zIp.d.ts +165 -0
- package/dist/openai-FJYOQ7WO.js +384 -0
- package/dist/openai-FJYOQ7WO.js.map +1 -0
- package/package.json +73 -0
- package/src/ai-parity/ai-parity.acceptance.test.ts +449 -0
- package/src/analyses/derive.ts +642 -0
- package/src/analyses/index.ts +50 -0
- package/src/analyses/investigations.ts +277 -0
- package/src/analyses/tier0.ts +568 -0
- package/src/auth/dashboard-auth.ts +269 -0
- package/src/auth/ingest-auth.ts +147 -0
- package/src/durable-objects/tail-hub.test.ts +32 -0
- package/src/durable-objects/tail-hub.ts +158 -0
- package/src/framework/collector.ts +467 -0
- package/src/framework/env.ts +90 -0
- package/src/framework/logger.ts +81 -0
- package/src/index.ts +211 -0
- package/src/lib/action-aggregates.ts +1031 -0
- package/src/lib/action-identity.ts +106 -0
- package/src/lib/actions-store.ts +397 -0
- package/src/lib/ai-pricing.ts +80 -0
- package/src/lib/ai-store/calls.ts +148 -0
- package/src/lib/ai-store/evaluations.test.ts +47 -0
- package/src/lib/ai-store/evaluations.ts +125 -0
- package/src/lib/ai-store/helpers.ts +37 -0
- package/src/lib/ai-store/retention.ts +17 -0
- package/src/lib/ai-store/sessions.ts +277 -0
- package/src/lib/ai-store/spans.ts +109 -0
- package/src/lib/ai-store/types.ts +112 -0
- package/src/lib/ai-store.ts +76 -0
- package/src/lib/alerts-store.ts +458 -0
- package/src/lib/analyses-runner.ts +439 -0
- package/src/lib/analyses-store.test.ts +243 -0
- package/src/lib/analyses-store.ts +454 -0
- package/src/lib/ask.test.ts +302 -0
- package/src/lib/ask.ts +390 -0
- package/src/lib/blob-store-s3.ts +213 -0
- package/src/lib/blob-store.ts +366 -0
- package/src/lib/eval-cases-store.test.ts +171 -0
- package/src/lib/eval-cases-store.ts +1033 -0
- package/src/lib/evidence-references.test.ts +10 -0
- package/src/lib/evidence-references.ts +684 -0
- package/src/lib/hash.ts +26 -0
- package/src/lib/identity-index/action-lookups.ts +805 -0
- package/src/lib/identity-index/constants.ts +1 -0
- package/src/lib/identity-index/key-lookups.ts +199 -0
- package/src/lib/identity-index/mappers.ts +349 -0
- package/src/lib/identity-index/types.ts +196 -0
- package/src/lib/identity-index/user-lookup.ts +137 -0
- package/src/lib/identity-index.test.ts +624 -0
- package/src/lib/identity-index.ts +85 -0
- package/src/lib/json.ts +39 -0
- package/src/lib/llm.ts +263 -0
- package/src/lib/logs-store.ts +188 -0
- package/src/lib/metrics-store.test.ts +189 -0
- package/src/lib/metrics-store.ts +312 -0
- package/src/lib/narrate-gate.test.ts +260 -0
- package/src/lib/narrate-gate.ts +253 -0
- package/src/lib/openai.test.ts +300 -0
- package/src/lib/openai.ts +512 -0
- package/src/lib/otlp.ts +212 -0
- package/src/lib/parse-pprof.test.ts +64 -0
- package/src/lib/parse-pprof.ts +184 -0
- package/src/lib/payload-capture.ts +22 -0
- package/src/lib/projects-store.ts +207 -0
- package/src/lib/propagation-metric.test.ts +196 -0
- package/src/lib/propagation-metric.ts +175 -0
- package/src/lib/sql-db-dialect.test.ts +22 -0
- package/src/lib/sql-db-postgres.ts +229 -0
- package/src/lib/sql-db.ts +166 -0
- package/src/lib/stage6.test.ts +216 -0
- package/src/lib/store/export.ts +65 -0
- package/src/lib/store/helpers.ts +367 -0
- package/src/lib/store/ingest.ts +68 -0
- package/src/lib/store/issues.ts +158 -0
- package/src/lib/store/overview.ts +127 -0
- package/src/lib/store/retention.ts +10 -0
- package/src/lib/store/service-map.ts +384 -0
- package/src/lib/store/trace-detail.bench.ts +76 -0
- package/src/lib/store/trace-detail.test.ts +410 -0
- package/src/lib/store/trace-detail.ts +427 -0
- package/src/lib/store/trace-queries.test.ts +29 -0
- package/src/lib/store/trace-queries.ts +89 -0
- package/src/lib/store-overview.test.ts +77 -0
- package/src/lib/store.ts +96 -0
- package/src/lib/tail-publisher.ts +64 -0
- package/src/lib/test-utils/mem-sql-db.test.ts +93 -0
- package/src/lib/test-utils/mem-sql-db.ts +86 -0
- package/src/lib/ua-parser.ts +61 -0
- package/src/lib/usage-store.ts +567 -0
- package/src/migrations/001_telemetry_spans.sql +28 -0
- package/src/migrations/002_usage_events.sql +24 -0
- package/src/migrations/003_usage_analytics_columns.sql +13 -0
- package/src/migrations/004_logs.sql +19 -0
- package/src/migrations/005_ai_calls.sql +25 -0
- package/src/migrations/006_user_profiles.sql +11 -0
- package/src/migrations/007_session_replay_metadata.sql +10 -0
- package/src/migrations/008_session_replay_storage_bytes.sql +1 -0
- package/src/migrations/009_projects_and_keys.sql +22 -0
- package/src/migrations/010_signal_project_id.sql +15 -0
- package/src/migrations/011_default_project_seed.sql +3 -0
- package/src/migrations/012_alert_rules.sql +17 -0
- package/src/migrations/013_alert_evaluations.sql +13 -0
- package/src/migrations/014_alert_state.sql +9 -0
- package/src/migrations/015_metrics.sql +58 -0
- package/src/migrations/016_metrics_exp_histogram_summary.sql +5 -0
- package/src/migrations/017_span_dropped_counts.sql +11 -0
- package/src/migrations/018_log_flags.sql +8 -0
- package/src/migrations/019_ai_span_payloads.sql +30 -0
- package/src/migrations/020_ai_span_evaluations.sql +33 -0
- package/src/migrations/021_ai_span_session.sql +13 -0
- package/src/migrations/022_span_log_session_id.sql +13 -0
- package/src/migrations/023_service_map_indexes.sql +32 -0
- package/src/migrations/024_analysis_results.sql +49 -0
- package/src/migrations/025_stage6_autopinning_alerts.sql +38 -0
- package/src/migrations/026_obs_dashboard_project_seed.sql +18 -0
- package/src/migrations/027_identity_propagation.sql +50 -0
- package/src/migrations/028_profile_blobs.sql +57 -0
- package/src/migrations/029_telemetry_sdk_name.sql +20 -0
- package/src/migrations/030_analysis_claim.sql +18 -0
- package/src/migrations/031_agent_action_graph.sql +154 -0
- package/src/migrations/032_payload_capture_flags.sql +8 -0
- package/src/migrations/033_eval_cases.sql +52 -0
- package/src/migrations/034_eval_case_results.sql +23 -0
- package/src/migrations/035_metric_exemplars.sql +33 -0
- package/src/migrations/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations/037_eval_runs.sql +37 -0
- package/src/migrations/038_tool_call_evidence.sql +11 -0
- package/src/migrations/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations/040_evidence_retrieval_refs.sql +50 -0
- package/src/migrations-postgres/001_telemetry_spans.sql +33 -0
- package/src/migrations-postgres/002_usage_events.sql +24 -0
- package/src/migrations-postgres/003_usage_analytics_columns.sql +13 -0
- package/src/migrations-postgres/004_logs.sql +19 -0
- package/src/migrations-postgres/005_ai_calls.sql +25 -0
- package/src/migrations-postgres/006_user_profiles.sql +11 -0
- package/src/migrations-postgres/007_session_replay_metadata.sql +10 -0
- package/src/migrations-postgres/008_session_replay_storage_bytes.sql +2 -0
- package/src/migrations-postgres/009_projects_and_keys.sql +25 -0
- package/src/migrations-postgres/010_signal_project_id.sql +18 -0
- package/src/migrations-postgres/011_default_project_seed.sql +5 -0
- package/src/migrations-postgres/012_alert_rules.sql +17 -0
- package/src/migrations-postgres/013_alert_evaluations.sql +13 -0
- package/src/migrations-postgres/014_alert_state.sql +9 -0
- package/src/migrations-postgres/015_metrics.sql +47 -0
- package/src/migrations-postgres/016_metrics_exp_histogram_summary.sql +1 -0
- package/src/migrations-postgres/017_span_dropped_counts.sql +4 -0
- package/src/migrations-postgres/018_log_flags.sql +2 -0
- package/src/migrations-postgres/019_ai_span_payloads.sql +18 -0
- package/src/migrations-postgres/020_ai_span_evaluations.sql +21 -0
- package/src/migrations-postgres/021_ai_span_session.sql +7 -0
- package/src/migrations-postgres/022_span_log_session_id.sql +7 -0
- package/src/migrations-postgres/023_service_map_indexes.sql +9 -0
- package/src/migrations-postgres/024_analysis_results.sql +41 -0
- package/src/migrations-postgres/025_stage6_autopinning_alerts.sql +17 -0
- package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +14 -0
- package/src/migrations-postgres/027_identity_propagation.sql +27 -0
- package/src/migrations-postgres/028_profile_blobs.sql +48 -0
- package/src/migrations-postgres/029_telemetry_sdk_name.sql +5 -0
- package/src/migrations-postgres/030_analysis_claim.sql +5 -0
- package/src/migrations-postgres/031_agent_action_graph.sql +190 -0
- package/src/migrations-postgres/032_payload_capture_flags.sql +8 -0
- package/src/migrations-postgres/033_eval_cases.sql +55 -0
- package/src/migrations-postgres/034_eval_case_results.sql +26 -0
- package/src/migrations-postgres/035_metric_exemplars.sql +31 -0
- package/src/migrations-postgres/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations-postgres/037_eval_runs.sql +40 -0
- package/src/migrations-postgres/038_tool_call_evidence.sql +11 -0
- package/src/migrations-postgres/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations-postgres/040_evidence_retrieval_refs.sql +47 -0
- package/src/migrations-postgres/README.md +40 -0
- package/src/otlp/acceptance.test.ts +373 -0
- package/src/otlp/decode/body.ts +127 -0
- package/src/otlp/decode/logs.ts +96 -0
- package/src/otlp/decode/metrics.ts +402 -0
- package/src/otlp/decode/traces.ts +92 -0
- package/src/otlp/decode/values.ts +150 -0
- package/src/otlp/decode.test.ts +800 -0
- package/src/otlp/decode.ts +11 -0
- package/src/otlp/gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.ts +132 -0
- package/src/otlp/gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/common/v1/common_pb.ts +289 -0
- package/src/otlp/gen/opentelemetry/proto/logs/v1/logs_pb.ts +397 -0
- package/src/otlp/gen/opentelemetry/proto/metrics/v1/metrics_pb.ts +1329 -0
- package/src/otlp/gen/opentelemetry/proto/resource/v1/resource_pb.ts +86 -0
- package/src/otlp/gen/opentelemetry/proto/trace/v1/trace_pb.ts +710 -0
- package/src/otlp/response.test.ts +158 -0
- package/src/otlp/response.ts +129 -0
- package/src/plugins/_context.ts +15 -0
- package/src/plugins/action-graph-processor/enrichers.ts +45 -0
- package/src/plugins/action-graph-processor/redaction.ts +84 -0
- package/src/plugins/action-graph-processor.test.ts +1023 -0
- package/src/plugins/action-graph-processor.ts +1172 -0
- package/src/plugins/action-routes.test.ts +716 -0
- package/src/plugins/action-routes.ts +343 -0
- package/src/plugins/ai-receiver.ts +254 -0
- package/src/plugins/ai-span-payloads-processor.test.ts +90 -0
- package/src/plugins/ai-span-payloads-processor.ts +180 -0
- package/src/plugins/alerts-evaluator.test.ts +91 -0
- package/src/plugins/alerts-evaluator.ts +328 -0
- package/src/plugins/alerts-routes.ts +101 -0
- package/src/plugins/analyses-routes.ts +138 -0
- package/src/plugins/ask-routes.ts +182 -0
- package/src/plugins/bot-filter.ts +19 -0
- package/src/plugins/connected-routes/manifest.ts +531 -0
- package/src/plugins/connected-routes.test.ts +1791 -0
- package/src/plugins/connected-routes.ts +1103 -0
- package/src/plugins/dashboard-routes.ts +41 -0
- package/src/plugins/default-span-enrichment.ts +92 -0
- package/src/plugins/eval-cases-routes.test.ts +391 -0
- package/src/plugins/eval-cases-routes.ts +529 -0
- package/src/plugins/evidence-retrieval-routes.benchmark.test.ts +245 -0
- package/src/plugins/evidence-retrieval-routes.test.ts +932 -0
- package/src/plugins/evidence-retrieval-routes.ts +2179 -0
- package/src/plugins/gen-ai-normalizer.test.ts +680 -0
- package/src/plugins/gen-ai-normalizer.ts +302 -0
- package/src/plugins/identity-receiver.ts +96 -0
- package/src/plugins/issue-enrichment.ts +132 -0
- package/src/plugins/issue-insights.ts +81 -0
- package/src/plugins/logs-receiver.ts +160 -0
- package/src/plugins/metrics-receiver.ts +91 -0
- package/src/plugins/onboarding-routes.ts +54 -0
- package/src/plugins/otlp-receiver.ts +124 -0
- package/src/plugins/platform-routes.ts +165 -0
- package/src/plugins/profile-routes.ts +468 -0
- package/src/plugins/projects-routes.ts +92 -0
- package/src/plugins/query-routes.ts +220 -0
- package/src/plugins/redaction-processor.ts +132 -0
- package/src/plugins/replay-query-routes.test.ts +38 -0
- package/src/plugins/replay-query-routes.ts +199 -0
- package/src/plugins/replay-receiver.ts +81 -0
- package/src/plugins/tail-routes.test.ts +47 -0
- package/src/plugins/tail-routes.ts +42 -0
- package/src/plugins/timeline-routes.ts +244 -0
- package/src/plugins/ua-enrichment.ts +27 -0
- package/src/plugins/usage-normalization.ts +46 -0
- package/src/plugins/usage-privacy.ts +55 -0
- package/src/plugins/usage-query-routes.ts +102 -0
- package/src/plugins/usage-receiver.ts +129 -0
- package/src/plugins/users-query-routes.ts +78 -0
- package/src/plugins/utm-enrichment.ts +36 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AICallRecord,
|
|
3
|
+
AICallsOverviewOptions,
|
|
4
|
+
AICallsOverviewResponse,
|
|
5
|
+
} from "@obsunified/types";
|
|
6
|
+
import { dialectFor, type SqlDb } from "../sql-db";
|
|
7
|
+
import type { AICallRow, AICallSummaryRow } from "./types";
|
|
8
|
+
import { clampInt } from "./types";
|
|
9
|
+
|
|
10
|
+
export async function ingestAICallBatch(
|
|
11
|
+
db: SqlDb,
|
|
12
|
+
calls: AICallRecord[],
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
if (calls.length === 0) return;
|
|
15
|
+
|
|
16
|
+
const stmt = db.prepare(`
|
|
17
|
+
INSERT OR IGNORE INTO ai_calls (
|
|
18
|
+
project_id, call_id, trace_id, span_id, service_name, model_name, provider, call_type,
|
|
19
|
+
request_json, response_json, prompt_tokens, completion_tokens, total_cost_usd,
|
|
20
|
+
latency_ms, is_error, error_message, occurred_at, received_at, expires_at,
|
|
21
|
+
session_id, interaction_id
|
|
22
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
23
|
+
`);
|
|
24
|
+
|
|
25
|
+
const batch = calls.map((call) => {
|
|
26
|
+
if (!call.projectId)
|
|
27
|
+
throw new Error("AIStore.ingestBatch: call.projectId is required");
|
|
28
|
+
return stmt.bind(
|
|
29
|
+
call.projectId,
|
|
30
|
+
call.callId,
|
|
31
|
+
call.traceId,
|
|
32
|
+
call.spanId,
|
|
33
|
+
call.serviceName,
|
|
34
|
+
call.modelName,
|
|
35
|
+
call.provider,
|
|
36
|
+
call.callType,
|
|
37
|
+
call.requestJson,
|
|
38
|
+
call.responseJson,
|
|
39
|
+
call.promptTokens,
|
|
40
|
+
call.completionTokens,
|
|
41
|
+
call.totalCostUsd,
|
|
42
|
+
call.latencyMs,
|
|
43
|
+
call.isError ? 1 : 0,
|
|
44
|
+
call.errorMessage,
|
|
45
|
+
call.occurredAt,
|
|
46
|
+
call.receivedAt,
|
|
47
|
+
call.expiresAt,
|
|
48
|
+
call.sessionId ?? null,
|
|
49
|
+
call.interactionId ?? null,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await db.batch(batch);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function getAICallsOverview(
|
|
57
|
+
db: SqlDb,
|
|
58
|
+
options: AICallsOverviewOptions,
|
|
59
|
+
): Promise<AICallsOverviewResponse> {
|
|
60
|
+
if (!options.projectId)
|
|
61
|
+
throw new Error("AIStore.getAICalls: projectId is required");
|
|
62
|
+
const dialect = dialectFor(db);
|
|
63
|
+
const hours = clampInt(options.hours, 1, 720, 24);
|
|
64
|
+
const limit = clampInt(options.limit, 1, 1000, 100);
|
|
65
|
+
|
|
66
|
+
let sql = `SELECT * FROM ai_calls WHERE project_id = ? AND received_at >= ${dialect.sinceHours("?")}`;
|
|
67
|
+
const params: unknown[] = [options.projectId, hours];
|
|
68
|
+
|
|
69
|
+
if (options.service) {
|
|
70
|
+
sql += ` AND service_name = ?`;
|
|
71
|
+
params.push(options.service);
|
|
72
|
+
}
|
|
73
|
+
if (options.model) {
|
|
74
|
+
sql += ` AND model_name = ?`;
|
|
75
|
+
params.push(options.model);
|
|
76
|
+
}
|
|
77
|
+
if (options.isError !== undefined) {
|
|
78
|
+
sql += ` AND is_error = ?`;
|
|
79
|
+
params.push(options.isError ? 1 : 0);
|
|
80
|
+
}
|
|
81
|
+
if (options.traceId) {
|
|
82
|
+
sql += ` AND trace_id = ?`;
|
|
83
|
+
params.push(options.traceId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
sql += ` ORDER BY received_at DESC LIMIT ?`;
|
|
87
|
+
params.push(limit);
|
|
88
|
+
|
|
89
|
+
const results = await db
|
|
90
|
+
.prepare(sql)
|
|
91
|
+
.bind(...params)
|
|
92
|
+
.all<AICallRow>();
|
|
93
|
+
|
|
94
|
+
const summarySql = `
|
|
95
|
+
SELECT
|
|
96
|
+
COUNT(*) as totalCalls,
|
|
97
|
+
SUM(total_cost_usd) as totalCostUsd,
|
|
98
|
+
SUM(prompt_tokens) as totalPromptTokens,
|
|
99
|
+
SUM(completion_tokens) as totalCompletionTokens,
|
|
100
|
+
SUM(is_error) as errorCalls
|
|
101
|
+
FROM ai_calls WHERE project_id = ? AND received_at >= ${dialect.sinceHours("?")}
|
|
102
|
+
${options.service ? "AND service_name = ?" : ""}
|
|
103
|
+
`;
|
|
104
|
+
const summaryParams: unknown[] = [options.projectId, hours];
|
|
105
|
+
if (options.service) summaryParams.push(options.service);
|
|
106
|
+
const summaryResult =
|
|
107
|
+
(await db
|
|
108
|
+
.prepare(summarySql)
|
|
109
|
+
.bind(...summaryParams)
|
|
110
|
+
.first<AICallSummaryRow>()) || {};
|
|
111
|
+
|
|
112
|
+
const calls: AICallRecord[] = (results.results || []).map((row) => ({
|
|
113
|
+
projectId: row.project_id ?? "default",
|
|
114
|
+
callId: row.call_id,
|
|
115
|
+
traceId: row.trace_id,
|
|
116
|
+
spanId: row.span_id,
|
|
117
|
+
serviceName: row.service_name,
|
|
118
|
+
modelName: row.model_name,
|
|
119
|
+
provider: row.provider,
|
|
120
|
+
callType: row.call_type,
|
|
121
|
+
requestJson: row.request_json,
|
|
122
|
+
responseJson: row.response_json,
|
|
123
|
+
promptTokens: row.prompt_tokens,
|
|
124
|
+
completionTokens: row.completion_tokens,
|
|
125
|
+
totalCostUsd: row.total_cost_usd,
|
|
126
|
+
latencyMs: row.latency_ms,
|
|
127
|
+
isError: row.is_error === 1,
|
|
128
|
+
errorMessage: row.error_message,
|
|
129
|
+
occurredAt: row.occurred_at,
|
|
130
|
+
receivedAt: row.received_at,
|
|
131
|
+
expiresAt: row.expires_at,
|
|
132
|
+
sessionId: row.session_id ?? null,
|
|
133
|
+
interactionId: row.interaction_id ?? null,
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
calls,
|
|
138
|
+
summary: {
|
|
139
|
+
totalCalls: summaryResult.totalCalls || 0,
|
|
140
|
+
totalCostUsd: summaryResult.totalCostUsd || 0,
|
|
141
|
+
totalPromptTokens: summaryResult.totalPromptTokens || 0,
|
|
142
|
+
totalCompletionTokens: summaryResult.totalCompletionTokens || 0,
|
|
143
|
+
errorCalls: summaryResult.errorCalls || 0,
|
|
144
|
+
},
|
|
145
|
+
windowHours: hours,
|
|
146
|
+
timestamp: new Date().toISOString(),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { mapEvaluationRows } from "./evaluations";
|
|
3
|
+
|
|
4
|
+
describe("mapEvaluationRows", () => {
|
|
5
|
+
it("adds evidence references that pivot from AI evaluation to span and trace", () => {
|
|
6
|
+
const [evaluation] = mapEvaluationRows([
|
|
7
|
+
{
|
|
8
|
+
evaluation_id: "eval-1",
|
|
9
|
+
project_id: "default",
|
|
10
|
+
trace_id: "trace-1",
|
|
11
|
+
span_id: "span-1",
|
|
12
|
+
name: "groundedness",
|
|
13
|
+
score: 0.2,
|
|
14
|
+
label: "fail",
|
|
15
|
+
explanation: "unsupported answer",
|
|
16
|
+
source: "llm_judge",
|
|
17
|
+
metadata_json: "{}",
|
|
18
|
+
created_at: "2026-05-01T00:00:00.000Z",
|
|
19
|
+
expires_at: "2026-05-02T00:00:00.000Z",
|
|
20
|
+
},
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
expect(evaluation.evidenceReferences).toEqual([
|
|
24
|
+
expect.objectContaining({
|
|
25
|
+
evidenceId: "ai-evaluation:eval-1",
|
|
26
|
+
entityKind: "eval",
|
|
27
|
+
entityId: "eval-1",
|
|
28
|
+
route: "#/evals/eval-1",
|
|
29
|
+
source: "ai_span_evaluations.llm_judge",
|
|
30
|
+
citations: [
|
|
31
|
+
{
|
|
32
|
+
label: "span span-1",
|
|
33
|
+
entityKind: "span",
|
|
34
|
+
entityId: "trace-1:span-1",
|
|
35
|
+
route: "#/traces/trace-1#span=span-1",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: "trace trace-1",
|
|
39
|
+
entityKind: "trace",
|
|
40
|
+
entityId: "trace-1",
|
|
41
|
+
route: "#/traces/trace-1",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
}),
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AIEvaluationRecord,
|
|
3
|
+
AIEvaluationSource,
|
|
4
|
+
AIEvaluationsListOptions,
|
|
5
|
+
AIEvaluationsListResponse,
|
|
6
|
+
} from "@obsunified/types";
|
|
7
|
+
import { EVIDENCE_REFERENCE_CONTRACT } from "@obsunified/types";
|
|
8
|
+
import { aiEvaluationEvidenceReferences } from "../evidence-references";
|
|
9
|
+
import { parseJsonRecord } from "../json";
|
|
10
|
+
import type { SqlDb } from "../sql-db";
|
|
11
|
+
import type { AIEvaluationRow, IngestEvaluation } from "./types";
|
|
12
|
+
import { clampInt } from "./types";
|
|
13
|
+
|
|
14
|
+
export async function ingestAIEvaluations(
|
|
15
|
+
db: SqlDb,
|
|
16
|
+
evaluations: IngestEvaluation[],
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
if (evaluations.length === 0) return;
|
|
19
|
+
const stmt = db.prepare(`
|
|
20
|
+
INSERT INTO ai_span_evaluations (
|
|
21
|
+
evaluation_id, project_id, trace_id, span_id, name,
|
|
22
|
+
score, label, explanation, source, metadata_json,
|
|
23
|
+
created_at, expires_at
|
|
24
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
25
|
+
`);
|
|
26
|
+
await db.batch(
|
|
27
|
+
evaluations.map((evaluation) =>
|
|
28
|
+
stmt.bind(
|
|
29
|
+
evaluation.evaluationId,
|
|
30
|
+
evaluation.projectId,
|
|
31
|
+
evaluation.traceId,
|
|
32
|
+
evaluation.spanId,
|
|
33
|
+
evaluation.name,
|
|
34
|
+
evaluation.score,
|
|
35
|
+
evaluation.label,
|
|
36
|
+
evaluation.explanation,
|
|
37
|
+
evaluation.source,
|
|
38
|
+
evaluation.metadataJson,
|
|
39
|
+
evaluation.createdAt,
|
|
40
|
+
evaluation.expiresAt,
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function listAIEvaluations(
|
|
47
|
+
db: SqlDb,
|
|
48
|
+
options: AIEvaluationsListOptions,
|
|
49
|
+
): Promise<AIEvaluationsListResponse> {
|
|
50
|
+
if (!options.projectId)
|
|
51
|
+
throw new Error("AIStore.listEvaluations: projectId is required");
|
|
52
|
+
const limit = clampInt(options.limit, 1, 1000, 200);
|
|
53
|
+
|
|
54
|
+
let sql = `SELECT * FROM ai_span_evaluations WHERE project_id = ?`;
|
|
55
|
+
const params: unknown[] = [options.projectId];
|
|
56
|
+
|
|
57
|
+
if (options.traceId) {
|
|
58
|
+
sql += ` AND trace_id = ?`;
|
|
59
|
+
params.push(options.traceId);
|
|
60
|
+
}
|
|
61
|
+
if (options.spanId) {
|
|
62
|
+
sql += ` AND span_id = ?`;
|
|
63
|
+
params.push(options.spanId);
|
|
64
|
+
}
|
|
65
|
+
if (options.name) {
|
|
66
|
+
sql += ` AND name = ?`;
|
|
67
|
+
params.push(options.name);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
sql += ` ORDER BY created_at DESC LIMIT ?`;
|
|
71
|
+
params.push(limit);
|
|
72
|
+
|
|
73
|
+
const results = await db
|
|
74
|
+
.prepare(sql)
|
|
75
|
+
.bind(...params)
|
|
76
|
+
.all<AIEvaluationRow>();
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
evaluations: mapEvaluationRows(results.results || []),
|
|
80
|
+
timestamp: new Date().toISOString(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function getAIEvaluation(
|
|
85
|
+
db: SqlDb,
|
|
86
|
+
projectId: string,
|
|
87
|
+
evaluationId: string,
|
|
88
|
+
): Promise<AIEvaluationRecord | null> {
|
|
89
|
+
if (!projectId)
|
|
90
|
+
throw new Error("AIStore.getEvaluation: projectId is required");
|
|
91
|
+
const row = await db
|
|
92
|
+
.prepare(
|
|
93
|
+
`SELECT * FROM ai_span_evaluations
|
|
94
|
+
WHERE project_id = ? AND evaluation_id = ?
|
|
95
|
+
LIMIT 1`,
|
|
96
|
+
)
|
|
97
|
+
.bind(projectId, evaluationId)
|
|
98
|
+
.first<AIEvaluationRow>();
|
|
99
|
+
|
|
100
|
+
return row ? mapEvaluationRows([row])[0] : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function mapEvaluationRows(
|
|
104
|
+
rows: AIEvaluationRow[],
|
|
105
|
+
): AIEvaluationRecord[] {
|
|
106
|
+
return rows.map((row) => {
|
|
107
|
+
const evaluation: AIEvaluationRecord = {
|
|
108
|
+
evaluationId: row.evaluation_id,
|
|
109
|
+
projectId: row.project_id,
|
|
110
|
+
traceId: row.trace_id,
|
|
111
|
+
spanId: row.span_id,
|
|
112
|
+
name: row.name,
|
|
113
|
+
score: row.score,
|
|
114
|
+
label: row.label,
|
|
115
|
+
explanation: row.explanation,
|
|
116
|
+
source: row.source as AIEvaluationSource,
|
|
117
|
+
metadata: parseJsonRecord(row.metadata_json),
|
|
118
|
+
createdAt: row.created_at,
|
|
119
|
+
expiresAt: row.expires_at,
|
|
120
|
+
};
|
|
121
|
+
evaluation.evidenceReferences = aiEvaluationEvidenceReferences(evaluation);
|
|
122
|
+
evaluation.evidenceContract = EVIDENCE_REFERENCE_CONTRACT;
|
|
123
|
+
return evaluation;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { JsonValue } from "@obsunified/types";
|
|
2
|
+
import { computeCost } from "../ai-pricing";
|
|
3
|
+
|
|
4
|
+
export const attrNum = (
|
|
5
|
+
attrs: Record<string, JsonValue>,
|
|
6
|
+
key: string,
|
|
7
|
+
): number | null => {
|
|
8
|
+
const v = attrs[key];
|
|
9
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const attrStr = (
|
|
13
|
+
attrs: Record<string, JsonValue>,
|
|
14
|
+
key: string,
|
|
15
|
+
): string | null => {
|
|
16
|
+
const v = attrs[key];
|
|
17
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Enrich a span's attributes with a computed `llm.cost.total_usd` when the
|
|
22
|
+
* span has token counts but no reported cost. Mutates the passed attrs
|
|
23
|
+
* object and returns the final cost (or null).
|
|
24
|
+
*/
|
|
25
|
+
export const enrichCost = (attrs: Record<string, JsonValue>): number | null => {
|
|
26
|
+
const existing = attrNum(attrs, "llm.cost.total_usd");
|
|
27
|
+
if (existing !== null) return existing;
|
|
28
|
+
const model = attrStr(attrs, "llm.model_name");
|
|
29
|
+
const prompt = attrNum(attrs, "llm.token_count.prompt");
|
|
30
|
+
const completion = attrNum(attrs, "llm.token_count.completion");
|
|
31
|
+
if (prompt === null && completion === null) return null;
|
|
32
|
+
const cost = computeCost(model, prompt, completion);
|
|
33
|
+
if (cost === null) return null;
|
|
34
|
+
attrs["llm.cost.total_usd"] = cost;
|
|
35
|
+
attrs["llm.cost.computed"] = true;
|
|
36
|
+
return cost;
|
|
37
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { dialectFor, type SqlDb } from "../sql-db";
|
|
2
|
+
|
|
3
|
+
export async function purgeExpiredAIData(db: SqlDb): Promise<number> {
|
|
4
|
+
const dialect = dialectFor(db);
|
|
5
|
+
const { meta } = await db
|
|
6
|
+
.prepare(`DELETE FROM ai_calls WHERE expires_at < ${dialect.now()}`)
|
|
7
|
+
.run();
|
|
8
|
+
const { meta: payloadMeta } = await db
|
|
9
|
+
.prepare(`DELETE FROM ai_span_payloads WHERE expires_at < ${dialect.now()}`)
|
|
10
|
+
.run();
|
|
11
|
+
const { meta: evalMeta } = await db
|
|
12
|
+
.prepare(
|
|
13
|
+
`DELETE FROM ai_span_evaluations WHERE expires_at < ${dialect.now()}`,
|
|
14
|
+
)
|
|
15
|
+
.run();
|
|
16
|
+
return meta.changes + (payloadMeta?.changes ?? 0) + (evalMeta?.changes ?? 0);
|
|
17
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AISessionDetailResponse,
|
|
3
|
+
AISessionSummary,
|
|
4
|
+
AISessionsListOptions,
|
|
5
|
+
AISessionsListResponse,
|
|
6
|
+
AISpanRecord,
|
|
7
|
+
JsonValue,
|
|
8
|
+
} from "@obsunified/types";
|
|
9
|
+
import { parseJsonRecord } from "../json";
|
|
10
|
+
import { dialectFor, type SqlDb } from "../sql-db";
|
|
11
|
+
import { mapEvaluationRows } from "./evaluations";
|
|
12
|
+
import { attrNum, enrichCost } from "./helpers";
|
|
13
|
+
import type {
|
|
14
|
+
AIEvaluationRow,
|
|
15
|
+
AISessionPreviewRow,
|
|
16
|
+
AISessionRow,
|
|
17
|
+
AISpanRow,
|
|
18
|
+
} from "./types";
|
|
19
|
+
import { clampInt } from "./types";
|
|
20
|
+
|
|
21
|
+
export async function listAISessions(
|
|
22
|
+
db: SqlDb,
|
|
23
|
+
options: AISessionsListOptions,
|
|
24
|
+
): Promise<AISessionsListResponse> {
|
|
25
|
+
if (!options.projectId)
|
|
26
|
+
throw new Error("AIStore.listSessions: projectId is required");
|
|
27
|
+
const dialect = dialectFor(db);
|
|
28
|
+
const hours = clampInt(options.hours, 1, 720, 24);
|
|
29
|
+
const limit = clampInt(options.limit, 1, 1000, 100);
|
|
30
|
+
|
|
31
|
+
let sql = `
|
|
32
|
+
SELECT
|
|
33
|
+
p.session_id AS session_id,
|
|
34
|
+
MAX(p.user_id) AS user_id,
|
|
35
|
+
COUNT(*) AS span_count,
|
|
36
|
+
SUM(CASE WHEN p.span_kind = 'LLM' THEN 1 ELSE 0 END) AS llm_span_count,
|
|
37
|
+
SUM(CASE WHEN s.status_code = 2 THEN 1 ELSE 0 END) AS error_count,
|
|
38
|
+
SUM(CAST(${dialect.jsonText("s.attributes_json", '$."llm.token_count.prompt"')} AS REAL)) AS prompt_tokens,
|
|
39
|
+
SUM(CAST(${dialect.jsonText("s.attributes_json", '$."llm.token_count.completion"')} AS REAL)) AS completion_tokens,
|
|
40
|
+
SUM(CAST(${dialect.jsonText("s.attributes_json", '$."llm.cost.total_usd"')} AS REAL)) AS cost_usd,
|
|
41
|
+
MIN(s.start_time) AS first_span_at,
|
|
42
|
+
MAX(s.start_time) AS last_span_at,
|
|
43
|
+
COUNT(DISTINCT s.trace_id) AS trace_count
|
|
44
|
+
FROM ai_span_payloads p
|
|
45
|
+
INNER JOIN telemetry_spans s
|
|
46
|
+
ON s.trace_id = p.trace_id AND s.span_id = p.span_id
|
|
47
|
+
WHERE p.project_id = ?
|
|
48
|
+
AND p.session_id IS NOT NULL
|
|
49
|
+
AND p.received_at >= ${dialect.sinceHours("?")}
|
|
50
|
+
`;
|
|
51
|
+
const params: unknown[] = [options.projectId, hours];
|
|
52
|
+
if (options.userId) {
|
|
53
|
+
sql += ` AND p.user_id = ?`;
|
|
54
|
+
params.push(options.userId);
|
|
55
|
+
}
|
|
56
|
+
sql += ` GROUP BY p.session_id ORDER BY last_span_at DESC LIMIT ?`;
|
|
57
|
+
params.push(limit);
|
|
58
|
+
|
|
59
|
+
const results = await db
|
|
60
|
+
.prepare(sql)
|
|
61
|
+
.bind(...params)
|
|
62
|
+
.all<AISessionRow>();
|
|
63
|
+
const sessionRows = results.results || [];
|
|
64
|
+
const previewBySession = await loadSessionPreviews(
|
|
65
|
+
db,
|
|
66
|
+
dialect,
|
|
67
|
+
options.projectId,
|
|
68
|
+
hours,
|
|
69
|
+
sessionRows.map((row) => row.session_id),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const sessions: AISessionSummary[] = sessionRows.map((row) => ({
|
|
73
|
+
sessionId: row.session_id,
|
|
74
|
+
userId: row.user_id ?? null,
|
|
75
|
+
spanCount: row.span_count ?? 0,
|
|
76
|
+
llmSpanCount: row.llm_span_count ?? 0,
|
|
77
|
+
errorCount: row.error_count ?? 0,
|
|
78
|
+
totalPromptTokens: row.prompt_tokens ?? 0,
|
|
79
|
+
totalCompletionTokens: row.completion_tokens ?? 0,
|
|
80
|
+
totalCostUsd: row.cost_usd ?? 0,
|
|
81
|
+
firstSpanAt: row.first_span_at,
|
|
82
|
+
lastSpanAt: row.last_span_at,
|
|
83
|
+
traceCount: row.trace_count ?? 0,
|
|
84
|
+
lastInputPreview: previewBySession.get(row.session_id) ?? null,
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
sessions,
|
|
89
|
+
windowHours: hours,
|
|
90
|
+
timestamp: new Date().toISOString(),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function getAISessionDetail(
|
|
95
|
+
db: SqlDb,
|
|
96
|
+
projectId: string,
|
|
97
|
+
sessionId: string,
|
|
98
|
+
): Promise<AISessionDetailResponse> {
|
|
99
|
+
if (!projectId) throw new Error("AIStore.getSession: projectId is required");
|
|
100
|
+
if (!sessionId) throw new Error("AIStore.getSession: sessionId is required");
|
|
101
|
+
|
|
102
|
+
const sql = `
|
|
103
|
+
SELECT
|
|
104
|
+
s.trace_id AS trace_id,
|
|
105
|
+
s.span_id AS span_id,
|
|
106
|
+
s.parent_span_id AS parent_span_id,
|
|
107
|
+
s.service_name AS service_name,
|
|
108
|
+
s.span_name AS span_name,
|
|
109
|
+
s.status_code AS status_code,
|
|
110
|
+
s.status_message AS status_message,
|
|
111
|
+
s.start_time AS start_time,
|
|
112
|
+
s.end_time AS end_time,
|
|
113
|
+
s.duration_ms AS duration_ms,
|
|
114
|
+
s.attributes_json AS attributes_json,
|
|
115
|
+
p.span_kind AS span_kind,
|
|
116
|
+
p.input_json AS input_json,
|
|
117
|
+
p.output_json AS output_json,
|
|
118
|
+
p.user_id AS user_id
|
|
119
|
+
FROM ai_span_payloads p
|
|
120
|
+
INNER JOIN telemetry_spans s
|
|
121
|
+
ON s.trace_id = p.trace_id AND s.span_id = p.span_id
|
|
122
|
+
WHERE p.project_id = ? AND p.session_id = ?
|
|
123
|
+
ORDER BY s.start_time ASC
|
|
124
|
+
LIMIT 1000
|
|
125
|
+
`;
|
|
126
|
+
const results = await db
|
|
127
|
+
.prepare(sql)
|
|
128
|
+
.bind(projectId, sessionId)
|
|
129
|
+
.all<AISpanRow>();
|
|
130
|
+
|
|
131
|
+
const state = {
|
|
132
|
+
userId: null as string | null,
|
|
133
|
+
totalPromptTokens: 0,
|
|
134
|
+
totalCompletionTokens: 0,
|
|
135
|
+
totalCostUsd: 0,
|
|
136
|
+
errorCount: 0,
|
|
137
|
+
firstSpanAt: null as string | null,
|
|
138
|
+
lastSpanAt: null as string | null,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const spans: AISpanRecord[] = (results.results || []).map((row) =>
|
|
142
|
+
mapSessionSpan(row, state),
|
|
143
|
+
);
|
|
144
|
+
const evaluations = await loadSessionEvaluations(db, projectId, spans);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
sessionId,
|
|
148
|
+
userId: state.userId,
|
|
149
|
+
spans,
|
|
150
|
+
evaluations,
|
|
151
|
+
summary: {
|
|
152
|
+
spanCount: spans.length,
|
|
153
|
+
totalPromptTokens: state.totalPromptTokens,
|
|
154
|
+
totalCompletionTokens: state.totalCompletionTokens,
|
|
155
|
+
totalCostUsd: state.totalCostUsd,
|
|
156
|
+
errorCount: state.errorCount,
|
|
157
|
+
firstSpanAt: state.firstSpanAt,
|
|
158
|
+
lastSpanAt: state.lastSpanAt,
|
|
159
|
+
},
|
|
160
|
+
timestamp: new Date().toISOString(),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function loadSessionPreviews(
|
|
165
|
+
db: SqlDb,
|
|
166
|
+
dialect: ReturnType<typeof dialectFor>,
|
|
167
|
+
projectId: string,
|
|
168
|
+
hours: number,
|
|
169
|
+
sessionIds: string[],
|
|
170
|
+
) {
|
|
171
|
+
const previewBySession = new Map<string, string>();
|
|
172
|
+
if (sessionIds.length === 0) return previewBySession;
|
|
173
|
+
|
|
174
|
+
const previewRows = await db
|
|
175
|
+
.prepare(
|
|
176
|
+
`
|
|
177
|
+
SELECT p.session_id, p.input_json, s.start_time
|
|
178
|
+
FROM ai_span_payloads p
|
|
179
|
+
INNER JOIN telemetry_spans s
|
|
180
|
+
ON s.trace_id = p.trace_id AND s.span_id = p.span_id
|
|
181
|
+
WHERE p.project_id = ?
|
|
182
|
+
AND p.session_id IS NOT NULL
|
|
183
|
+
AND p.session_id IN (${sessionIds.map(() => "?").join(", ")})
|
|
184
|
+
AND p.received_at >= ${dialect.sinceHours("?")}
|
|
185
|
+
AND p.input_json IS NOT NULL
|
|
186
|
+
ORDER BY s.start_time DESC
|
|
187
|
+
LIMIT ?
|
|
188
|
+
`,
|
|
189
|
+
)
|
|
190
|
+
.bind(projectId, ...sessionIds, hours, sessionIds.length * 5)
|
|
191
|
+
.all<AISessionPreviewRow>();
|
|
192
|
+
|
|
193
|
+
for (const row of previewRows.results || []) {
|
|
194
|
+
if (!previewBySession.has(row.session_id)) {
|
|
195
|
+
const raw = typeof row.input_json === "string" ? row.input_json : "";
|
|
196
|
+
previewBySession.set(
|
|
197
|
+
row.session_id,
|
|
198
|
+
raw.length > 200 ? `${raw.slice(0, 200)}…` : raw,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return previewBySession;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function mapSessionSpan(
|
|
207
|
+
row: AISpanRow,
|
|
208
|
+
state: {
|
|
209
|
+
userId: string | null;
|
|
210
|
+
totalPromptTokens: number;
|
|
211
|
+
totalCompletionTokens: number;
|
|
212
|
+
totalCostUsd: number;
|
|
213
|
+
errorCount: number;
|
|
214
|
+
firstSpanAt: string | null;
|
|
215
|
+
lastSpanAt: string | null;
|
|
216
|
+
},
|
|
217
|
+
): AISpanRecord {
|
|
218
|
+
const attrs = parseJsonRecord(row.attributes_json) as Record<
|
|
219
|
+
string,
|
|
220
|
+
JsonValue
|
|
221
|
+
>;
|
|
222
|
+
const cost = enrichCost(attrs);
|
|
223
|
+
if (!state.userId && row.user_id) state.userId = row.user_id;
|
|
224
|
+
state.totalPromptTokens += attrNum(attrs, "llm.token_count.prompt") ?? 0;
|
|
225
|
+
state.totalCompletionTokens +=
|
|
226
|
+
attrNum(attrs, "llm.token_count.completion") ?? 0;
|
|
227
|
+
if (cost !== null) state.totalCostUsd += cost;
|
|
228
|
+
if (row.status_code === 2) state.errorCount++;
|
|
229
|
+
if (!state.firstSpanAt || row.start_time < state.firstSpanAt) {
|
|
230
|
+
state.firstSpanAt = row.start_time;
|
|
231
|
+
}
|
|
232
|
+
if (!state.lastSpanAt || row.start_time > state.lastSpanAt) {
|
|
233
|
+
state.lastSpanAt = row.start_time;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
traceId: row.trace_id,
|
|
238
|
+
spanId: row.span_id,
|
|
239
|
+
parentSpanId: row.parent_span_id,
|
|
240
|
+
serviceName: row.service_name,
|
|
241
|
+
spanName: row.span_name,
|
|
242
|
+
spanKind: row.span_kind,
|
|
243
|
+
statusCode: row.status_code ?? 0,
|
|
244
|
+
statusMessage: row.status_message,
|
|
245
|
+
startTime: row.start_time,
|
|
246
|
+
endTime: row.end_time ?? row.start_time,
|
|
247
|
+
durationMs: row.duration_ms ?? 0,
|
|
248
|
+
attributes: attrs,
|
|
249
|
+
inputJson: row.input_json,
|
|
250
|
+
outputJson: row.output_json,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function loadSessionEvaluations(
|
|
255
|
+
db: SqlDb,
|
|
256
|
+
projectId: string,
|
|
257
|
+
spans: AISpanRecord[],
|
|
258
|
+
) {
|
|
259
|
+
if (spans.length === 0) return [];
|
|
260
|
+
|
|
261
|
+
const placeholders = spans.map(() => "(?, ?)").join(", ");
|
|
262
|
+
const bindings: unknown[] = [projectId];
|
|
263
|
+
for (const span of spans) {
|
|
264
|
+
bindings.push(span.traceId, span.spanId);
|
|
265
|
+
}
|
|
266
|
+
const sql = `
|
|
267
|
+
SELECT * FROM ai_span_evaluations
|
|
268
|
+
WHERE project_id = ?
|
|
269
|
+
AND (trace_id, span_id) IN (${placeholders})
|
|
270
|
+
ORDER BY created_at DESC
|
|
271
|
+
`;
|
|
272
|
+
const results = await db
|
|
273
|
+
.prepare(sql)
|
|
274
|
+
.bind(...bindings)
|
|
275
|
+
.all<AIEvaluationRow>();
|
|
276
|
+
return mapEvaluationRows(results.results || []);
|
|
277
|
+
}
|