@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,1103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0006 Phase 3.1 — Connected rail manifest endpoint.
|
|
3
|
+
*
|
|
4
|
+
* GET /internal/connected/:kind/:id
|
|
5
|
+
*
|
|
6
|
+
* Returns the entity's identity-graph neighbors (Up / Across / Down) +
|
|
7
|
+
* topic neighbors (Related), bundled into the shape the dashboard's
|
|
8
|
+
* <ConnectedRail /> renders directly. Built on `IdentityIndex` so the
|
|
9
|
+
* underlying SQL is shared with `/internal/timeline/:sessionId` and any
|
|
10
|
+
* future cross-signal consumer.
|
|
11
|
+
*
|
|
12
|
+
* The handler is the single read path the rail uses. Sections that have
|
|
13
|
+
* no neighbors render as `links: []` with an `emptyReason` explaining
|
|
14
|
+
* why — never absent. This is the "informative absence" pattern from
|
|
15
|
+
* RFC 0006 § Connected rail / Empty-state.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { ActionConfidence } from "@obsunified/types/constants";
|
|
19
|
+
import type { CollectorPlugin } from "../framework/collector";
|
|
20
|
+
import { IdentityIndex } from "../lib/identity-index";
|
|
21
|
+
import {
|
|
22
|
+
mapAction,
|
|
23
|
+
mapAgentRun,
|
|
24
|
+
mapEvalResult,
|
|
25
|
+
mapToolCall,
|
|
26
|
+
} from "../lib/identity-index/mappers";
|
|
27
|
+
import type {
|
|
28
|
+
ActionRef,
|
|
29
|
+
AgentRunRef,
|
|
30
|
+
EvalResultRef,
|
|
31
|
+
ToolCallRef,
|
|
32
|
+
} from "../lib/identity-index/types";
|
|
33
|
+
import type { SqlDb } from "../lib/sql-db";
|
|
34
|
+
import { sqlDbFor } from "../lib/sql-db";
|
|
35
|
+
import { getProjectId } from "./_context";
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
type ActionContextLinkMetadata,
|
|
39
|
+
type ConnectedEntityKind,
|
|
40
|
+
type ConnectedManifest,
|
|
41
|
+
KNOWN_KINDS,
|
|
42
|
+
linksFromActions,
|
|
43
|
+
linksFromAi,
|
|
44
|
+
linksFromArtifacts,
|
|
45
|
+
linksFromEvalResults,
|
|
46
|
+
linksFromLogs,
|
|
47
|
+
linksFromMetricExemplars,
|
|
48
|
+
linksFromRetrievalEvents,
|
|
49
|
+
linksFromSpans,
|
|
50
|
+
linksFromToolCalls,
|
|
51
|
+
linksFromUsage,
|
|
52
|
+
linkToAction,
|
|
53
|
+
linkToAgentRun,
|
|
54
|
+
linkToSession,
|
|
55
|
+
linkToTrace,
|
|
56
|
+
MAX_LINKS_INLINE,
|
|
57
|
+
profileLinksForTrace,
|
|
58
|
+
} from "./connected-routes/manifest";
|
|
59
|
+
|
|
60
|
+
export type {
|
|
61
|
+
ConnectedEntityKind,
|
|
62
|
+
ConnectedLink,
|
|
63
|
+
ConnectedManifest,
|
|
64
|
+
ConnectedSection,
|
|
65
|
+
} from "./connected-routes/manifest";
|
|
66
|
+
|
|
67
|
+
interface SignalActionContext {
|
|
68
|
+
actions: ActionRef[];
|
|
69
|
+
agentRuns: AgentRunRef[];
|
|
70
|
+
toolCalls: ToolCallRef[];
|
|
71
|
+
evalResults: EvalResultRef[];
|
|
72
|
+
matchLevel: "span" | "trace" | "none";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const emptySignalActionContext = (): SignalActionContext => ({
|
|
76
|
+
actions: [],
|
|
77
|
+
agentRuns: [],
|
|
78
|
+
toolCalls: [],
|
|
79
|
+
evalResults: [],
|
|
80
|
+
matchLevel: "none",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const loadSignalActionContext = async (
|
|
84
|
+
db: SqlDb,
|
|
85
|
+
projectId: string,
|
|
86
|
+
traceId: string | null | undefined,
|
|
87
|
+
spanId?: string | null,
|
|
88
|
+
): Promise<SignalActionContext> => {
|
|
89
|
+
if (!traceId) return emptySignalActionContext();
|
|
90
|
+
|
|
91
|
+
const exactRows = spanId
|
|
92
|
+
? await db
|
|
93
|
+
.prepare(
|
|
94
|
+
`SELECT *
|
|
95
|
+
FROM actions
|
|
96
|
+
WHERE project_id = ? AND trace_id = ? AND span_id = ?
|
|
97
|
+
ORDER BY started_at ASC
|
|
98
|
+
LIMIT 20`,
|
|
99
|
+
)
|
|
100
|
+
.bind(projectId, traceId, spanId)
|
|
101
|
+
.all<Parameters<typeof mapAction>[0]>()
|
|
102
|
+
: { results: [] as Parameters<typeof mapAction>[0][] };
|
|
103
|
+
|
|
104
|
+
const traceRows =
|
|
105
|
+
exactRows.results.length > 0
|
|
106
|
+
? exactRows
|
|
107
|
+
: await db
|
|
108
|
+
.prepare(
|
|
109
|
+
`SELECT *
|
|
110
|
+
FROM actions
|
|
111
|
+
WHERE project_id = ? AND trace_id = ?
|
|
112
|
+
ORDER BY started_at ASC
|
|
113
|
+
LIMIT 20`,
|
|
114
|
+
)
|
|
115
|
+
.bind(projectId, traceId)
|
|
116
|
+
.all<Parameters<typeof mapAction>[0]>();
|
|
117
|
+
|
|
118
|
+
const actionRows = traceRows.results;
|
|
119
|
+
if (actionRows.length === 0) return emptySignalActionContext();
|
|
120
|
+
|
|
121
|
+
const actionIds = actionRows.map((action) => action.id);
|
|
122
|
+
const rootActionIds = Array.from(
|
|
123
|
+
new Set(
|
|
124
|
+
actionRows
|
|
125
|
+
.map((action) => action.agent_run_id ?? action.root_action_id)
|
|
126
|
+
.filter((id): id is string => Boolean(id)),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
const actionPlaceholders = actionIds.map(() => "?").join(", ");
|
|
130
|
+
const rootPlaceholders = rootActionIds.map(() => "?").join(", ");
|
|
131
|
+
|
|
132
|
+
const [toolCalls, evalResults, agentRuns] = await Promise.all([
|
|
133
|
+
db
|
|
134
|
+
.prepare(
|
|
135
|
+
`SELECT *
|
|
136
|
+
FROM tool_calls
|
|
137
|
+
WHERE project_id = ? AND action_id IN (${actionPlaceholders})
|
|
138
|
+
ORDER BY id ASC
|
|
139
|
+
LIMIT 20`,
|
|
140
|
+
)
|
|
141
|
+
.bind(projectId, ...actionIds)
|
|
142
|
+
.all<Parameters<typeof mapToolCall>[0]>(),
|
|
143
|
+
db
|
|
144
|
+
.prepare(
|
|
145
|
+
`SELECT *
|
|
146
|
+
FROM eval_results
|
|
147
|
+
WHERE project_id = ? AND action_id IN (${actionPlaceholders})
|
|
148
|
+
ORDER BY id ASC
|
|
149
|
+
LIMIT 20`,
|
|
150
|
+
)
|
|
151
|
+
.bind(projectId, ...actionIds)
|
|
152
|
+
.all<Parameters<typeof mapEvalResult>[0]>(),
|
|
153
|
+
rootActionIds.length > 0
|
|
154
|
+
? db
|
|
155
|
+
.prepare(
|
|
156
|
+
`SELECT *
|
|
157
|
+
FROM agent_runs
|
|
158
|
+
WHERE project_id = ? AND id IN (${rootPlaceholders})
|
|
159
|
+
ORDER BY id ASC
|
|
160
|
+
LIMIT 20`,
|
|
161
|
+
)
|
|
162
|
+
.bind(projectId, ...rootActionIds)
|
|
163
|
+
.all<Parameters<typeof mapAgentRun>[0]>()
|
|
164
|
+
: { results: [] as Parameters<typeof mapAgentRun>[0][] },
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
actions: actionRows.map(mapAction),
|
|
169
|
+
agentRuns: agentRuns.results.map(mapAgentRun),
|
|
170
|
+
toolCalls: toolCalls.results.map(mapToolCall),
|
|
171
|
+
evalResults: evalResults.results.map(mapEvalResult),
|
|
172
|
+
matchLevel: exactRows.results.length > 0 ? "span" : "trace",
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const actionMatchEmptyReason = (context: SignalActionContext): string =>
|
|
177
|
+
context.matchLevel === "trace"
|
|
178
|
+
? "No exact span-level action existed, so these links use trace-level action context."
|
|
179
|
+
: "No action graph records share this signal's trace/span identity.";
|
|
180
|
+
|
|
181
|
+
const actionContextLinkMetadata = (
|
|
182
|
+
context: SignalActionContext,
|
|
183
|
+
entityLabel: string,
|
|
184
|
+
): ActionContextLinkMetadata => {
|
|
185
|
+
if (context.matchLevel === "trace") {
|
|
186
|
+
return {
|
|
187
|
+
source: "trace_id",
|
|
188
|
+
causalConfidence: ActionConfidence.Fallback,
|
|
189
|
+
confidence: 0.55,
|
|
190
|
+
reason: `No exact span-level action matched this ${entityLabel}; linked by shared trace_id instead.`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
source: "trace_id+span_id",
|
|
195
|
+
confidence: 0.95,
|
|
196
|
+
reason: `Action context matched this ${entityLabel} by exact trace_id and span_id.`,
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const metadataForSignalActions = (
|
|
201
|
+
context: SignalActionContext,
|
|
202
|
+
entityLabel: string,
|
|
203
|
+
): ((actionId: string) => ActionContextLinkMetadata | undefined) => {
|
|
204
|
+
const byActionId = new Map(
|
|
205
|
+
context.actions.map((action) => {
|
|
206
|
+
const metadata = actionContextLinkMetadata(context, entityLabel);
|
|
207
|
+
return [
|
|
208
|
+
action.id,
|
|
209
|
+
context.matchLevel === "span"
|
|
210
|
+
? { ...metadata, causalConfidence: action.causalConfidence }
|
|
211
|
+
: metadata,
|
|
212
|
+
] as const;
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
return (actionId) => byActionId.get(actionId);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const traceLevelMetadata = (
|
|
219
|
+
entityLabel: string,
|
|
220
|
+
): ActionContextLinkMetadata => ({
|
|
221
|
+
source: "trace_id",
|
|
222
|
+
causalConfidence: ActionConfidence.Fallback,
|
|
223
|
+
confidence: 0.55,
|
|
224
|
+
reason: `Linked ${entityLabel} through sampled trace_id context; no exact span-level causal edge is implied.`,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const linksForAgentRuns = (
|
|
228
|
+
runs: AgentRunRef[],
|
|
229
|
+
context: SignalActionContext,
|
|
230
|
+
entityLabel: string,
|
|
231
|
+
) =>
|
|
232
|
+
runs.slice(0, MAX_LINKS_INLINE).map((run) => ({
|
|
233
|
+
...linkToAgentRun(run.id, `${run.agentName} (v${run.agentVersion})`),
|
|
234
|
+
...actionContextLinkMetadata(context, entityLabel),
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
export const connectedRoutesPlugin: CollectorPlugin = {
|
|
238
|
+
name: "connected-routes",
|
|
239
|
+
register(app) {
|
|
240
|
+
app.get("/internal/connected/:kind/:id", async (c) => {
|
|
241
|
+
const projectId = getProjectId(c);
|
|
242
|
+
const rawKind = c.req.param("kind");
|
|
243
|
+
const id = c.req.param("id");
|
|
244
|
+
if (!id) {
|
|
245
|
+
return c.json({ error: "id required" }, 400);
|
|
246
|
+
}
|
|
247
|
+
if (!KNOWN_KINDS.has(rawKind)) {
|
|
248
|
+
// Unknown kinds used to fall through every `if` branch and
|
|
249
|
+
// return 200 + empty sections, which hid client bugs.
|
|
250
|
+
return c.json(
|
|
251
|
+
{
|
|
252
|
+
error: `unknown entity kind: ${rawKind}`,
|
|
253
|
+
known: [...KNOWN_KINDS],
|
|
254
|
+
},
|
|
255
|
+
400,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const kind = rawKind as ConnectedEntityKind;
|
|
259
|
+
|
|
260
|
+
const index = new IdentityIndex(sqlDbFor(c.env));
|
|
261
|
+
|
|
262
|
+
// Each entity kind starts with a different lookup key. The
|
|
263
|
+
// helpers above shape ref arrays into ConnectedSection.
|
|
264
|
+
const manifest: ConnectedManifest = {
|
|
265
|
+
entity: { kind, id, projectId },
|
|
266
|
+
up: [],
|
|
267
|
+
across: [],
|
|
268
|
+
down: [],
|
|
269
|
+
related: [],
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
if (kind === "span") {
|
|
273
|
+
// Span: load by trace_id (its parent chain) + by interaction_id
|
|
274
|
+
// (the click that caused it). Then bucket.
|
|
275
|
+
const traceManifest = await index.byTrace(
|
|
276
|
+
projectId,
|
|
277
|
+
id.split(":")[0] ?? id,
|
|
278
|
+
);
|
|
279
|
+
const span = traceManifest.spans.find(
|
|
280
|
+
(s) => `${s.traceId}:${s.spanId}` === id,
|
|
281
|
+
);
|
|
282
|
+
if (span) {
|
|
283
|
+
const actionContext = await loadSignalActionContext(
|
|
284
|
+
sqlDbFor(c.env),
|
|
285
|
+
projectId,
|
|
286
|
+
span.traceId,
|
|
287
|
+
span.spanId,
|
|
288
|
+
);
|
|
289
|
+
const otherSpans = traceManifest.spans.filter(
|
|
290
|
+
(s) => s.spanId !== span.spanId,
|
|
291
|
+
);
|
|
292
|
+
manifest.up = [
|
|
293
|
+
{
|
|
294
|
+
label: "Trace",
|
|
295
|
+
links: [linkToTrace(span.traceId, "Parent trace")],
|
|
296
|
+
},
|
|
297
|
+
];
|
|
298
|
+
manifest.across = [
|
|
299
|
+
linksFromSpans(otherSpans, `Other spans in this trace`),
|
|
300
|
+
linksFromLogs(traceManifest.logs, "Logs in this trace"),
|
|
301
|
+
linksFromAi(traceManifest.aiCalls, "AI calls in this trace"),
|
|
302
|
+
];
|
|
303
|
+
// RFC 0009 acceptance #5 — surface pprof profiles
|
|
304
|
+
// (CPU + off-CPU) covering this trace's window in the
|
|
305
|
+
// rail's Down section. Each profile_type renders as
|
|
306
|
+
// its own link so a viewer can see at a glance whether
|
|
307
|
+
// only CPU was sampled or off-CPU is present too.
|
|
308
|
+
manifest.down = [
|
|
309
|
+
...(await profileLinksForTrace(
|
|
310
|
+
sqlDbFor(c.env),
|
|
311
|
+
projectId,
|
|
312
|
+
span.traceId,
|
|
313
|
+
)),
|
|
314
|
+
linksFromActions(
|
|
315
|
+
actionContext.actions,
|
|
316
|
+
actionContext.matchLevel === "span"
|
|
317
|
+
? "Causal actions for this span"
|
|
318
|
+
: "Trace-level actions for this span",
|
|
319
|
+
actionContextLinkMetadata(actionContext, "span"),
|
|
320
|
+
),
|
|
321
|
+
linksFromToolCalls(
|
|
322
|
+
actionContext.toolCalls,
|
|
323
|
+
actionContext.matchLevel === "span"
|
|
324
|
+
? "Tool calls for this span"
|
|
325
|
+
: "Trace-level tool calls for this span",
|
|
326
|
+
metadataForSignalActions(actionContext, "span"),
|
|
327
|
+
),
|
|
328
|
+
linksFromEvalResults(
|
|
329
|
+
actionContext.evalResults,
|
|
330
|
+
actionContext.matchLevel === "span"
|
|
331
|
+
? "Evaluations for this span"
|
|
332
|
+
: "Trace-level evaluations for this span",
|
|
333
|
+
metadataForSignalActions(actionContext, "span"),
|
|
334
|
+
),
|
|
335
|
+
linksFromMetricExemplars(
|
|
336
|
+
traceManifest.metricExemplars.filter(
|
|
337
|
+
(e) => !e.spanId || e.spanId === span.spanId,
|
|
338
|
+
),
|
|
339
|
+
"Metric exemplars in this trace",
|
|
340
|
+
),
|
|
341
|
+
];
|
|
342
|
+
if (span.interactionId) {
|
|
343
|
+
const interactionManifest = await index.byInteraction(
|
|
344
|
+
projectId,
|
|
345
|
+
span.interactionId,
|
|
346
|
+
);
|
|
347
|
+
manifest.related = [
|
|
348
|
+
linksFromUsage(
|
|
349
|
+
interactionManifest.usageEvents,
|
|
350
|
+
`Click that caused this trace`,
|
|
351
|
+
),
|
|
352
|
+
];
|
|
353
|
+
} else {
|
|
354
|
+
manifest.related = [
|
|
355
|
+
{
|
|
356
|
+
label: "Originating click",
|
|
357
|
+
links: [],
|
|
358
|
+
emptyReason:
|
|
359
|
+
"Server-originated work (cron, retry, queue consumer) — not bound to a user click.",
|
|
360
|
+
},
|
|
361
|
+
];
|
|
362
|
+
}
|
|
363
|
+
if (actionContext.agentRuns.length > 0) {
|
|
364
|
+
manifest.related.push({
|
|
365
|
+
label:
|
|
366
|
+
actionContext.matchLevel === "span"
|
|
367
|
+
? "Agent runs for this span"
|
|
368
|
+
: "Trace-level agent runs for this span",
|
|
369
|
+
links: linksForAgentRuns(
|
|
370
|
+
actionContext.agentRuns,
|
|
371
|
+
actionContext,
|
|
372
|
+
"span",
|
|
373
|
+
),
|
|
374
|
+
});
|
|
375
|
+
} else {
|
|
376
|
+
manifest.related.push({
|
|
377
|
+
label: "Agent runs for this span",
|
|
378
|
+
links: [],
|
|
379
|
+
emptyReason: actionMatchEmptyReason(actionContext),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} else if (kind === "profile") {
|
|
384
|
+
const db = sqlDbFor(c.env);
|
|
385
|
+
const profile = await db
|
|
386
|
+
.prepare(
|
|
387
|
+
`SELECT id, service_name, profile_type, start_ts, end_ts,
|
|
388
|
+
duration_ms, sample_count, agent
|
|
389
|
+
FROM profile_blobs
|
|
390
|
+
WHERE project_id = ? AND id = ?
|
|
391
|
+
LIMIT 1`,
|
|
392
|
+
)
|
|
393
|
+
.bind(projectId, id)
|
|
394
|
+
.first<{
|
|
395
|
+
id: string;
|
|
396
|
+
service_name: string | null;
|
|
397
|
+
profile_type: string;
|
|
398
|
+
start_ts: string;
|
|
399
|
+
end_ts: string;
|
|
400
|
+
duration_ms: number;
|
|
401
|
+
sample_count: number | null;
|
|
402
|
+
agent: string | null;
|
|
403
|
+
}>();
|
|
404
|
+
|
|
405
|
+
if (profile) {
|
|
406
|
+
const traceRows = await db
|
|
407
|
+
.prepare(
|
|
408
|
+
`SELECT trace_id
|
|
409
|
+
FROM profile_trace_index
|
|
410
|
+
WHERE project_id = ? AND profile_id = ?
|
|
411
|
+
ORDER BY trace_id ASC
|
|
412
|
+
LIMIT 50`,
|
|
413
|
+
)
|
|
414
|
+
.bind(projectId, id)
|
|
415
|
+
.all<{ trace_id: string }>();
|
|
416
|
+
const traceIds = traceRows.results.map((row) => row.trace_id);
|
|
417
|
+
const sampledTraceIds = traceIds.slice(0, MAX_LINKS_INLINE);
|
|
418
|
+
const traceManifests = await Promise.all(
|
|
419
|
+
sampledTraceIds.map((traceId) => index.byTrace(projectId, traceId)),
|
|
420
|
+
);
|
|
421
|
+
const spans = traceManifests.flatMap((m) => m.spans);
|
|
422
|
+
const logs = traceManifests.flatMap((m) => m.logs);
|
|
423
|
+
const aiCalls = traceManifests.flatMap((m) => m.aiCalls);
|
|
424
|
+
const metricExemplars = traceManifests.flatMap(
|
|
425
|
+
(m) => m.metricExemplars,
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
manifest.up = [
|
|
429
|
+
{
|
|
430
|
+
label: "Profile window",
|
|
431
|
+
links: [],
|
|
432
|
+
emptyReason: `${profile.service_name ?? "unknown service"} · ${profile.profile_type} · ${profile.duration_ms}ms · ${profile.sample_count ?? "unknown"} samples`,
|
|
433
|
+
},
|
|
434
|
+
];
|
|
435
|
+
manifest.across = [
|
|
436
|
+
traceIds.length > 0
|
|
437
|
+
? {
|
|
438
|
+
label: "Sampled traces",
|
|
439
|
+
links:
|
|
440
|
+
traceIds.length > MAX_LINKS_INLINE
|
|
441
|
+
? [
|
|
442
|
+
{
|
|
443
|
+
label: `${traceIds.length} sampled traces`,
|
|
444
|
+
href: `#/traces?trace=${encodeURIComponent(traceIds[0])}`,
|
|
445
|
+
count: traceIds.length,
|
|
446
|
+
sample: traceIds[0],
|
|
447
|
+
},
|
|
448
|
+
]
|
|
449
|
+
: traceIds.map((traceId) => linkToTrace(traceId)),
|
|
450
|
+
}
|
|
451
|
+
: {
|
|
452
|
+
label: "Sampled traces",
|
|
453
|
+
links: [],
|
|
454
|
+
emptyReason:
|
|
455
|
+
"No trace_id labels were indexed for this profile. Push pprof samples with trace_id labels or x-obs-trace-ids to enable trace pivots.",
|
|
456
|
+
},
|
|
457
|
+
linksFromSpans(spans, "Sampled spans"),
|
|
458
|
+
linksFromLogs(logs, "Logs in sampled traces"),
|
|
459
|
+
linksFromAi(aiCalls, "AI calls in sampled traces"),
|
|
460
|
+
linksFromMetricExemplars(
|
|
461
|
+
metricExemplars,
|
|
462
|
+
"Metric exemplars in sampled traces",
|
|
463
|
+
),
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
if (traceIds.length > 0) {
|
|
467
|
+
const placeholders = traceIds.map(() => "?").join(", ");
|
|
468
|
+
const actions = await db
|
|
469
|
+
.prepare(
|
|
470
|
+
`SELECT id, action_kind, name
|
|
471
|
+
FROM actions
|
|
472
|
+
WHERE project_id = ? AND trace_id IN (${placeholders})
|
|
473
|
+
ORDER BY started_at ASC
|
|
474
|
+
LIMIT 20`,
|
|
475
|
+
)
|
|
476
|
+
.bind(projectId, ...traceIds)
|
|
477
|
+
.all<{
|
|
478
|
+
id: string;
|
|
479
|
+
action_kind: string;
|
|
480
|
+
name: string | null;
|
|
481
|
+
}>();
|
|
482
|
+
const actionIds = actions.results.map((action) => action.id);
|
|
483
|
+
const actionPlaceholders = actionIds.map(() => "?").join(", ");
|
|
484
|
+
|
|
485
|
+
const toolCalls =
|
|
486
|
+
actionIds.length > 0
|
|
487
|
+
? await db
|
|
488
|
+
.prepare(
|
|
489
|
+
`SELECT id, tool_name
|
|
490
|
+
FROM tool_calls
|
|
491
|
+
WHERE project_id = ? AND action_id IN (${actionPlaceholders})
|
|
492
|
+
ORDER BY id ASC
|
|
493
|
+
LIMIT 20`,
|
|
494
|
+
)
|
|
495
|
+
.bind(projectId, ...actionIds)
|
|
496
|
+
.all<{ id: string; tool_name: string }>()
|
|
497
|
+
: { results: [] as Array<{ id: string; tool_name: string }> };
|
|
498
|
+
|
|
499
|
+
const agentRuns = await db
|
|
500
|
+
.prepare(
|
|
501
|
+
`SELECT DISTINCT ar.id, ar.agent_name, ar.agent_version
|
|
502
|
+
FROM actions a
|
|
503
|
+
JOIN agent_runs ar
|
|
504
|
+
ON ar.project_id = a.project_id
|
|
505
|
+
AND ar.id = COALESCE(a.agent_run_id, a.root_action_id)
|
|
506
|
+
WHERE a.project_id = ? AND a.trace_id IN (${placeholders})
|
|
507
|
+
ORDER BY ar.id ASC
|
|
508
|
+
LIMIT 20`,
|
|
509
|
+
)
|
|
510
|
+
.bind(projectId, ...traceIds)
|
|
511
|
+
.all<{
|
|
512
|
+
id: string;
|
|
513
|
+
agent_name: string;
|
|
514
|
+
agent_version: string;
|
|
515
|
+
}>();
|
|
516
|
+
|
|
517
|
+
manifest.down = [
|
|
518
|
+
actions.results.length > 0
|
|
519
|
+
? {
|
|
520
|
+
label: "Causal actions in sampled traces",
|
|
521
|
+
links: actions.results
|
|
522
|
+
.slice(0, MAX_LINKS_INLINE)
|
|
523
|
+
.map((action) => ({
|
|
524
|
+
...linkToAction(
|
|
525
|
+
action.id,
|
|
526
|
+
`[${action.action_kind}] ${action.name ?? action.id}`,
|
|
527
|
+
),
|
|
528
|
+
...traceLevelMetadata("profile action"),
|
|
529
|
+
})),
|
|
530
|
+
}
|
|
531
|
+
: {
|
|
532
|
+
label: "Causal actions in sampled traces",
|
|
533
|
+
links: [],
|
|
534
|
+
emptyReason:
|
|
535
|
+
"No action graph records share this profile's sampled trace IDs.",
|
|
536
|
+
},
|
|
537
|
+
toolCalls.results.length > 0
|
|
538
|
+
? {
|
|
539
|
+
label: "Tool calls in sampled traces",
|
|
540
|
+
links: toolCalls.results
|
|
541
|
+
.slice(0, MAX_LINKS_INLINE)
|
|
542
|
+
.map((toolCall) => ({
|
|
543
|
+
label: `tool: ${toolCall.tool_name}`,
|
|
544
|
+
href: `#/tool-calls/${toolCall.id}`,
|
|
545
|
+
entityKind: "tool_call",
|
|
546
|
+
entityId: toolCall.id,
|
|
547
|
+
...traceLevelMetadata("profile tool call"),
|
|
548
|
+
})),
|
|
549
|
+
}
|
|
550
|
+
: {
|
|
551
|
+
label: "Tool calls in sampled traces",
|
|
552
|
+
links: [],
|
|
553
|
+
emptyReason:
|
|
554
|
+
"No tool calls were attached to actions in this profile.",
|
|
555
|
+
},
|
|
556
|
+
];
|
|
557
|
+
|
|
558
|
+
manifest.related =
|
|
559
|
+
agentRuns.results.length > 0
|
|
560
|
+
? [
|
|
561
|
+
{
|
|
562
|
+
label: "Agent runs",
|
|
563
|
+
links: agentRuns.results
|
|
564
|
+
.slice(0, MAX_LINKS_INLINE)
|
|
565
|
+
.map((run) => ({
|
|
566
|
+
...linkToAgentRun(
|
|
567
|
+
run.id,
|
|
568
|
+
`${run.agent_name} (v${run.agent_version})`,
|
|
569
|
+
),
|
|
570
|
+
...traceLevelMetadata("profile agent run"),
|
|
571
|
+
})),
|
|
572
|
+
},
|
|
573
|
+
]
|
|
574
|
+
: [
|
|
575
|
+
{
|
|
576
|
+
label: "Agent runs",
|
|
577
|
+
links: [],
|
|
578
|
+
emptyReason:
|
|
579
|
+
"No agent runs were attached to actions in this profile.",
|
|
580
|
+
},
|
|
581
|
+
];
|
|
582
|
+
} else {
|
|
583
|
+
manifest.down = [
|
|
584
|
+
{
|
|
585
|
+
label: "Causal actions in sampled traces",
|
|
586
|
+
links: [],
|
|
587
|
+
emptyReason:
|
|
588
|
+
"No action graph records can be joined because this profile has no indexed trace IDs.",
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
label: "Tool calls in sampled traces",
|
|
592
|
+
links: [],
|
|
593
|
+
emptyReason:
|
|
594
|
+
"No tool calls can be joined because this profile has no indexed trace IDs.",
|
|
595
|
+
},
|
|
596
|
+
];
|
|
597
|
+
manifest.related = [
|
|
598
|
+
{
|
|
599
|
+
label: "Agent runs",
|
|
600
|
+
links: [],
|
|
601
|
+
emptyReason:
|
|
602
|
+
"No agent runs can be joined because this profile has no indexed trace IDs.",
|
|
603
|
+
},
|
|
604
|
+
];
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
} else if (kind === "log") {
|
|
608
|
+
// Log: resolve direct log ids when the caller did not already carry
|
|
609
|
+
// trace/session params, then add exact action graph pivots.
|
|
610
|
+
const db = sqlDbFor(c.env);
|
|
611
|
+
const logRow = await db
|
|
612
|
+
.prepare(
|
|
613
|
+
`SELECT trace_id, span_id, session_id
|
|
614
|
+
FROM logs
|
|
615
|
+
WHERE project_id = ? AND log_id = ?
|
|
616
|
+
LIMIT 1`,
|
|
617
|
+
)
|
|
618
|
+
.bind(projectId, id)
|
|
619
|
+
.first<{
|
|
620
|
+
trace_id: string | null;
|
|
621
|
+
span_id: string | null;
|
|
622
|
+
session_id: string | null;
|
|
623
|
+
}>();
|
|
624
|
+
const traceId =
|
|
625
|
+
c.req.query("trace_id") ?? logRow?.trace_id ?? undefined;
|
|
626
|
+
const spanId = c.req.query("span_id") ?? logRow?.span_id ?? undefined;
|
|
627
|
+
const sessionId =
|
|
628
|
+
c.req.query("session_id") ?? logRow?.session_id ?? undefined;
|
|
629
|
+
if (traceId) {
|
|
630
|
+
const traceManifest = await index.byTrace(projectId, traceId);
|
|
631
|
+
const actionContext = await loadSignalActionContext(
|
|
632
|
+
db,
|
|
633
|
+
projectId,
|
|
634
|
+
traceId,
|
|
635
|
+
spanId,
|
|
636
|
+
);
|
|
637
|
+
manifest.up = [
|
|
638
|
+
{
|
|
639
|
+
label: "Trace",
|
|
640
|
+
links: [linkToTrace(traceId, "Parent trace")],
|
|
641
|
+
},
|
|
642
|
+
];
|
|
643
|
+
manifest.across = [
|
|
644
|
+
linksFromLogs(
|
|
645
|
+
traceManifest.logs.filter((l) => l.logId !== id),
|
|
646
|
+
"Other logs in this trace",
|
|
647
|
+
),
|
|
648
|
+
];
|
|
649
|
+
manifest.down = [
|
|
650
|
+
linksFromActions(
|
|
651
|
+
actionContext.actions,
|
|
652
|
+
actionContext.matchLevel === "span"
|
|
653
|
+
? "Causal actions for this log"
|
|
654
|
+
: "Trace-level actions for this log",
|
|
655
|
+
actionContextLinkMetadata(actionContext, "log"),
|
|
656
|
+
),
|
|
657
|
+
linksFromToolCalls(
|
|
658
|
+
actionContext.toolCalls,
|
|
659
|
+
actionContext.matchLevel === "span"
|
|
660
|
+
? "Tool calls for this log"
|
|
661
|
+
: "Trace-level tool calls for this log",
|
|
662
|
+
metadataForSignalActions(actionContext, "log"),
|
|
663
|
+
),
|
|
664
|
+
linksFromEvalResults(
|
|
665
|
+
actionContext.evalResults,
|
|
666
|
+
actionContext.matchLevel === "span"
|
|
667
|
+
? "Evaluations for this log"
|
|
668
|
+
: "Trace-level evaluations for this log",
|
|
669
|
+
metadataForSignalActions(actionContext, "log"),
|
|
670
|
+
),
|
|
671
|
+
];
|
|
672
|
+
manifest.related = [
|
|
673
|
+
actionContext.agentRuns.length > 0
|
|
674
|
+
? {
|
|
675
|
+
label:
|
|
676
|
+
actionContext.matchLevel === "span"
|
|
677
|
+
? "Agent runs for this log"
|
|
678
|
+
: "Trace-level agent runs for this log",
|
|
679
|
+
links: actionContext.agentRuns
|
|
680
|
+
.slice(0, MAX_LINKS_INLINE)
|
|
681
|
+
.map((run) => ({
|
|
682
|
+
...linkToAgentRun(
|
|
683
|
+
run.id,
|
|
684
|
+
`${run.agentName} (v${run.agentVersion})`,
|
|
685
|
+
),
|
|
686
|
+
...actionContextLinkMetadata(actionContext, "log"),
|
|
687
|
+
})),
|
|
688
|
+
}
|
|
689
|
+
: {
|
|
690
|
+
label: "Agent runs for this log",
|
|
691
|
+
links: [],
|
|
692
|
+
emptyReason: actionMatchEmptyReason(actionContext),
|
|
693
|
+
},
|
|
694
|
+
];
|
|
695
|
+
}
|
|
696
|
+
if (sessionId) {
|
|
697
|
+
const sessionManifest = await index.bySession(projectId, sessionId);
|
|
698
|
+
manifest.across.push(
|
|
699
|
+
linksFromUsage(
|
|
700
|
+
sessionManifest.usageEvents,
|
|
701
|
+
"Usage events in this session",
|
|
702
|
+
),
|
|
703
|
+
);
|
|
704
|
+
if (sessionManifest.replay) {
|
|
705
|
+
manifest.across.push({
|
|
706
|
+
label: "Replay",
|
|
707
|
+
links: [linkToSession(sessionId)],
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
} else if (kind === "usage" || kind === "replay") {
|
|
712
|
+
// Usage / replay events are session-scoped — load by session_id
|
|
713
|
+
// from the query string.
|
|
714
|
+
const sessionId = c.req.query("session_id") ?? id;
|
|
715
|
+
const sessionManifest = await index.bySession(projectId, sessionId);
|
|
716
|
+
manifest.up = [
|
|
717
|
+
{
|
|
718
|
+
label: "Session",
|
|
719
|
+
links: [linkToSession(sessionId)],
|
|
720
|
+
},
|
|
721
|
+
];
|
|
722
|
+
manifest.across = [
|
|
723
|
+
linksFromSpans(sessionManifest.spans, "Spans in this session"),
|
|
724
|
+
linksFromLogs(sessionManifest.logs, "Logs in this session"),
|
|
725
|
+
linksFromAi(sessionManifest.aiCalls, "AI calls in this session"),
|
|
726
|
+
];
|
|
727
|
+
manifest.related = [
|
|
728
|
+
sessionManifest.replay
|
|
729
|
+
? {
|
|
730
|
+
label: "Replay",
|
|
731
|
+
links: [linkToSession(sessionId)],
|
|
732
|
+
}
|
|
733
|
+
: {
|
|
734
|
+
label: "Replay",
|
|
735
|
+
links: [],
|
|
736
|
+
emptyReason: "No rrweb replay recorded for this session.",
|
|
737
|
+
},
|
|
738
|
+
];
|
|
739
|
+
} else if (kind === "ai_call") {
|
|
740
|
+
const db = sqlDbFor(c.env);
|
|
741
|
+
const callRow = await db
|
|
742
|
+
.prepare(
|
|
743
|
+
`SELECT trace_id, span_id, session_id
|
|
744
|
+
FROM ai_calls
|
|
745
|
+
WHERE project_id = ? AND call_id = ?
|
|
746
|
+
LIMIT 1`,
|
|
747
|
+
)
|
|
748
|
+
.bind(projectId, id)
|
|
749
|
+
.first<{
|
|
750
|
+
trace_id: string | null;
|
|
751
|
+
span_id: string | null;
|
|
752
|
+
session_id: string | null;
|
|
753
|
+
}>();
|
|
754
|
+
const traceId =
|
|
755
|
+
c.req.query("trace_id") ?? callRow?.trace_id ?? undefined;
|
|
756
|
+
const spanId = c.req.query("span_id") ?? callRow?.span_id ?? undefined;
|
|
757
|
+
if (traceId) {
|
|
758
|
+
const traceManifest = await index.byTrace(projectId, traceId);
|
|
759
|
+
const actionContext = await loadSignalActionContext(
|
|
760
|
+
db,
|
|
761
|
+
projectId,
|
|
762
|
+
traceId,
|
|
763
|
+
spanId,
|
|
764
|
+
);
|
|
765
|
+
manifest.up = [
|
|
766
|
+
{
|
|
767
|
+
label: "Trace",
|
|
768
|
+
links: [linkToTrace(traceId, "Parent trace")],
|
|
769
|
+
},
|
|
770
|
+
];
|
|
771
|
+
manifest.across = [
|
|
772
|
+
linksFromSpans(traceManifest.spans, "Spans in this trace"),
|
|
773
|
+
linksFromAi(
|
|
774
|
+
traceManifest.aiCalls.filter((a) => a.callId !== id),
|
|
775
|
+
"Other AI calls in this trace",
|
|
776
|
+
),
|
|
777
|
+
linksFromMetricExemplars(
|
|
778
|
+
traceManifest.metricExemplars,
|
|
779
|
+
"Metric exemplars in this trace",
|
|
780
|
+
),
|
|
781
|
+
];
|
|
782
|
+
manifest.down = [
|
|
783
|
+
linksFromActions(
|
|
784
|
+
actionContext.actions,
|
|
785
|
+
actionContext.matchLevel === "span"
|
|
786
|
+
? "Causal actions for this AI call"
|
|
787
|
+
: "Trace-level actions for this AI call",
|
|
788
|
+
actionContextLinkMetadata(actionContext, "AI call"),
|
|
789
|
+
),
|
|
790
|
+
linksFromToolCalls(
|
|
791
|
+
actionContext.toolCalls,
|
|
792
|
+
actionContext.matchLevel === "span"
|
|
793
|
+
? "Tool calls for this AI call"
|
|
794
|
+
: "Trace-level tool calls for this AI call",
|
|
795
|
+
metadataForSignalActions(actionContext, "AI call"),
|
|
796
|
+
),
|
|
797
|
+
linksFromEvalResults(
|
|
798
|
+
actionContext.evalResults,
|
|
799
|
+
actionContext.matchLevel === "span"
|
|
800
|
+
? "Evaluations for this AI call"
|
|
801
|
+
: "Trace-level evaluations for this AI call",
|
|
802
|
+
metadataForSignalActions(actionContext, "AI call"),
|
|
803
|
+
),
|
|
804
|
+
];
|
|
805
|
+
manifest.related = [
|
|
806
|
+
actionContext.agentRuns.length > 0
|
|
807
|
+
? {
|
|
808
|
+
label:
|
|
809
|
+
actionContext.matchLevel === "span"
|
|
810
|
+
? "Agent runs for this AI call"
|
|
811
|
+
: "Trace-level agent runs for this AI call",
|
|
812
|
+
links: actionContext.agentRuns
|
|
813
|
+
.slice(0, MAX_LINKS_INLINE)
|
|
814
|
+
.map((run) => ({
|
|
815
|
+
...linkToAgentRun(
|
|
816
|
+
run.id,
|
|
817
|
+
`${run.agentName} (v${run.agentVersion})`,
|
|
818
|
+
),
|
|
819
|
+
...actionContextLinkMetadata(actionContext, "AI call"),
|
|
820
|
+
})),
|
|
821
|
+
}
|
|
822
|
+
: {
|
|
823
|
+
label: "Agent runs for this AI call",
|
|
824
|
+
links: [],
|
|
825
|
+
emptyReason: actionMatchEmptyReason(actionContext),
|
|
826
|
+
},
|
|
827
|
+
];
|
|
828
|
+
}
|
|
829
|
+
const sessionId =
|
|
830
|
+
c.req.query("session_id") ?? callRow?.session_id ?? undefined;
|
|
831
|
+
if (sessionId) {
|
|
832
|
+
manifest.across.push({
|
|
833
|
+
label: "Session",
|
|
834
|
+
links: [linkToSession(sessionId)],
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
} else if (kind === "user") {
|
|
838
|
+
// RFC 0006 Scenario B — pivot from a user. Loads the user's
|
|
839
|
+
// most recent sessions and surfaces top-line activity in
|
|
840
|
+
// `across`. The "first" session is the canonical "latest
|
|
841
|
+
// session" link used by the AI-cost-spike walkthrough.
|
|
842
|
+
const userManifest = await index.byUser(projectId, id, {
|
|
843
|
+
sessions: 5,
|
|
844
|
+
limit: 50,
|
|
845
|
+
});
|
|
846
|
+
const recentSessions = Array.from(
|
|
847
|
+
new Set(
|
|
848
|
+
userManifest.usageEvents
|
|
849
|
+
.map((e) => e.sessionId)
|
|
850
|
+
.filter((s): s is string => Boolean(s)),
|
|
851
|
+
),
|
|
852
|
+
);
|
|
853
|
+
manifest.up = [
|
|
854
|
+
{
|
|
855
|
+
label: "User",
|
|
856
|
+
links: [],
|
|
857
|
+
emptyReason: "User is the root identity — no parent context above.",
|
|
858
|
+
},
|
|
859
|
+
];
|
|
860
|
+
if (recentSessions.length > 0) {
|
|
861
|
+
manifest.across = [
|
|
862
|
+
{
|
|
863
|
+
label: "Latest session",
|
|
864
|
+
links: [linkToSession(recentSessions[0])],
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
label: "Recent sessions",
|
|
868
|
+
links: recentSessions
|
|
869
|
+
.slice(0, MAX_LINKS_INLINE)
|
|
870
|
+
.map((sid) => linkToSession(sid)),
|
|
871
|
+
},
|
|
872
|
+
linksFromSpans(userManifest.spans, "Recent traces"),
|
|
873
|
+
linksFromAi(userManifest.aiCalls, "Recent AI calls"),
|
|
874
|
+
];
|
|
875
|
+
} else {
|
|
876
|
+
manifest.across = [
|
|
877
|
+
{
|
|
878
|
+
label: "Sessions",
|
|
879
|
+
links: [],
|
|
880
|
+
emptyReason:
|
|
881
|
+
"No sessions found for this user. user_profiles needs a visitor_id, and that visitor needs at least one usage_event with a session_id.",
|
|
882
|
+
},
|
|
883
|
+
];
|
|
884
|
+
}
|
|
885
|
+
if (userManifest.replay) {
|
|
886
|
+
manifest.related = [
|
|
887
|
+
{
|
|
888
|
+
label: "Replay",
|
|
889
|
+
links: [linkToSession(userManifest.replay.sessionId)],
|
|
890
|
+
},
|
|
891
|
+
];
|
|
892
|
+
}
|
|
893
|
+
} else if (kind === "action") {
|
|
894
|
+
const actionManifest = await index.byAction(projectId, id);
|
|
895
|
+
const action = actionManifest.actions.find((a) => a.id === id);
|
|
896
|
+
if (action) {
|
|
897
|
+
manifest.up = [];
|
|
898
|
+
if (action.causedByActionId) {
|
|
899
|
+
manifest.up.push({
|
|
900
|
+
label: "Parent Action",
|
|
901
|
+
links: [linkToAction(action.causedByActionId)],
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
if (action.agentRunId) {
|
|
905
|
+
manifest.up.push({
|
|
906
|
+
label: "Agent Run",
|
|
907
|
+
links: [linkToAgentRun(action.agentRunId)],
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
if (manifest.up.length === 0) {
|
|
911
|
+
manifest.up.push({
|
|
912
|
+
label: "Up",
|
|
913
|
+
links: [],
|
|
914
|
+
emptyReason:
|
|
915
|
+
"No parent context: this action is the root of its execution tree.",
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
manifest.across = [
|
|
920
|
+
linksFromActions(
|
|
921
|
+
actionManifest.actions.filter(
|
|
922
|
+
(a) =>
|
|
923
|
+
a.id !== id && a.causedByActionId === action.causedByActionId,
|
|
924
|
+
),
|
|
925
|
+
"Sibling Actions",
|
|
926
|
+
),
|
|
927
|
+
linksFromLogs(actionManifest.logs, "Logs in this action context"),
|
|
928
|
+
linksFromAi(actionManifest.aiCalls, "AI Calls in this action"),
|
|
929
|
+
];
|
|
930
|
+
|
|
931
|
+
manifest.down = [
|
|
932
|
+
linksFromActions(
|
|
933
|
+
actionManifest.actions.filter((a) => a.causedByActionId === id),
|
|
934
|
+
"Sub-actions",
|
|
935
|
+
),
|
|
936
|
+
linksFromToolCalls(
|
|
937
|
+
actionManifest.toolCalls.filter((t) => t.actionId === id),
|
|
938
|
+
"Tool Calls",
|
|
939
|
+
),
|
|
940
|
+
linksFromRetrievalEvents(
|
|
941
|
+
actionManifest.retrievalEvents.filter((r) => r.actionId === id),
|
|
942
|
+
"Retrievals",
|
|
943
|
+
),
|
|
944
|
+
linksFromEvalResults(
|
|
945
|
+
actionManifest.evalResults.filter((e) => e.actionId === id),
|
|
946
|
+
"Evaluations",
|
|
947
|
+
),
|
|
948
|
+
linksFromArtifacts(
|
|
949
|
+
actionManifest.artifacts.filter((a) => a.actionId === id),
|
|
950
|
+
"Artifacts",
|
|
951
|
+
),
|
|
952
|
+
];
|
|
953
|
+
|
|
954
|
+
if (action.traceId) {
|
|
955
|
+
manifest.related = [
|
|
956
|
+
{
|
|
957
|
+
label: "OTel Trace",
|
|
958
|
+
links: [linkToTrace(action.traceId)],
|
|
959
|
+
},
|
|
960
|
+
];
|
|
961
|
+
}
|
|
962
|
+
manifest.rawManifest = actionManifest;
|
|
963
|
+
}
|
|
964
|
+
} else if (kind === "agent_run") {
|
|
965
|
+
const runManifest = await index.byAgentRun(projectId, id);
|
|
966
|
+
const run = runManifest.agentRuns.find((r) => r.id === id);
|
|
967
|
+
if (run) {
|
|
968
|
+
manifest.up = [
|
|
969
|
+
{
|
|
970
|
+
label: "Agent",
|
|
971
|
+
links: [
|
|
972
|
+
{
|
|
973
|
+
label: `${run.agentName} (v${run.agentVersion})`,
|
|
974
|
+
href: `#/agents/${run.agentId}`,
|
|
975
|
+
},
|
|
976
|
+
],
|
|
977
|
+
},
|
|
978
|
+
];
|
|
979
|
+
|
|
980
|
+
manifest.across = [
|
|
981
|
+
linksFromSpans(runManifest.spans, "Traces triggered by this run"),
|
|
982
|
+
linksFromAi(runManifest.aiCalls, "AI Calls in this run"),
|
|
983
|
+
];
|
|
984
|
+
|
|
985
|
+
manifest.down = [
|
|
986
|
+
linksFromActions(
|
|
987
|
+
runManifest.actions.filter((a) => a.rootActionId === id),
|
|
988
|
+
"Actions (Decision Spine)",
|
|
989
|
+
),
|
|
990
|
+
linksFromToolCalls(runManifest.toolCalls, "Tool Calls Executed"),
|
|
991
|
+
];
|
|
992
|
+
|
|
993
|
+
if (runManifest.replay) {
|
|
994
|
+
manifest.related = [
|
|
995
|
+
{
|
|
996
|
+
label: "User Session",
|
|
997
|
+
links: [linkToSession(runManifest.replay.sessionId)],
|
|
998
|
+
},
|
|
999
|
+
];
|
|
1000
|
+
}
|
|
1001
|
+
manifest.rawManifest = runManifest;
|
|
1002
|
+
}
|
|
1003
|
+
} else if (kind === "tool_call") {
|
|
1004
|
+
const db = sqlDbFor(c.env);
|
|
1005
|
+
const toolCallRow = await db
|
|
1006
|
+
.prepare(
|
|
1007
|
+
`SELECT action_id, tool_name FROM tool_calls WHERE project_id = ? AND id = ? LIMIT 1`,
|
|
1008
|
+
)
|
|
1009
|
+
.bind(projectId, id)
|
|
1010
|
+
.first<{ action_id: string; tool_name: string }>();
|
|
1011
|
+
|
|
1012
|
+
if (toolCallRow) {
|
|
1013
|
+
const actionManifest = await index.byAction(
|
|
1014
|
+
projectId,
|
|
1015
|
+
toolCallRow.action_id,
|
|
1016
|
+
);
|
|
1017
|
+
const toolCall = actionManifest.toolCalls.find((t) => t.id === id);
|
|
1018
|
+
if (toolCall) {
|
|
1019
|
+
manifest.up = [
|
|
1020
|
+
{
|
|
1021
|
+
label: "Causal Action",
|
|
1022
|
+
links: [linkToAction(toolCall.actionId)],
|
|
1023
|
+
},
|
|
1024
|
+
];
|
|
1025
|
+
|
|
1026
|
+
manifest.across = [
|
|
1027
|
+
linksFromToolCalls(
|
|
1028
|
+
actionManifest.toolCalls.filter((t) => t.id !== id),
|
|
1029
|
+
"Other tool calls in this action",
|
|
1030
|
+
),
|
|
1031
|
+
];
|
|
1032
|
+
|
|
1033
|
+
manifest.down = [
|
|
1034
|
+
{
|
|
1035
|
+
label: "Tool Details",
|
|
1036
|
+
links: [],
|
|
1037
|
+
emptyReason: `Tool: ${toolCall.toolName} (hash: ${toolCall.argsHash.slice(0, 8)})`,
|
|
1038
|
+
},
|
|
1039
|
+
];
|
|
1040
|
+
manifest.rawManifest = actionManifest;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
} else if (kind === "alert" || kind === "analysis") {
|
|
1044
|
+
// Alerts and Analyses are topic-related, not identity-related.
|
|
1045
|
+
// Their connections come from the alert_rules.analysis_id and
|
|
1046
|
+
// analysis_results join, not from the identity graph.
|
|
1047
|
+
// For now, return empty-explanatory sections. A follow-up
|
|
1048
|
+
// can route through AlertsStore + AnalysesStore for the
|
|
1049
|
+
// topic links.
|
|
1050
|
+
manifest.related = [
|
|
1051
|
+
{
|
|
1052
|
+
label: "Topic neighbors",
|
|
1053
|
+
links: [],
|
|
1054
|
+
emptyReason:
|
|
1055
|
+
"Alert/Analysis topic links are surfaced inside their detail views directly — the connected rail focuses on identity-graph neighbors here.",
|
|
1056
|
+
},
|
|
1057
|
+
];
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Always at least set placeholder labels so the rail renders
|
|
1061
|
+
// section headers consistently across kinds.
|
|
1062
|
+
if (manifest.up.length === 0) {
|
|
1063
|
+
manifest.up = [
|
|
1064
|
+
{
|
|
1065
|
+
label: "Up",
|
|
1066
|
+
links: [],
|
|
1067
|
+
emptyReason: "No parent context for this entity.",
|
|
1068
|
+
},
|
|
1069
|
+
];
|
|
1070
|
+
}
|
|
1071
|
+
if (manifest.across.length === 0) {
|
|
1072
|
+
manifest.across = [
|
|
1073
|
+
{
|
|
1074
|
+
label: "Across",
|
|
1075
|
+
links: [],
|
|
1076
|
+
emptyReason: "No identity-graph peers under this entity.",
|
|
1077
|
+
},
|
|
1078
|
+
];
|
|
1079
|
+
}
|
|
1080
|
+
if (manifest.down.length === 0) {
|
|
1081
|
+
manifest.down = [
|
|
1082
|
+
{
|
|
1083
|
+
label: "Down",
|
|
1084
|
+
links: [],
|
|
1085
|
+
emptyReason:
|
|
1086
|
+
"No child entities yet (profiles + kernel events arrive with later RFCs).",
|
|
1087
|
+
},
|
|
1088
|
+
];
|
|
1089
|
+
}
|
|
1090
|
+
if (manifest.related.length === 0) {
|
|
1091
|
+
manifest.related = [
|
|
1092
|
+
{
|
|
1093
|
+
label: "Related",
|
|
1094
|
+
links: [],
|
|
1095
|
+
emptyReason: "No topic neighbors found.",
|
|
1096
|
+
},
|
|
1097
|
+
];
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return c.json(manifest);
|
|
1101
|
+
});
|
|
1102
|
+
},
|
|
1103
|
+
};
|