@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.
Files changed (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/auth.d.ts +2 -0
  4. package/dist/auth.js +9 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/chunk-UGWDOGGH.js +319 -0
  7. package/dist/chunk-UGWDOGGH.js.map +1 -0
  8. package/dist/chunk-XKQTVIRE.js +671 -0
  9. package/dist/chunk-XKQTVIRE.js.map +1 -0
  10. package/dist/index.d.ts +1197 -0
  11. package/dist/index.js +17707 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/ingest-auth-HFVO1zIp.d.ts +165 -0
  14. package/dist/openai-FJYOQ7WO.js +384 -0
  15. package/dist/openai-FJYOQ7WO.js.map +1 -0
  16. package/package.json +73 -0
  17. package/src/ai-parity/ai-parity.acceptance.test.ts +449 -0
  18. package/src/analyses/derive.ts +642 -0
  19. package/src/analyses/index.ts +50 -0
  20. package/src/analyses/investigations.ts +277 -0
  21. package/src/analyses/tier0.ts +568 -0
  22. package/src/auth/dashboard-auth.ts +269 -0
  23. package/src/auth/ingest-auth.ts +147 -0
  24. package/src/durable-objects/tail-hub.test.ts +32 -0
  25. package/src/durable-objects/tail-hub.ts +158 -0
  26. package/src/framework/collector.ts +467 -0
  27. package/src/framework/env.ts +90 -0
  28. package/src/framework/logger.ts +81 -0
  29. package/src/index.ts +211 -0
  30. package/src/lib/action-aggregates.ts +1031 -0
  31. package/src/lib/action-identity.ts +106 -0
  32. package/src/lib/actions-store.ts +397 -0
  33. package/src/lib/ai-pricing.ts +80 -0
  34. package/src/lib/ai-store/calls.ts +148 -0
  35. package/src/lib/ai-store/evaluations.test.ts +47 -0
  36. package/src/lib/ai-store/evaluations.ts +125 -0
  37. package/src/lib/ai-store/helpers.ts +37 -0
  38. package/src/lib/ai-store/retention.ts +17 -0
  39. package/src/lib/ai-store/sessions.ts +277 -0
  40. package/src/lib/ai-store/spans.ts +109 -0
  41. package/src/lib/ai-store/types.ts +112 -0
  42. package/src/lib/ai-store.ts +76 -0
  43. package/src/lib/alerts-store.ts +458 -0
  44. package/src/lib/analyses-runner.ts +439 -0
  45. package/src/lib/analyses-store.test.ts +243 -0
  46. package/src/lib/analyses-store.ts +454 -0
  47. package/src/lib/ask.test.ts +302 -0
  48. package/src/lib/ask.ts +390 -0
  49. package/src/lib/blob-store-s3.ts +213 -0
  50. package/src/lib/blob-store.ts +366 -0
  51. package/src/lib/eval-cases-store.test.ts +171 -0
  52. package/src/lib/eval-cases-store.ts +1033 -0
  53. package/src/lib/evidence-references.test.ts +10 -0
  54. package/src/lib/evidence-references.ts +684 -0
  55. package/src/lib/hash.ts +26 -0
  56. package/src/lib/identity-index/action-lookups.ts +805 -0
  57. package/src/lib/identity-index/constants.ts +1 -0
  58. package/src/lib/identity-index/key-lookups.ts +199 -0
  59. package/src/lib/identity-index/mappers.ts +349 -0
  60. package/src/lib/identity-index/types.ts +196 -0
  61. package/src/lib/identity-index/user-lookup.ts +137 -0
  62. package/src/lib/identity-index.test.ts +624 -0
  63. package/src/lib/identity-index.ts +85 -0
  64. package/src/lib/json.ts +39 -0
  65. package/src/lib/llm.ts +263 -0
  66. package/src/lib/logs-store.ts +188 -0
  67. package/src/lib/metrics-store.test.ts +189 -0
  68. package/src/lib/metrics-store.ts +312 -0
  69. package/src/lib/narrate-gate.test.ts +260 -0
  70. package/src/lib/narrate-gate.ts +253 -0
  71. package/src/lib/openai.test.ts +300 -0
  72. package/src/lib/openai.ts +512 -0
  73. package/src/lib/otlp.ts +212 -0
  74. package/src/lib/parse-pprof.test.ts +64 -0
  75. package/src/lib/parse-pprof.ts +184 -0
  76. package/src/lib/payload-capture.ts +22 -0
  77. package/src/lib/projects-store.ts +207 -0
  78. package/src/lib/propagation-metric.test.ts +196 -0
  79. package/src/lib/propagation-metric.ts +175 -0
  80. package/src/lib/sql-db-dialect.test.ts +22 -0
  81. package/src/lib/sql-db-postgres.ts +229 -0
  82. package/src/lib/sql-db.ts +166 -0
  83. package/src/lib/stage6.test.ts +216 -0
  84. package/src/lib/store/export.ts +65 -0
  85. package/src/lib/store/helpers.ts +367 -0
  86. package/src/lib/store/ingest.ts +68 -0
  87. package/src/lib/store/issues.ts +158 -0
  88. package/src/lib/store/overview.ts +127 -0
  89. package/src/lib/store/retention.ts +10 -0
  90. package/src/lib/store/service-map.ts +384 -0
  91. package/src/lib/store/trace-detail.bench.ts +76 -0
  92. package/src/lib/store/trace-detail.test.ts +410 -0
  93. package/src/lib/store/trace-detail.ts +427 -0
  94. package/src/lib/store/trace-queries.test.ts +29 -0
  95. package/src/lib/store/trace-queries.ts +89 -0
  96. package/src/lib/store-overview.test.ts +77 -0
  97. package/src/lib/store.ts +96 -0
  98. package/src/lib/tail-publisher.ts +64 -0
  99. package/src/lib/test-utils/mem-sql-db.test.ts +93 -0
  100. package/src/lib/test-utils/mem-sql-db.ts +86 -0
  101. package/src/lib/ua-parser.ts +61 -0
  102. package/src/lib/usage-store.ts +567 -0
  103. package/src/migrations/001_telemetry_spans.sql +28 -0
  104. package/src/migrations/002_usage_events.sql +24 -0
  105. package/src/migrations/003_usage_analytics_columns.sql +13 -0
  106. package/src/migrations/004_logs.sql +19 -0
  107. package/src/migrations/005_ai_calls.sql +25 -0
  108. package/src/migrations/006_user_profiles.sql +11 -0
  109. package/src/migrations/007_session_replay_metadata.sql +10 -0
  110. package/src/migrations/008_session_replay_storage_bytes.sql +1 -0
  111. package/src/migrations/009_projects_and_keys.sql +22 -0
  112. package/src/migrations/010_signal_project_id.sql +15 -0
  113. package/src/migrations/011_default_project_seed.sql +3 -0
  114. package/src/migrations/012_alert_rules.sql +17 -0
  115. package/src/migrations/013_alert_evaluations.sql +13 -0
  116. package/src/migrations/014_alert_state.sql +9 -0
  117. package/src/migrations/015_metrics.sql +58 -0
  118. package/src/migrations/016_metrics_exp_histogram_summary.sql +5 -0
  119. package/src/migrations/017_span_dropped_counts.sql +11 -0
  120. package/src/migrations/018_log_flags.sql +8 -0
  121. package/src/migrations/019_ai_span_payloads.sql +30 -0
  122. package/src/migrations/020_ai_span_evaluations.sql +33 -0
  123. package/src/migrations/021_ai_span_session.sql +13 -0
  124. package/src/migrations/022_span_log_session_id.sql +13 -0
  125. package/src/migrations/023_service_map_indexes.sql +32 -0
  126. package/src/migrations/024_analysis_results.sql +49 -0
  127. package/src/migrations/025_stage6_autopinning_alerts.sql +38 -0
  128. package/src/migrations/026_obs_dashboard_project_seed.sql +18 -0
  129. package/src/migrations/027_identity_propagation.sql +50 -0
  130. package/src/migrations/028_profile_blobs.sql +57 -0
  131. package/src/migrations/029_telemetry_sdk_name.sql +20 -0
  132. package/src/migrations/030_analysis_claim.sql +18 -0
  133. package/src/migrations/031_agent_action_graph.sql +154 -0
  134. package/src/migrations/032_payload_capture_flags.sql +8 -0
  135. package/src/migrations/033_eval_cases.sql +52 -0
  136. package/src/migrations/034_eval_case_results.sql +23 -0
  137. package/src/migrations/035_metric_exemplars.sql +33 -0
  138. package/src/migrations/036_trace_instrumentation_gaps.sql +19 -0
  139. package/src/migrations/037_eval_runs.sql +37 -0
  140. package/src/migrations/038_tool_call_evidence.sql +11 -0
  141. package/src/migrations/039_drop_trace_instrumentation_gaps.sql +11 -0
  142. package/src/migrations/040_evidence_retrieval_refs.sql +50 -0
  143. package/src/migrations-postgres/001_telemetry_spans.sql +33 -0
  144. package/src/migrations-postgres/002_usage_events.sql +24 -0
  145. package/src/migrations-postgres/003_usage_analytics_columns.sql +13 -0
  146. package/src/migrations-postgres/004_logs.sql +19 -0
  147. package/src/migrations-postgres/005_ai_calls.sql +25 -0
  148. package/src/migrations-postgres/006_user_profiles.sql +11 -0
  149. package/src/migrations-postgres/007_session_replay_metadata.sql +10 -0
  150. package/src/migrations-postgres/008_session_replay_storage_bytes.sql +2 -0
  151. package/src/migrations-postgres/009_projects_and_keys.sql +25 -0
  152. package/src/migrations-postgres/010_signal_project_id.sql +18 -0
  153. package/src/migrations-postgres/011_default_project_seed.sql +5 -0
  154. package/src/migrations-postgres/012_alert_rules.sql +17 -0
  155. package/src/migrations-postgres/013_alert_evaluations.sql +13 -0
  156. package/src/migrations-postgres/014_alert_state.sql +9 -0
  157. package/src/migrations-postgres/015_metrics.sql +47 -0
  158. package/src/migrations-postgres/016_metrics_exp_histogram_summary.sql +1 -0
  159. package/src/migrations-postgres/017_span_dropped_counts.sql +4 -0
  160. package/src/migrations-postgres/018_log_flags.sql +2 -0
  161. package/src/migrations-postgres/019_ai_span_payloads.sql +18 -0
  162. package/src/migrations-postgres/020_ai_span_evaluations.sql +21 -0
  163. package/src/migrations-postgres/021_ai_span_session.sql +7 -0
  164. package/src/migrations-postgres/022_span_log_session_id.sql +7 -0
  165. package/src/migrations-postgres/023_service_map_indexes.sql +9 -0
  166. package/src/migrations-postgres/024_analysis_results.sql +41 -0
  167. package/src/migrations-postgres/025_stage6_autopinning_alerts.sql +17 -0
  168. package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +14 -0
  169. package/src/migrations-postgres/027_identity_propagation.sql +27 -0
  170. package/src/migrations-postgres/028_profile_blobs.sql +48 -0
  171. package/src/migrations-postgres/029_telemetry_sdk_name.sql +5 -0
  172. package/src/migrations-postgres/030_analysis_claim.sql +5 -0
  173. package/src/migrations-postgres/031_agent_action_graph.sql +190 -0
  174. package/src/migrations-postgres/032_payload_capture_flags.sql +8 -0
  175. package/src/migrations-postgres/033_eval_cases.sql +55 -0
  176. package/src/migrations-postgres/034_eval_case_results.sql +26 -0
  177. package/src/migrations-postgres/035_metric_exemplars.sql +31 -0
  178. package/src/migrations-postgres/036_trace_instrumentation_gaps.sql +19 -0
  179. package/src/migrations-postgres/037_eval_runs.sql +40 -0
  180. package/src/migrations-postgres/038_tool_call_evidence.sql +11 -0
  181. package/src/migrations-postgres/039_drop_trace_instrumentation_gaps.sql +11 -0
  182. package/src/migrations-postgres/040_evidence_retrieval_refs.sql +47 -0
  183. package/src/migrations-postgres/README.md +40 -0
  184. package/src/otlp/acceptance.test.ts +373 -0
  185. package/src/otlp/decode/body.ts +127 -0
  186. package/src/otlp/decode/logs.ts +96 -0
  187. package/src/otlp/decode/metrics.ts +402 -0
  188. package/src/otlp/decode/traces.ts +92 -0
  189. package/src/otlp/decode/values.ts +150 -0
  190. package/src/otlp/decode.test.ts +800 -0
  191. package/src/otlp/decode.ts +11 -0
  192. package/src/otlp/gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.ts +132 -0
  193. package/src/otlp/gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.ts +158 -0
  194. package/src/otlp/gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.ts +158 -0
  195. package/src/otlp/gen/opentelemetry/proto/common/v1/common_pb.ts +289 -0
  196. package/src/otlp/gen/opentelemetry/proto/logs/v1/logs_pb.ts +397 -0
  197. package/src/otlp/gen/opentelemetry/proto/metrics/v1/metrics_pb.ts +1329 -0
  198. package/src/otlp/gen/opentelemetry/proto/resource/v1/resource_pb.ts +86 -0
  199. package/src/otlp/gen/opentelemetry/proto/trace/v1/trace_pb.ts +710 -0
  200. package/src/otlp/response.test.ts +158 -0
  201. package/src/otlp/response.ts +129 -0
  202. package/src/plugins/_context.ts +15 -0
  203. package/src/plugins/action-graph-processor/enrichers.ts +45 -0
  204. package/src/plugins/action-graph-processor/redaction.ts +84 -0
  205. package/src/plugins/action-graph-processor.test.ts +1023 -0
  206. package/src/plugins/action-graph-processor.ts +1172 -0
  207. package/src/plugins/action-routes.test.ts +716 -0
  208. package/src/plugins/action-routes.ts +343 -0
  209. package/src/plugins/ai-receiver.ts +254 -0
  210. package/src/plugins/ai-span-payloads-processor.test.ts +90 -0
  211. package/src/plugins/ai-span-payloads-processor.ts +180 -0
  212. package/src/plugins/alerts-evaluator.test.ts +91 -0
  213. package/src/plugins/alerts-evaluator.ts +328 -0
  214. package/src/plugins/alerts-routes.ts +101 -0
  215. package/src/plugins/analyses-routes.ts +138 -0
  216. package/src/plugins/ask-routes.ts +182 -0
  217. package/src/plugins/bot-filter.ts +19 -0
  218. package/src/plugins/connected-routes/manifest.ts +531 -0
  219. package/src/plugins/connected-routes.test.ts +1791 -0
  220. package/src/plugins/connected-routes.ts +1103 -0
  221. package/src/plugins/dashboard-routes.ts +41 -0
  222. package/src/plugins/default-span-enrichment.ts +92 -0
  223. package/src/plugins/eval-cases-routes.test.ts +391 -0
  224. package/src/plugins/eval-cases-routes.ts +529 -0
  225. package/src/plugins/evidence-retrieval-routes.benchmark.test.ts +245 -0
  226. package/src/plugins/evidence-retrieval-routes.test.ts +932 -0
  227. package/src/plugins/evidence-retrieval-routes.ts +2179 -0
  228. package/src/plugins/gen-ai-normalizer.test.ts +680 -0
  229. package/src/plugins/gen-ai-normalizer.ts +302 -0
  230. package/src/plugins/identity-receiver.ts +96 -0
  231. package/src/plugins/issue-enrichment.ts +132 -0
  232. package/src/plugins/issue-insights.ts +81 -0
  233. package/src/plugins/logs-receiver.ts +160 -0
  234. package/src/plugins/metrics-receiver.ts +91 -0
  235. package/src/plugins/onboarding-routes.ts +54 -0
  236. package/src/plugins/otlp-receiver.ts +124 -0
  237. package/src/plugins/platform-routes.ts +165 -0
  238. package/src/plugins/profile-routes.ts +468 -0
  239. package/src/plugins/projects-routes.ts +92 -0
  240. package/src/plugins/query-routes.ts +220 -0
  241. package/src/plugins/redaction-processor.ts +132 -0
  242. package/src/plugins/replay-query-routes.test.ts +38 -0
  243. package/src/plugins/replay-query-routes.ts +199 -0
  244. package/src/plugins/replay-receiver.ts +81 -0
  245. package/src/plugins/tail-routes.test.ts +47 -0
  246. package/src/plugins/tail-routes.ts +42 -0
  247. package/src/plugins/timeline-routes.ts +244 -0
  248. package/src/plugins/ua-enrichment.ts +27 -0
  249. package/src/plugins/usage-normalization.ts +46 -0
  250. package/src/plugins/usage-privacy.ts +55 -0
  251. package/src/plugins/usage-query-routes.ts +102 -0
  252. package/src/plugins/usage-receiver.ts +129 -0
  253. package/src/plugins/users-query-routes.ts +78 -0
  254. package/src/plugins/utm-enrichment.ts +36 -0
@@ -0,0 +1,10 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { evidenceRouteFor } from "./evidence-references";
3
+
4
+ describe("evidenceRouteFor", () => {
5
+ it("routes analysis evidence to the canonical investigation page", () => {
6
+ expect(evidenceRouteFor("analysis", "latency.p95")).toBe(
7
+ "#/investigate/latency.p95",
8
+ );
9
+ });
10
+ });
@@ -0,0 +1,684 @@
1
+ import type {
2
+ AIEvaluationRecord,
3
+ AlertEvaluation,
4
+ AlertRule,
5
+ AnalysisResult,
6
+ AskEvidence,
7
+ EvidenceCitation,
8
+ EvidenceEntityKind,
9
+ EvidenceNextPivot,
10
+ EvidenceReference,
11
+ JsonValue,
12
+ } from "@obsunified/types";
13
+
14
+ const analysisRoute = (analysisId: string) =>
15
+ `#/investigate/${encodeURIComponent(analysisId)}`;
16
+
17
+ export const analysisEvidenceReference = (
18
+ evidence: AskEvidence,
19
+ ): EvidenceReference => {
20
+ const route = analysisRoute(evidence.analysisId);
21
+ const status = evidence.result?.status ?? "unknown";
22
+ return {
23
+ evidenceId: `ask:analysis:${evidence.analysisId}`,
24
+ entityKind: "analysis",
25
+ entityId: evidence.analysisId,
26
+ route,
27
+ source: "ask.run_analysis",
28
+ confidence: evidence.result ? 1 : 0.55,
29
+ reason: evidence.result
30
+ ? `Ask consulted analysis "${evidence.definition.title}" with latest status ${status}.`
31
+ : `Ask consulted analysis "${evidence.definition.title}", but no latest result was available.`,
32
+ citations: [
33
+ {
34
+ label: evidence.definition.title,
35
+ entityKind: "analysis",
36
+ entityId: evidence.analysisId,
37
+ route,
38
+ },
39
+ ],
40
+ suggestedNextPivots: [
41
+ {
42
+ label: "Open investigation",
43
+ entityKind: "analysis",
44
+ entityId: evidence.analysisId,
45
+ route,
46
+ reason:
47
+ "Review the analysis payload, narrative, and connected telemetry rail.",
48
+ },
49
+ ],
50
+ };
51
+ };
52
+
53
+ export const askEvidenceReferences = (
54
+ evidence: Iterable<AskEvidence>,
55
+ ): EvidenceReference[] =>
56
+ [...evidence].map((item) => analysisEvidenceReference(item));
57
+
58
+ const MAX_ANALYSIS_EVIDENCE_REFERENCES = 50;
59
+
60
+ interface EvidenceTable {
61
+ title?: string | null;
62
+ headers: string[];
63
+ rows: JsonValue[][];
64
+ }
65
+
66
+ interface RowEntity {
67
+ entityKind: EvidenceEntityKind;
68
+ entityId: string;
69
+ route: string;
70
+ label: string;
71
+ confidence: number;
72
+ citations: EvidenceCitation[];
73
+ suggestedNextPivots: EvidenceNextPivot[];
74
+ }
75
+
76
+ const isRecord = (value: unknown): value is Record<string, JsonValue> =>
77
+ !!value && typeof value === "object" && !Array.isArray(value);
78
+
79
+ const asString = (value: JsonValue | undefined): string | null => {
80
+ if (typeof value === "string" && value.trim()) return value.trim();
81
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
82
+ return null;
83
+ };
84
+
85
+ const normalizeHeader = (header: string) =>
86
+ header
87
+ .trim()
88
+ .toLowerCase()
89
+ .replace(/[\s-]+/g, "_");
90
+
91
+ const valueFor = (
92
+ row: JsonValue[],
93
+ headerIndex: Map<string, number>,
94
+ names: string[],
95
+ ): string | null => {
96
+ for (const name of names) {
97
+ const index = headerIndex.get(name);
98
+ if (index === undefined) continue;
99
+ const value = asString(row[index]);
100
+ if (value) return value;
101
+ }
102
+ return null;
103
+ };
104
+
105
+ const traceRoute = (traceId: string) =>
106
+ `#/traces/${encodeURIComponent(traceId)}`;
107
+
108
+ const spanRoute = (traceId: string, spanId: string) =>
109
+ `${traceRoute(traceId)}#span=${encodeURIComponent(spanId)}`;
110
+
111
+ export const evidenceRouteFor = (
112
+ entityKind: EvidenceEntityKind,
113
+ entityId: string,
114
+ traceId?: string | null,
115
+ ) => {
116
+ switch (entityKind) {
117
+ case "analysis":
118
+ return analysisRoute(entityId);
119
+ case "action":
120
+ return `#/actions/${encodeURIComponent(entityId)}`;
121
+ case "alert":
122
+ return `#/alerts?alert=${encodeURIComponent(entityId)}`;
123
+ case "agent_run":
124
+ return `#/agent-runs/${encodeURIComponent(entityId)}`;
125
+ case "ai_call":
126
+ return `#/ai/calls/${encodeURIComponent(entityId)}`;
127
+ case "eval":
128
+ return `#/evals/${encodeURIComponent(entityId)}`;
129
+ case "log":
130
+ return `#/logs?id=${encodeURIComponent(entityId)}`;
131
+ case "profile":
132
+ return traceId
133
+ ? `#/profiles/${encodeURIComponent(entityId)}?trace_id=${encodeURIComponent(traceId)}`
134
+ : `#/profiles/${encodeURIComponent(entityId)}`;
135
+ case "replay":
136
+ return `#/replay?session=${encodeURIComponent(entityId)}`;
137
+ case "service":
138
+ return `#/traces?service=${encodeURIComponent(entityId)}`;
139
+ case "span": {
140
+ const [tracePart, spanPart] = entityId.split(":");
141
+ return spanPart ? spanRoute(tracePart, spanPart) : `#/spans/${entityId}`;
142
+ }
143
+ case "tool_call":
144
+ return `#/tool-calls/${encodeURIComponent(entityId)}`;
145
+ case "trace":
146
+ return traceRoute(entityId);
147
+ case "docs":
148
+ return entityId.startsWith("#/") ? entityId : `#/docs/${entityId}`;
149
+ }
150
+ const exhaustive: never = entityKind;
151
+ return exhaustive;
152
+ };
153
+
154
+ const parseEvidenceTables = (
155
+ payload: Record<string, unknown>,
156
+ ): Array<{ sectionName: string; table: EvidenceTable }> => {
157
+ const evidence = payload.evidence;
158
+ if (!isRecord(evidence)) return [];
159
+
160
+ const tables: Array<{ sectionName: string; table: EvidenceTable }> = [];
161
+ for (const [sectionName, section] of Object.entries(evidence)) {
162
+ if (!isRecord(section)) continue;
163
+ const headers = section.headers;
164
+ const rows = section.rows;
165
+ if (
166
+ !Array.isArray(headers) ||
167
+ !headers.every(
168
+ (header): header is string => typeof header === "string",
169
+ ) ||
170
+ !Array.isArray(rows)
171
+ ) {
172
+ continue;
173
+ }
174
+
175
+ const normalizedRows = rows.filter((row): row is JsonValue[] =>
176
+ Array.isArray(row),
177
+ );
178
+ tables.push({
179
+ sectionName,
180
+ table: {
181
+ title: asString(section.title),
182
+ headers,
183
+ rows: normalizedRows,
184
+ },
185
+ });
186
+ }
187
+ return tables;
188
+ };
189
+
190
+ const instrumentationGapEvidenceReferences = (
191
+ result: Pick<AnalysisResult, "analysisId" | "payload">,
192
+ ): EvidenceReference[] => {
193
+ const gaps = result.payload.instrumentationGaps;
194
+ if (!isRecord(gaps)) return [];
195
+ const traceId = asString(gaps.traceId);
196
+ const blindspots = gaps.blindspots;
197
+ if (!traceId || !Array.isArray(blindspots)) return [];
198
+
199
+ return blindspots
200
+ .filter(isRecord)
201
+ .map((gap, index): EvidenceReference | null => {
202
+ const spanId = asString(gap.parentSpanId);
203
+ if (!spanId) return null;
204
+ const serviceName = asString(gap.parentServiceName);
205
+ const spanName = asString(gap.parentSpanName);
206
+ const durationMs =
207
+ typeof gap.durationMs === "number" && Number.isFinite(gap.durationMs)
208
+ ? gap.durationMs
209
+ : null;
210
+ const ratio =
211
+ typeof gap.ratioOfParent === "number" &&
212
+ Number.isFinite(gap.ratioOfParent)
213
+ ? gap.ratioOfParent
214
+ : null;
215
+ const recommendation = asString(gap.recommendation);
216
+ const thresholdVersion =
217
+ asString(gap.thresholdVersion) ??
218
+ (isRecord(gaps.thresholds) ? asString(gaps.thresholds.version) : null);
219
+ const entityId = `${traceId}:${spanId}`;
220
+ const route = spanRoute(traceId, spanId);
221
+ const label = spanName ? `${spanName} (${spanId})` : `span ${spanId}`;
222
+ const durationText =
223
+ durationMs === null ? "unknown duration" : `${durationMs}ms`;
224
+ const ratioText =
225
+ ratio === null ? "" : `, ${(ratio * 100).toFixed(1)}% of parent`;
226
+ const thresholdText = thresholdVersion
227
+ ? ` Thresholds: ${thresholdVersion}.`
228
+ : "";
229
+ const docsRoute = "#/docs/howto/ebpf";
230
+ return {
231
+ evidenceId: `analysis:${result.analysisId}:instrumentation-gap:${index}:span:${entityId}`,
232
+ entityKind: "span",
233
+ entityId,
234
+ route,
235
+ source: "analysis.payload.instrumentationGaps",
236
+ confidence: 0.9,
237
+ reason: `Analysis "${result.analysisId}" found uninstrumented self-time on ${label}: ${durationText}${ratioText}.${thresholdText}${recommendation ? ` ${recommendation}` : ""}`,
238
+ citations: [
239
+ {
240
+ label: `trace ${traceId}`,
241
+ entityKind: "trace",
242
+ entityId: traceId,
243
+ route: traceRoute(traceId),
244
+ },
245
+ ],
246
+ suggestedNextPivots: [
247
+ {
248
+ label: "Open uninstrumented span",
249
+ entityKind: "span",
250
+ entityId,
251
+ route,
252
+ reason:
253
+ "Inspect the parent span whose self-time suggests missing child instrumentation.",
254
+ },
255
+ {
256
+ label: "Open trace gap data",
257
+ entityKind: "trace",
258
+ entityId: traceId,
259
+ route: `${traceRoute(traceId)}/gaps`,
260
+ reason:
261
+ "Review all structured instrumentation blindspots for this trace.",
262
+ },
263
+ {
264
+ label: "Open profiler setup docs",
265
+ entityKind: "docs",
266
+ entityId: "howto/ebpf",
267
+ route: docsRoute,
268
+ reason:
269
+ serviceName != null
270
+ ? `Use profiler or eBPF setup guidance to cover missing spans in ${serviceName}.`
271
+ : "Use profiler or eBPF setup guidance to cover missing spans.",
272
+ },
273
+ ],
274
+ };
275
+ })
276
+ .filter((ref): ref is EvidenceReference => ref !== null)
277
+ .slice(0, MAX_ANALYSIS_EVIDENCE_REFERENCES);
278
+ };
279
+
280
+ const entityFromRow = (
281
+ row: JsonValue[],
282
+ headers: string[],
283
+ ): RowEntity | null => {
284
+ const headerIndex = new Map(headers.map((header, index) => [header, index]));
285
+ const traceId = valueFor(row, headerIndex, ["trace_id", "traceid", "trace"]);
286
+ const spanId = valueFor(row, headerIndex, ["span_id", "spanid"]);
287
+ const logId = valueFor(row, headerIndex, ["log_id", "logid"]);
288
+ const profileId = valueFor(row, headerIndex, ["profile_id", "profileid"]);
289
+ const actionId = valueFor(row, headerIndex, ["action_id", "actionid"]);
290
+ const agentRunId = valueFor(row, headerIndex, [
291
+ "agent_run_id",
292
+ "agentrunid",
293
+ "agent_run",
294
+ ]);
295
+ const toolCallId = valueFor(row, headerIndex, [
296
+ "tool_call_id",
297
+ "toolcallid",
298
+ "tool_call",
299
+ ]);
300
+ const evalId = valueFor(row, headerIndex, ["eval_id", "evalid"]);
301
+ const service = valueFor(row, headerIndex, ["service", "service_name"]);
302
+
303
+ const traceCitation = traceId
304
+ ? [
305
+ {
306
+ label: `trace ${traceId}`,
307
+ entityKind: "trace" as const,
308
+ entityId: traceId,
309
+ route: traceRoute(traceId),
310
+ },
311
+ ]
312
+ : [];
313
+
314
+ if (traceId && spanId) {
315
+ const entityId = `${traceId}:${spanId}`;
316
+ return {
317
+ entityKind: "span",
318
+ entityId,
319
+ route: spanRoute(traceId, spanId),
320
+ label: `span ${spanId}`,
321
+ confidence: 0.95,
322
+ citations: traceCitation,
323
+ suggestedNextPivots: [
324
+ {
325
+ label: "Open span",
326
+ entityKind: "span",
327
+ entityId,
328
+ route: spanRoute(traceId, spanId),
329
+ reason: "Inspect the cited span in its trace context.",
330
+ },
331
+ {
332
+ label: "Open trace",
333
+ entityKind: "trace",
334
+ entityId: traceId,
335
+ route: traceRoute(traceId),
336
+ reason: "Review the surrounding trace context.",
337
+ },
338
+ ],
339
+ };
340
+ }
341
+
342
+ const candidates: Array<{
343
+ entityKind: EvidenceEntityKind;
344
+ entityId: string | null;
345
+ labelPrefix: string;
346
+ confidence: number;
347
+ }> = [
348
+ {
349
+ entityKind: "trace",
350
+ entityId: traceId,
351
+ labelPrefix: "trace",
352
+ confidence: 0.95,
353
+ },
354
+ {
355
+ entityKind: "action",
356
+ entityId: actionId,
357
+ labelPrefix: "action",
358
+ confidence: 0.9,
359
+ },
360
+ {
361
+ entityKind: "tool_call",
362
+ entityId: toolCallId,
363
+ labelPrefix: "tool call",
364
+ confidence: 0.9,
365
+ },
366
+ {
367
+ entityKind: "agent_run",
368
+ entityId: agentRunId,
369
+ labelPrefix: "agent run",
370
+ confidence: 0.9,
371
+ },
372
+ {
373
+ entityKind: "profile",
374
+ entityId: profileId,
375
+ labelPrefix: "profile",
376
+ confidence: 0.9,
377
+ },
378
+ {
379
+ entityKind: "log",
380
+ entityId: logId,
381
+ labelPrefix: "log",
382
+ confidence: 0.85,
383
+ },
384
+ {
385
+ entityKind: "eval",
386
+ entityId: evalId,
387
+ labelPrefix: "eval",
388
+ confidence: 0.85,
389
+ },
390
+ {
391
+ entityKind: "service",
392
+ entityId: service,
393
+ labelPrefix: "service",
394
+ confidence: 0.7,
395
+ },
396
+ ];
397
+
398
+ const candidate = candidates.find((item) => item.entityId);
399
+ if (!candidate?.entityId) return null;
400
+
401
+ const route = evidenceRouteFor(
402
+ candidate.entityKind,
403
+ candidate.entityId,
404
+ traceId,
405
+ );
406
+ return {
407
+ entityKind: candidate.entityKind,
408
+ entityId: candidate.entityId,
409
+ route,
410
+ label: `${candidate.labelPrefix} ${candidate.entityId}`,
411
+ confidence: candidate.confidence,
412
+ citations: traceCitation.filter(
413
+ (citation) =>
414
+ citation.entityKind !== candidate.entityKind ||
415
+ citation.entityId !== candidate.entityId,
416
+ ),
417
+ suggestedNextPivots: [
418
+ {
419
+ label: `Open ${candidate.labelPrefix}`,
420
+ entityKind: candidate.entityKind,
421
+ entityId: candidate.entityId,
422
+ route,
423
+ reason: "Inspect the cited evidence row.",
424
+ },
425
+ ],
426
+ };
427
+ };
428
+
429
+ export const alertEvidenceReferences = (
430
+ rule: AlertRule,
431
+ evaluation?: AlertEvaluation | null,
432
+ ): EvidenceReference[] => {
433
+ const route = evidenceRouteFor("alert", rule.id);
434
+ const references: EvidenceReference[] = [
435
+ {
436
+ evidenceId: evaluation
437
+ ? `alert:${rule.id}:evaluation:${evaluation.id}`
438
+ : `alert:${rule.id}:rule`,
439
+ entityKind: "alert",
440
+ entityId: rule.id,
441
+ route,
442
+ source: evaluation
443
+ ? "alert_evaluations"
444
+ : "alerts.rule_evaluation_preview",
445
+ confidence: evaluation ? 1 : 0.8,
446
+ reason: evaluation
447
+ ? `Alert "${rule.name}" evaluated to ${evaluation.value} and state ${evaluation.state}.`
448
+ : `Alert "${rule.name}" was evaluated against its configured threshold.`,
449
+ citations: [
450
+ {
451
+ label: rule.name,
452
+ entityKind: "alert",
453
+ entityId: rule.id,
454
+ route,
455
+ },
456
+ ],
457
+ suggestedNextPivots: [
458
+ {
459
+ label: "Open alert",
460
+ entityKind: "alert",
461
+ entityId: rule.id,
462
+ route,
463
+ reason: "Inspect the alert rule, threshold, and recent evaluations.",
464
+ },
465
+ ],
466
+ },
467
+ ];
468
+
469
+ if (rule.analysisId) {
470
+ const analysisRouteValue = analysisRoute(rule.analysisId);
471
+ references.push({
472
+ evidenceId: evaluation
473
+ ? `alert:${rule.id}:evaluation:${evaluation.id}:analysis:${rule.analysisId}`
474
+ : `alert:${rule.id}:analysis:${rule.analysisId}`,
475
+ entityKind: "analysis",
476
+ entityId: rule.analysisId,
477
+ route: analysisRouteValue,
478
+ source: "alert_rules.analysis_id",
479
+ confidence: 0.9,
480
+ reason: `Alert "${rule.name}" is bound to analysis "${rule.analysisId}".`,
481
+ citations: [
482
+ {
483
+ label: rule.name,
484
+ entityKind: "alert",
485
+ entityId: rule.id,
486
+ route,
487
+ },
488
+ ],
489
+ suggestedNextPivots: [
490
+ {
491
+ label: "Open investigation",
492
+ entityKind: "analysis",
493
+ entityId: rule.analysisId,
494
+ route: analysisRouteValue,
495
+ reason: "Review the analysis result that drives this alert.",
496
+ },
497
+ ],
498
+ });
499
+ }
500
+
501
+ return references;
502
+ };
503
+
504
+ export const aiEvaluationEvidenceReferences = (
505
+ evaluation: Pick<
506
+ AIEvaluationRecord,
507
+ "evaluationId" | "traceId" | "spanId" | "name" | "source"
508
+ >,
509
+ ): EvidenceReference[] => {
510
+ const spanEntityId = `${evaluation.traceId}:${evaluation.spanId}`;
511
+ const spanRouteValue = evidenceRouteFor(
512
+ "span",
513
+ spanEntityId,
514
+ evaluation.traceId,
515
+ );
516
+ return [
517
+ {
518
+ evidenceId: `ai-evaluation:${evaluation.evaluationId}`,
519
+ entityKind: "eval",
520
+ entityId: evaluation.evaluationId,
521
+ route: evidenceRouteFor("eval", evaluation.evaluationId),
522
+ source: `ai_span_evaluations.${evaluation.source}`,
523
+ confidence: 0.95,
524
+ reason: `AI evaluation "${evaluation.name}" graded span "${evaluation.spanId}".`,
525
+ citations: [
526
+ {
527
+ label: `span ${evaluation.spanId}`,
528
+ entityKind: "span",
529
+ entityId: spanEntityId,
530
+ route: spanRouteValue,
531
+ },
532
+ {
533
+ label: `trace ${evaluation.traceId}`,
534
+ entityKind: "trace",
535
+ entityId: evaluation.traceId,
536
+ route: evidenceRouteFor("trace", evaluation.traceId),
537
+ },
538
+ ],
539
+ suggestedNextPivots: [
540
+ {
541
+ label: "Open span",
542
+ entityKind: "span",
543
+ entityId: spanEntityId,
544
+ route: spanRouteValue,
545
+ reason: "Inspect the evaluated AI span and its surrounding trace.",
546
+ },
547
+ ],
548
+ },
549
+ ];
550
+ };
551
+
552
+ export interface SourceEvidenceLinks {
553
+ sourceAgentRunId?: string | null;
554
+ sourceActionId?: string | null;
555
+ sourceToolCallId?: string | null;
556
+ sourceTraceId?: string | null;
557
+ sourceSpanId?: string | null;
558
+ }
559
+
560
+ export const sourceLinkEvidenceReferences = (
561
+ source: {
562
+ sourceLabel: string;
563
+ sourceId: string;
564
+ sourceKind: "eval_case" | "eval_case_result" | "eval_run";
565
+ sourceRoute?: string | null;
566
+ sourceName?: string | null;
567
+ },
568
+ links: SourceEvidenceLinks,
569
+ ): EvidenceReference[] => {
570
+ const citations: EvidenceCitation[] = [];
571
+ if (source.sourceRoute) {
572
+ citations.push({
573
+ label: source.sourceName ?? source.sourceId,
574
+ entityKind: "eval",
575
+ entityId: source.sourceId,
576
+ route: source.sourceRoute,
577
+ });
578
+ }
579
+
580
+ const candidates: Array<{
581
+ kind: EvidenceEntityKind;
582
+ id: string | null | undefined;
583
+ label: string;
584
+ confidence: number;
585
+ }> = [
586
+ {
587
+ kind: "action",
588
+ id: links.sourceActionId,
589
+ label: "source action",
590
+ confidence: 0.92,
591
+ },
592
+ {
593
+ kind: "tool_call",
594
+ id: links.sourceToolCallId,
595
+ label: "source tool call",
596
+ confidence: 0.9,
597
+ },
598
+ {
599
+ kind: "agent_run",
600
+ id: links.sourceAgentRunId,
601
+ label: "source agent run",
602
+ confidence: 0.9,
603
+ },
604
+ {
605
+ kind: "span",
606
+ id:
607
+ links.sourceTraceId && links.sourceSpanId
608
+ ? `${links.sourceTraceId}:${links.sourceSpanId}`
609
+ : null,
610
+ label: "source span",
611
+ confidence: 0.9,
612
+ },
613
+ {
614
+ kind: "trace",
615
+ id: links.sourceTraceId,
616
+ label: "source trace",
617
+ confidence: 0.88,
618
+ },
619
+ ];
620
+
621
+ return candidates
622
+ .filter((candidate): candidate is typeof candidate & { id: string } =>
623
+ Boolean(candidate.id),
624
+ )
625
+ .map((candidate) => {
626
+ const route = evidenceRouteFor(
627
+ candidate.kind,
628
+ candidate.id,
629
+ links.sourceTraceId,
630
+ );
631
+ return {
632
+ evidenceId: `${source.sourceKind}:${source.sourceId}:${candidate.kind}:${candidate.id}`,
633
+ entityKind: candidate.kind,
634
+ entityId: candidate.id,
635
+ route,
636
+ source: `${source.sourceKind}.source_links`,
637
+ confidence: candidate.confidence,
638
+ reason: `${source.sourceLabel} links to ${candidate.label} "${candidate.id}".`,
639
+ citations,
640
+ suggestedNextPivots: [
641
+ {
642
+ label: `Open ${candidate.label}`,
643
+ entityKind: candidate.kind,
644
+ entityId: candidate.id,
645
+ route,
646
+ reason:
647
+ "Inspect the production evidence behind this evaluation item.",
648
+ },
649
+ ],
650
+ } satisfies EvidenceReference;
651
+ });
652
+ };
653
+
654
+ export const analysisResultEvidenceReferences = (
655
+ result: Pick<AnalysisResult, "analysisId" | "payload">,
656
+ ): EvidenceReference[] => {
657
+ const references: EvidenceReference[] = [
658
+ ...instrumentationGapEvidenceReferences(result),
659
+ ];
660
+ for (const { sectionName, table } of parseEvidenceTables(result.payload)) {
661
+ const headers = table.headers.map(normalizeHeader);
662
+ for (const [rowIndex, row] of table.rows.entries()) {
663
+ if (references.length >= MAX_ANALYSIS_EVIDENCE_REFERENCES) {
664
+ return references;
665
+ }
666
+ const entity = entityFromRow(row, headers);
667
+ if (!entity) continue;
668
+
669
+ const title = table.title ?? sectionName;
670
+ references.push({
671
+ evidenceId: `analysis:${result.analysisId}:${sectionName}:${rowIndex}:${entity.entityKind}:${entity.entityId}`,
672
+ entityKind: entity.entityKind,
673
+ entityId: entity.entityId,
674
+ route: entity.route,
675
+ source: `analysis.payload.evidence.${sectionName}`,
676
+ confidence: entity.confidence,
677
+ reason: `Analysis "${result.analysisId}" cited ${entity.label} in "${title}" row ${rowIndex + 1}.`,
678
+ citations: entity.citations,
679
+ suggestedNextPivots: entity.suggestedNextPivots,
680
+ });
681
+ }
682
+ }
683
+ return references;
684
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Cross-runtime crypto helpers using Web Crypto API.
3
+ * Works in Cloudflare Workers, Node 20+, Deno, Bun.
4
+ */
5
+
6
+ /** SHA-256 hash of a string, returned as lowercase hex. */
7
+ export async function sha256Hex(input: string): Promise<string> {
8
+ const encoder = new TextEncoder();
9
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(input));
10
+ return bytesToHex(new Uint8Array(digest));
11
+ }
12
+
13
+ /** Generate `bytes` cryptographically random bytes and return as lowercase hex. */
14
+ export function randomHex(bytes: number): string {
15
+ const buf = new Uint8Array(bytes);
16
+ crypto.getRandomValues(buf);
17
+ return bytesToHex(buf);
18
+ }
19
+
20
+ function bytesToHex(bytes: Uint8Array): string {
21
+ let out = "";
22
+ for (let i = 0; i < bytes.length; i++) {
23
+ out += bytes[i].toString(16).padStart(2, "0");
24
+ }
25
+ return out;
26
+ }