@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,41 @@
|
|
|
1
|
+
import type { CollectorPlugin } from "../framework/collector";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Serves the dashboard SPA.
|
|
5
|
+
*
|
|
6
|
+
* For Cloudflare Workers: configure [assets] in wrangler.toml pointing to
|
|
7
|
+
* the @obsunified/dashboard build output. The Workers runtime handles static file
|
|
8
|
+
* serving automatically; this plugin provides the fallback route for SPA
|
|
9
|
+
* client-side routing.
|
|
10
|
+
*
|
|
11
|
+
* For Node.js / other runtimes: serve static files from @obsunified/dashboard/dist
|
|
12
|
+
* using your framework's static file middleware, then mount this plugin for
|
|
13
|
+
* the SPA fallback.
|
|
14
|
+
*/
|
|
15
|
+
export const dashboardRoutesPlugin: CollectorPlugin = {
|
|
16
|
+
name: "dashboard-routes",
|
|
17
|
+
register(app) {
|
|
18
|
+
// SPA fallback: any /dashboard/* route that doesn't match a static file
|
|
19
|
+
// should serve the dashboard index.html
|
|
20
|
+
app.get("/dashboard/*", async (c) => {
|
|
21
|
+
// In CF Workers with [assets], the runtime serves static files before
|
|
22
|
+
// hitting this route. If we get here under Workers with ASSETS, we can
|
|
23
|
+
// fetch and return `/dashboard/` (which serves the index.html SPA entrypoint)
|
|
24
|
+
// directly, preserving the client-side URL in the browser address bar.
|
|
25
|
+
const env = c.env as unknown as Record<string, unknown>;
|
|
26
|
+
const assets = env?.ASSETS as { fetch: typeof fetch } | undefined;
|
|
27
|
+
if (assets && typeof assets.fetch === "function") {
|
|
28
|
+
const url = new URL(c.req.url);
|
|
29
|
+
url.pathname = "/dashboard/";
|
|
30
|
+
return assets.fetch(new Request(url.toString(), c.req.raw));
|
|
31
|
+
}
|
|
32
|
+
// Fallback: redirects to SPA root
|
|
33
|
+
return c.redirect("/dashboard/");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Root redirect: visiting the collector root goes to dashboard
|
|
37
|
+
app.get("/", (c) => {
|
|
38
|
+
return c.redirect("/dashboard/");
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { JsonValue, StoredSpan } from "@obsunified/types";
|
|
2
|
+
import {
|
|
3
|
+
INTERACTION_ID_KEY,
|
|
4
|
+
SESSION_ID_KEY,
|
|
5
|
+
} from "@obsunified/types/constants";
|
|
6
|
+
import type { CollectorPlugin } from "../framework/collector";
|
|
7
|
+
import { parseJsonRecord } from "../lib/json";
|
|
8
|
+
|
|
9
|
+
export const defaultSpanEnrichmentPlugin: CollectorPlugin = {
|
|
10
|
+
name: "default-span-enrichment",
|
|
11
|
+
register(_app, runtime) {
|
|
12
|
+
runtime.addSpanProcessor({
|
|
13
|
+
name: "default-span-enrichment",
|
|
14
|
+
process(spans) {
|
|
15
|
+
return spans.map((span): StoredSpan => {
|
|
16
|
+
const attributes = parseJsonRecord(span.attributesJson);
|
|
17
|
+
const resourceAttributes = parseJsonRecord(
|
|
18
|
+
span.resourceAttributesJson,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const method =
|
|
22
|
+
typeof attributes["http.request.method"] === "string"
|
|
23
|
+
? attributes["http.request.method"]
|
|
24
|
+
: undefined;
|
|
25
|
+
const path =
|
|
26
|
+
typeof attributes["url.path"] === "string"
|
|
27
|
+
? attributes["url.path"]
|
|
28
|
+
: undefined;
|
|
29
|
+
|
|
30
|
+
const normalizedAttributes: Record<string, JsonValue> = {
|
|
31
|
+
...attributes,
|
|
32
|
+
"collector.status_label":
|
|
33
|
+
span.statusCode === 2
|
|
34
|
+
? "error"
|
|
35
|
+
: span.statusCode === 1
|
|
36
|
+
? "ok"
|
|
37
|
+
: "unset",
|
|
38
|
+
"collector.received_at": span.receivedAt,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (method && path) {
|
|
42
|
+
normalizedAttributes["collector.route_label"] = `${method} ${path}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const serviceName =
|
|
46
|
+
span.serviceName ||
|
|
47
|
+
(typeof resourceAttributes["service.name"] === "string"
|
|
48
|
+
? resourceAttributes["service.name"]
|
|
49
|
+
: null) ||
|
|
50
|
+
(typeof attributes["server.address"] === "string"
|
|
51
|
+
? attributes["server.address"]
|
|
52
|
+
: null);
|
|
53
|
+
|
|
54
|
+
const sessionId =
|
|
55
|
+
typeof attributes[SESSION_ID_KEY] === "string" &&
|
|
56
|
+
attributes[SESSION_ID_KEY].length > 0
|
|
57
|
+
? (attributes[SESSION_ID_KEY] as string)
|
|
58
|
+
: null;
|
|
59
|
+
|
|
60
|
+
// RFC 0004 — denormalize obs.interaction.id from attributes
|
|
61
|
+
// to a top-level column. Stamped by @obsunified/telemetry-sdk's
|
|
62
|
+
// stampInteractionFromRequest on the inbound request.
|
|
63
|
+
const interactionId =
|
|
64
|
+
typeof attributes[INTERACTION_ID_KEY] === "string" &&
|
|
65
|
+
attributes[INTERACTION_ID_KEY].length > 0
|
|
66
|
+
? (attributes[INTERACTION_ID_KEY] as string)
|
|
67
|
+
: null;
|
|
68
|
+
|
|
69
|
+
// RFC 0009 — denormalize telemetry.sdk.name from
|
|
70
|
+
// resource_attributes. Beyla and other eBPF agents set
|
|
71
|
+
// this to identify themselves; the service map's
|
|
72
|
+
// source filter uses it to distinguish kernel-observed
|
|
73
|
+
// edges from SDK-instrumented ones.
|
|
74
|
+
const telemetrySdkName =
|
|
75
|
+
typeof resourceAttributes["telemetry.sdk.name"] === "string" &&
|
|
76
|
+
(resourceAttributes["telemetry.sdk.name"] as string).length > 0
|
|
77
|
+
? (resourceAttributes["telemetry.sdk.name"] as string)
|
|
78
|
+
: null;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
...span,
|
|
82
|
+
serviceName,
|
|
83
|
+
attributesJson: JSON.stringify(normalizedAttributes),
|
|
84
|
+
sessionId,
|
|
85
|
+
interactionId,
|
|
86
|
+
telemetrySdkName,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
};
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { CollectorRuntime } from "../framework/collector";
|
|
4
|
+
import type { CollectorEnv } from "../framework/env";
|
|
5
|
+
import { MemSqlDb } from "../lib/test-utils/mem-sql-db";
|
|
6
|
+
import { evalCasesRoutesPlugin } from "./eval-cases-routes";
|
|
7
|
+
|
|
8
|
+
const env = (db: MemSqlDb): CollectorEnv => ({
|
|
9
|
+
DB: db as unknown as D1Database,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const setup = () => {
|
|
13
|
+
const app = new Hono<{ Bindings: CollectorEnv }>();
|
|
14
|
+
evalCasesRoutesPlugin.register(app, new CollectorRuntime());
|
|
15
|
+
return app;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const evalCaseRow = {
|
|
19
|
+
id: "case-1",
|
|
20
|
+
project_id: "default",
|
|
21
|
+
source_entity_type: "action",
|
|
22
|
+
source_entity_id: "action-1",
|
|
23
|
+
source_agent_run_id: "run-1",
|
|
24
|
+
source_action_id: "action-1",
|
|
25
|
+
source_ai_call_id: null,
|
|
26
|
+
source_tool_call_id: "tool-1",
|
|
27
|
+
source_trace_id: "trace-1",
|
|
28
|
+
source_span_id: "span-1",
|
|
29
|
+
name: "Wrong invoice update",
|
|
30
|
+
expected_outcome: "Reject unsafe invoice update",
|
|
31
|
+
rubric_json: '{"criteria":["approval"]}',
|
|
32
|
+
redacted_prompt_json: '{"messages":[]}',
|
|
33
|
+
reference_payload_json: '{"answer":"reject"}',
|
|
34
|
+
metadata_json: '{"sourceLinks":{"actionId":"action-1"}}',
|
|
35
|
+
created_at: "2026-05-31T00:00:00.000Z",
|
|
36
|
+
updated_at: "2026-05-31T00:00:00.000Z",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const evalRunRow = {
|
|
40
|
+
id: "run-123",
|
|
41
|
+
project_id: "default",
|
|
42
|
+
eval_case_id: "case-1",
|
|
43
|
+
candidate_agent_id: "billing-agent",
|
|
44
|
+
candidate_agent_version: "2026.05.31",
|
|
45
|
+
candidate_prompt_id: "invoice-safety",
|
|
46
|
+
candidate_prompt_version: "v7",
|
|
47
|
+
candidate_model_provider: "openai",
|
|
48
|
+
candidate_model: "gpt-4.1",
|
|
49
|
+
candidate_model_version: "2026-04-14",
|
|
50
|
+
status: "running",
|
|
51
|
+
started_at: "2026-05-31T01:00:00.000Z",
|
|
52
|
+
ended_at: null,
|
|
53
|
+
total_count: 1,
|
|
54
|
+
pass_count: 1,
|
|
55
|
+
fail_count: 0,
|
|
56
|
+
average_score: 0.95,
|
|
57
|
+
metadata_json: '{"branch":"fix/invoice-safety"}',
|
|
58
|
+
created_at: "2026-05-31T01:00:00.000Z",
|
|
59
|
+
case_id: "case-1",
|
|
60
|
+
case_name: "Wrong invoice update",
|
|
61
|
+
case_source_entity_type: "action",
|
|
62
|
+
case_source_entity_id: "action-1",
|
|
63
|
+
case_source_agent_run_id: "run-1",
|
|
64
|
+
case_source_action_id: "action-1",
|
|
65
|
+
case_source_ai_call_id: null,
|
|
66
|
+
case_source_tool_call_id: "tool-1",
|
|
67
|
+
case_source_trace_id: "trace-1",
|
|
68
|
+
case_source_span_id: "span-1",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
describe("evalCasesRoutesPlugin", () => {
|
|
72
|
+
it("saves an eval case from a production action source", async () => {
|
|
73
|
+
const db = new MemSqlDb({
|
|
74
|
+
first: (sql) => {
|
|
75
|
+
if (sql.includes("FROM actions")) {
|
|
76
|
+
return {
|
|
77
|
+
id: "action-1",
|
|
78
|
+
agent_run_id: "run-1",
|
|
79
|
+
tool_call_id: "tool-1",
|
|
80
|
+
trace_id: "trace-1",
|
|
81
|
+
span_id: "span-1",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (sql.includes("FROM tool_calls")) {
|
|
85
|
+
return {
|
|
86
|
+
id: "tool-1",
|
|
87
|
+
action_id: "action-1",
|
|
88
|
+
tool_name: "update_invoice",
|
|
89
|
+
args_hash: "args-hash",
|
|
90
|
+
result_hash: "result-hash",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (sql.includes("FROM ai_span_payloads")) {
|
|
94
|
+
return {
|
|
95
|
+
input_json: '{"prompt":"redacted"}',
|
|
96
|
+
output_json: '{"result":"blocked"}',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
},
|
|
101
|
+
all: () => [],
|
|
102
|
+
});
|
|
103
|
+
const app = setup();
|
|
104
|
+
|
|
105
|
+
const res = await app.request(
|
|
106
|
+
"/internal/eval-cases",
|
|
107
|
+
{
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { "content-type": "application/json" },
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
sourceEntityType: "action",
|
|
112
|
+
sourceEntityId: "action-1",
|
|
113
|
+
name: "Wrong invoice update",
|
|
114
|
+
expectedOutcome: "Reject unsafe invoice update",
|
|
115
|
+
rubric: { criteria: ["approval"] },
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
env(db),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(res.status).toBe(201);
|
|
122
|
+
const body = (await res.json()) as {
|
|
123
|
+
evalCase: {
|
|
124
|
+
sourceActionId: string;
|
|
125
|
+
sourceAgentRunId: string;
|
|
126
|
+
sourceToolCallId: string;
|
|
127
|
+
redactedPrompt: unknown;
|
|
128
|
+
referencePayload: unknown;
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
expect(body.evalCase.sourceActionId).toBe("action-1");
|
|
132
|
+
expect(body.evalCase.sourceAgentRunId).toBe("run-1");
|
|
133
|
+
expect(body.evalCase.sourceToolCallId).toBe("tool-1");
|
|
134
|
+
expect(body.evalCase.redactedPrompt).toEqual({ prompt: "redacted" });
|
|
135
|
+
expect(body.evalCase.referencePayload).toEqual({ result: "blocked" });
|
|
136
|
+
expect(db.callsMatching("INSERT INTO eval_cases")).toHaveLength(1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("fetches and lists eval cases", async () => {
|
|
140
|
+
const db = new MemSqlDb({
|
|
141
|
+
first: () => evalCaseRow,
|
|
142
|
+
all: () => [evalCaseRow],
|
|
143
|
+
});
|
|
144
|
+
const app = setup();
|
|
145
|
+
|
|
146
|
+
const getRes = await app.request(
|
|
147
|
+
"/internal/eval-cases/case-1",
|
|
148
|
+
{ method: "GET" },
|
|
149
|
+
env(db),
|
|
150
|
+
);
|
|
151
|
+
const listRes = await app.request(
|
|
152
|
+
"/internal/eval-cases?sourceEntityType=action&sourceEntityId=action-1",
|
|
153
|
+
{ method: "GET" },
|
|
154
|
+
env(db),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(getRes.status).toBe(200);
|
|
158
|
+
expect(listRes.status).toBe(200);
|
|
159
|
+
const getBody = (await getRes.json()) as { evalCase: { id: string } };
|
|
160
|
+
const listBody = (await listRes.json()) as {
|
|
161
|
+
evalCases: Array<{ sourceEntityId: string }>;
|
|
162
|
+
};
|
|
163
|
+
expect(getBody.evalCase.id).toBe("case-1");
|
|
164
|
+
expect(listBody.evalCases[0].sourceEntityId).toBe("action-1");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("validates source type and reports missing production sources", async () => {
|
|
168
|
+
const app = setup();
|
|
169
|
+
|
|
170
|
+
const invalid = await app.request(
|
|
171
|
+
"/internal/eval-cases",
|
|
172
|
+
{
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: { "content-type": "application/json" },
|
|
175
|
+
body: JSON.stringify({
|
|
176
|
+
sourceEntityType: "span",
|
|
177
|
+
sourceEntityId: "span-1",
|
|
178
|
+
name: "Bad source",
|
|
179
|
+
}),
|
|
180
|
+
},
|
|
181
|
+
env(new MemSqlDb()),
|
|
182
|
+
);
|
|
183
|
+
expect(invalid.status).toBe(400);
|
|
184
|
+
|
|
185
|
+
const db = new MemSqlDb({ first: () => null });
|
|
186
|
+
const missing = await setup().request(
|
|
187
|
+
"/internal/eval-cases",
|
|
188
|
+
{
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: { "content-type": "application/json" },
|
|
191
|
+
body: JSON.stringify({
|
|
192
|
+
sourceEntityType: "ai_call",
|
|
193
|
+
sourceEntityId: "missing-call",
|
|
194
|
+
name: "Missing call",
|
|
195
|
+
}),
|
|
196
|
+
},
|
|
197
|
+
env(db),
|
|
198
|
+
);
|
|
199
|
+
expect(missing.status).toBe(404);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("creates, fetches, and lists durable eval runs", async () => {
|
|
203
|
+
const db = new MemSqlDb({
|
|
204
|
+
first: (sql) => {
|
|
205
|
+
if (sql.includes("FROM eval_cases")) return evalCaseRow;
|
|
206
|
+
if (sql.includes("FROM eval_runs")) return evalRunRow;
|
|
207
|
+
return null;
|
|
208
|
+
},
|
|
209
|
+
all: (sql) => (sql.includes("FROM eval_runs") ? [evalRunRow] : []),
|
|
210
|
+
});
|
|
211
|
+
const app = setup();
|
|
212
|
+
|
|
213
|
+
const createRes = await app.request(
|
|
214
|
+
"/internal/eval-runs",
|
|
215
|
+
{
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers: { "content-type": "application/json" },
|
|
218
|
+
body: JSON.stringify({
|
|
219
|
+
id: "run-123",
|
|
220
|
+
evalCaseId: "case-1",
|
|
221
|
+
status: "running",
|
|
222
|
+
candidate: {
|
|
223
|
+
agentId: "billing-agent",
|
|
224
|
+
agentVersion: "2026.05.31",
|
|
225
|
+
promptId: "invoice-safety",
|
|
226
|
+
promptVersion: "v7",
|
|
227
|
+
modelProvider: "openai",
|
|
228
|
+
model: "gpt-4.1",
|
|
229
|
+
modelVersion: "2026-04-14",
|
|
230
|
+
},
|
|
231
|
+
metadata: { branch: "fix/invoice-safety" },
|
|
232
|
+
}),
|
|
233
|
+
},
|
|
234
|
+
env(db),
|
|
235
|
+
);
|
|
236
|
+
const getRes = await app.request(
|
|
237
|
+
"/internal/eval-runs/run-123",
|
|
238
|
+
{ method: "GET" },
|
|
239
|
+
env(db),
|
|
240
|
+
);
|
|
241
|
+
const listRes = await app.request(
|
|
242
|
+
"/internal/eval-runs?evalCaseId=case-1&status=running",
|
|
243
|
+
{ method: "GET" },
|
|
244
|
+
env(db),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect(createRes.status).toBe(201);
|
|
248
|
+
expect(getRes.status).toBe(200);
|
|
249
|
+
expect(listRes.status).toBe(200);
|
|
250
|
+
const createBody = (await createRes.json()) as {
|
|
251
|
+
evalRun: {
|
|
252
|
+
id: string;
|
|
253
|
+
evalCaseId: string;
|
|
254
|
+
candidate: { agentVersion: string; promptVersion: string };
|
|
255
|
+
sourceEvalCase: { sourceActionId: string };
|
|
256
|
+
averageScore: number;
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
expect(createBody.evalRun.id).toBe("run-123");
|
|
260
|
+
expect(createBody.evalRun.candidate.agentVersion).toBe("2026.05.31");
|
|
261
|
+
expect(createBody.evalRun.candidate.promptVersion).toBe("v7");
|
|
262
|
+
expect(createBody.evalRun.sourceEvalCase.sourceActionId).toBe("action-1");
|
|
263
|
+
expect(createBody.evalRun.averageScore).toBe(0.95);
|
|
264
|
+
const listBody = (await listRes.json()) as {
|
|
265
|
+
evalRuns: Array<{ id: string }>;
|
|
266
|
+
};
|
|
267
|
+
expect(listBody.evalRuns[0].id).toBe("run-123");
|
|
268
|
+
expect(db.callsMatching("INSERT INTO eval_runs")).toHaveLength(1);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("rejects invalid eval run input", async () => {
|
|
272
|
+
const res = await setup().request(
|
|
273
|
+
"/internal/eval-runs",
|
|
274
|
+
{
|
|
275
|
+
method: "POST",
|
|
276
|
+
headers: { "content-type": "application/json" },
|
|
277
|
+
body: JSON.stringify({
|
|
278
|
+
id: "",
|
|
279
|
+
status: "done",
|
|
280
|
+
candidate: [],
|
|
281
|
+
}),
|
|
282
|
+
},
|
|
283
|
+
env(new MemSqlDb()),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
expect(res.status).toBe(400);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("ingests and lists eval case results", async () => {
|
|
290
|
+
const db = new MemSqlDb({
|
|
291
|
+
first: (sql) => {
|
|
292
|
+
if (sql.includes("FROM eval_cases")) {
|
|
293
|
+
return evalCaseRow;
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
},
|
|
297
|
+
all: () => [
|
|
298
|
+
{
|
|
299
|
+
id: "result-1",
|
|
300
|
+
project_id: "default",
|
|
301
|
+
eval_case_id: "case-1",
|
|
302
|
+
run_id: "run-123",
|
|
303
|
+
passed: 1,
|
|
304
|
+
score: 0.95,
|
|
305
|
+
actual_outcome: "Correctly updated address",
|
|
306
|
+
details_json: '{"diff":"none"}',
|
|
307
|
+
created_at: "2026-05-31T01:00:00.000Z",
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
});
|
|
311
|
+
const app = setup();
|
|
312
|
+
|
|
313
|
+
// POST result
|
|
314
|
+
const postRes = await app.request(
|
|
315
|
+
"/internal/eval-cases/case-1/results",
|
|
316
|
+
{
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers: { "content-type": "application/json" },
|
|
319
|
+
body: JSON.stringify({
|
|
320
|
+
runId: "run-123",
|
|
321
|
+
passed: true,
|
|
322
|
+
score: 0.95,
|
|
323
|
+
actualOutcome: "Correctly updated address",
|
|
324
|
+
details: { diff: "none" },
|
|
325
|
+
}),
|
|
326
|
+
},
|
|
327
|
+
env(db),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
expect(postRes.status).toBe(201);
|
|
331
|
+
const postBody = (await postRes.json()) as {
|
|
332
|
+
evalCaseResult: { id: string; runId: string; passed: boolean };
|
|
333
|
+
};
|
|
334
|
+
expect(postBody.evalCaseResult.runId).toBe("run-123");
|
|
335
|
+
expect(postBody.evalCaseResult.passed).toBe(true);
|
|
336
|
+
|
|
337
|
+
// GET results
|
|
338
|
+
const getRes = await app.request(
|
|
339
|
+
"/internal/eval-cases/case-1/results",
|
|
340
|
+
{ method: "GET" },
|
|
341
|
+
env(db),
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
expect(getRes.status).toBe(200);
|
|
345
|
+
const getBody = (await getRes.json()) as {
|
|
346
|
+
evalCaseResults: Array<{ runId: string; passed: boolean }>;
|
|
347
|
+
};
|
|
348
|
+
expect(getBody.evalCaseResults).toHaveLength(1);
|
|
349
|
+
expect(getBody.evalCaseResults[0].runId).toBe("run-123");
|
|
350
|
+
expect(getBody.evalCaseResults[0].passed).toBe(true);
|
|
351
|
+
expect(db.callsMatching("UPDATE eval_runs")).toHaveLength(0);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("attaches eval case results to durable eval runs", async () => {
|
|
355
|
+
const db = new MemSqlDb({
|
|
356
|
+
first: (sql) => {
|
|
357
|
+
if (sql.includes("FROM eval_cases")) return evalCaseRow;
|
|
358
|
+
if (sql.includes("FROM eval_runs")) return evalRunRow;
|
|
359
|
+
return null;
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
const app = setup();
|
|
363
|
+
|
|
364
|
+
const postRes = await app.request(
|
|
365
|
+
"/internal/eval-cases/case-1/results",
|
|
366
|
+
{
|
|
367
|
+
method: "POST",
|
|
368
|
+
headers: { "content-type": "application/json" },
|
|
369
|
+
body: JSON.stringify({
|
|
370
|
+
runId: "run-123",
|
|
371
|
+
passed: true,
|
|
372
|
+
score: 0.95,
|
|
373
|
+
actualOutcome: "Correctly updated address",
|
|
374
|
+
details: { diff: "none" },
|
|
375
|
+
}),
|
|
376
|
+
},
|
|
377
|
+
env(db),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
expect(postRes.status).toBe(201);
|
|
381
|
+
const postBody = (await postRes.json()) as {
|
|
382
|
+
evalCaseResult: { runId: string };
|
|
383
|
+
evalRun: { id: string; passCount: number } | null;
|
|
384
|
+
};
|
|
385
|
+
expect(postBody.evalCaseResult.runId).toBe("run-123");
|
|
386
|
+
expect(postBody.evalRun?.id).toBe("run-123");
|
|
387
|
+
expect(postBody.evalRun?.passCount).toBe(1);
|
|
388
|
+
expect(db.callsMatching("INSERT INTO eval_case_results")).toHaveLength(1);
|
|
389
|
+
expect(db.callsMatching("UPDATE eval_runs")).toHaveLength(1);
|
|
390
|
+
});
|
|
391
|
+
});
|