@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,277 @@
1
+ /**
2
+ * RFC 0002 Stage 4 — investigation templates.
3
+ *
4
+ * The RFC originally targeted a Python sidecar for these (Stage 2). We
5
+ * skipped Stage 2; investigations here are pure SQL (multiple result-sets
6
+ * stitched into one structured `payload.evidence` blob) executed by the
7
+ * existing runner. SQLite's window functions + json builders cover what
8
+ * Polars would do for the cohort/funnel/anomaly shapes we ship.
9
+ *
10
+ * Convention used by `InvestigationPage`:
11
+ *
12
+ * payload.evidence = {
13
+ * [section_name]: {
14
+ * title?: string,
15
+ * headers: string[],
16
+ * rows: Array<Array<string | number | null>>,
17
+ * note?: string,
18
+ * }
19
+ * }
20
+ *
21
+ * The runner shapes status / primary / baseline / delta the same way as
22
+ * tile analyses; the page UI renders the headline numbers above the
23
+ * narrative and tables below.
24
+ */
25
+
26
+ import type { AnalysisDefinition } from "@obsunified/types";
27
+
28
+ // ── error_top_offenders ────────────────────────────────────────────────────
29
+ // Cohort-comparison investigation. "Which services contributed the most
30
+ // errors in the last 5 minutes vs the trailing hour, and is the mix
31
+ // shifting?" Status: critical if current top-3 service error count > 50;
32
+ // warn if > 10; ok otherwise. Primary value is current top-1 error count.
33
+ const errorTopOffenders: AnalysisDefinition = {
34
+ id: "investigate.error_top_offenders",
35
+ title: "Error top offenders",
36
+ group: "Health",
37
+ source: "tier0",
38
+ view: "page",
39
+ refreshSeconds: 300,
40
+ narrate: {
41
+ prompt:
42
+ "Summarize which services dominate errors right now and how the mix shifted vs the last hour. If one service moved sharply, name it. Time anchor.",
43
+ only_when: "always",
44
+ },
45
+ sql: `
46
+ WITH cur AS (
47
+ SELECT COALESCE(service_name, '<unknown>') AS service,
48
+ COUNT(*) AS errors
49
+ FROM telemetry_spans
50
+ WHERE project_id = '{{PROJECT_ID}}'
51
+ AND received_at >= datetime('now', '-5 minutes')
52
+ AND status_code = 2
53
+ GROUP BY service_name
54
+ ORDER BY errors DESC
55
+ LIMIT 5
56
+ ),
57
+ prev AS (
58
+ SELECT COALESCE(service_name, '<unknown>') AS service,
59
+ COUNT(*) AS errors
60
+ FROM telemetry_spans
61
+ WHERE project_id = '{{PROJECT_ID}}'
62
+ AND received_at >= datetime('now', '-1 hour')
63
+ AND received_at < datetime('now', '-5 minutes')
64
+ AND status_code = 2
65
+ GROUP BY service_name
66
+ ORDER BY errors DESC
67
+ LIMIT 5
68
+ ),
69
+ top1 AS (
70
+ SELECT errors AS top_errors FROM cur LIMIT 1
71
+ )
72
+ SELECT
73
+ CASE
74
+ WHEN COALESCE((SELECT top_errors FROM top1), 0) > 50 THEN 'critical'
75
+ WHEN COALESCE((SELECT top_errors FROM top1), 0) > 10 THEN 'warn'
76
+ ELSE 'ok'
77
+ END AS status,
78
+ COALESCE((SELECT top_errors FROM top1), 0) AS primary_value,
79
+ (SELECT errors FROM prev LIMIT 1) AS baseline_value,
80
+ (SELECT json_object(
81
+ 'window', '5m',
82
+ 'baselineWindow', '5m–60m ago',
83
+ 'evidence', json_object(
84
+ 'current_top_services', json_object(
85
+ 'title', 'Top error services — last 5 minutes',
86
+ 'headers', json_array('service', 'errors'),
87
+ 'rows', (
88
+ SELECT json_group_array(json_array(service, errors))
89
+ FROM cur
90
+ )
91
+ ),
92
+ 'baseline_top_services', json_object(
93
+ 'title', 'Top error services — 5–60 minutes ago',
94
+ 'headers', json_array('service', 'errors'),
95
+ 'rows', (
96
+ SELECT json_group_array(json_array(service, errors))
97
+ FROM prev
98
+ )
99
+ )
100
+ )
101
+ )) AS payload
102
+ `,
103
+ };
104
+
105
+ // ── latency_outlier_attribution ───────────────────────────────────────────
106
+ // Funnel-style investigation: of the slow tail (last 15min), which
107
+ // service+span combinations dominate? Helps users go from "p95 is up" to
108
+ // "checkout.submit is the offender" in one click.
109
+ const latencyOutlierAttribution: AnalysisDefinition = {
110
+ id: "investigate.latency_outlier_attribution",
111
+ title: "Latency outlier attribution",
112
+ group: "Health",
113
+ source: "tier0",
114
+ view: "page",
115
+ refreshSeconds: 300,
116
+ narrate: {
117
+ prompt:
118
+ "Identify which span(s) dominate the slow tail in the last 15 minutes. Quote concrete duration numbers and span names from the evidence. Time anchor.",
119
+ only_when: "always",
120
+ },
121
+ sql: `
122
+ WITH ranked AS (
123
+ SELECT
124
+ duration_ms,
125
+ COALESCE(service_name, '<unknown>') AS service,
126
+ COALESCE(span_name, '<unknown>') AS span,
127
+ ROW_NUMBER() OVER (ORDER BY duration_ms DESC) AS rn,
128
+ COUNT(*) OVER () AS n
129
+ FROM telemetry_spans
130
+ WHERE project_id = '{{PROJECT_ID}}'
131
+ AND received_at >= datetime('now', '-15 minutes')
132
+ ),
133
+ p95_cut AS (
134
+ -- Conservatively call "tail" the slowest 5% of the window.
135
+ SELECT MIN(duration_ms) AS p95
136
+ FROM ranked
137
+ WHERE rn <= MAX(1, (n + 19) / 20)
138
+ ),
139
+ offenders AS (
140
+ SELECT service, span,
141
+ COUNT(*) AS tail_count,
142
+ MAX(duration_ms) AS max_ms,
143
+ ROUND(AVG(duration_ms), 1) AS avg_ms
144
+ FROM ranked, p95_cut
145
+ WHERE duration_ms >= COALESCE(p95_cut.p95, 0)
146
+ GROUP BY service, span
147
+ ORDER BY tail_count DESC
148
+ LIMIT 8
149
+ ),
150
+ top_max AS (
151
+ SELECT max_ms FROM offenders ORDER BY max_ms DESC LIMIT 1
152
+ )
153
+ SELECT
154
+ CASE
155
+ WHEN COALESCE((SELECT max_ms FROM top_max), 0) > 5000 THEN 'critical'
156
+ WHEN COALESCE((SELECT max_ms FROM top_max), 0) > 1000 THEN 'warn'
157
+ ELSE 'ok'
158
+ END AS status,
159
+ COALESCE((SELECT max_ms FROM top_max), 0) AS primary_value,
160
+ (SELECT p95 FROM p95_cut) AS baseline_value,
161
+ (SELECT json_object(
162
+ 'window', '15m',
163
+ 'unit', 'ms',
164
+ 'evidence', json_object(
165
+ 'tail_offenders', json_object(
166
+ 'title', 'Top tail offenders — slowest 5% of last 15 minutes',
167
+ 'headers', json_array('service', 'span', 'count_in_tail', 'max_ms', 'avg_ms'),
168
+ 'rows', (
169
+ SELECT json_group_array(json_array(service, span, tail_count, max_ms, avg_ms))
170
+ FROM offenders
171
+ )
172
+ )
173
+ )
174
+ )) AS payload
175
+ `,
176
+ };
177
+
178
+ // ── log_anomaly_summary ────────────────────────────────────────────────────
179
+ // Anomaly investigation: which ERROR-severity logger names showed up in
180
+ // the last 5 minutes that weren't present in the prior hour? These are
181
+ // the "first time we've ever seen this fail" cases — usually the most
182
+ // interesting line in an incident.
183
+ const logAnomalySummary: AnalysisDefinition = {
184
+ id: "investigate.log_anomaly_summary",
185
+ title: "Log anomaly summary",
186
+ group: "Health",
187
+ source: "tier0",
188
+ view: "page",
189
+ refreshSeconds: 300,
190
+ narrate: {
191
+ prompt:
192
+ "List the new ERROR loggers (those absent from the prior hour). If any look like 'never seen before', call it out. Quote one representative message per logger. Time anchor.",
193
+ only_when: "always",
194
+ },
195
+ sql: `
196
+ WITH recent AS (
197
+ -- Correlated subquery for sample_message uses the table alias
198
+ -- l1 so SQLite resolves the outer column reference; the AS alias
199
+ -- 'logger' defined in the SELECT list isn't visible inside a
200
+ -- correlated subquery in SQLite.
201
+ SELECT COALESCE(l1.logger_name, '<unknown>') AS logger,
202
+ COUNT(*) AS occurrences,
203
+ (SELECT l2.message FROM logs l2
204
+ WHERE l2.project_id = '{{PROJECT_ID}}'
205
+ AND COALESCE(l2.logger_name, '<unknown>') = COALESCE(l1.logger_name, '<unknown>')
206
+ AND l2.received_at >= datetime('now', '-5 minutes')
207
+ AND l2.severity_number >= 17
208
+ ORDER BY l2.received_at DESC LIMIT 1) AS sample_message
209
+ FROM logs l1
210
+ WHERE l1.project_id = '{{PROJECT_ID}}'
211
+ AND l1.received_at >= datetime('now', '-5 minutes')
212
+ AND l1.severity_number >= 17
213
+ GROUP BY l1.logger_name
214
+ ),
215
+ baseline AS (
216
+ SELECT DISTINCT COALESCE(logger_name, '<unknown>') AS logger
217
+ FROM logs
218
+ WHERE project_id = '{{PROJECT_ID}}'
219
+ AND received_at >= datetime('now', '-1 hour')
220
+ AND received_at < datetime('now', '-5 minutes')
221
+ AND severity_number >= 17
222
+ ),
223
+ newcomers AS (
224
+ SELECT recent.*
225
+ FROM recent
226
+ LEFT JOIN baseline ON baseline.logger = recent.logger
227
+ WHERE baseline.logger IS NULL
228
+ ORDER BY recent.occurrences DESC
229
+ LIMIT 8
230
+ ),
231
+ all_recent AS (
232
+ SELECT recent.*,
233
+ CASE WHEN baseline.logger IS NULL THEN 1 ELSE 0 END AS is_new
234
+ FROM recent
235
+ LEFT JOIN baseline ON baseline.logger = recent.logger
236
+ ORDER BY is_new DESC, occurrences DESC
237
+ LIMIT 12
238
+ )
239
+ SELECT
240
+ CASE
241
+ WHEN (SELECT COUNT(*) FROM newcomers) >= 3 THEN 'critical'
242
+ WHEN (SELECT COUNT(*) FROM newcomers) >= 1 THEN 'warn'
243
+ ELSE 'ok'
244
+ END AS status,
245
+ (SELECT COUNT(*) FROM newcomers) AS primary_value,
246
+ (SELECT COUNT(*) FROM baseline) AS baseline_value,
247
+ (SELECT json_object(
248
+ 'window', '5m',
249
+ 'baselineWindow', '5–60m ago',
250
+ 'evidence', json_object(
251
+ 'new_error_loggers', json_object(
252
+ 'title', 'New ERROR loggers — first seen in last 5 minutes',
253
+ 'headers', json_array('logger', 'occurrences', 'sample_message'),
254
+ 'note', 'Loggers absent from the prior 5–60 minute window.',
255
+ 'rows', (
256
+ SELECT json_group_array(json_array(logger, occurrences, sample_message))
257
+ FROM newcomers
258
+ )
259
+ ),
260
+ 'all_recent_loggers', json_object(
261
+ 'title', 'All ERROR loggers — last 5 minutes',
262
+ 'headers', json_array('logger', 'occurrences', 'is_new', 'sample_message'),
263
+ 'rows', (
264
+ SELECT json_group_array(json_array(logger, occurrences, is_new, sample_message))
265
+ FROM all_recent
266
+ )
267
+ )
268
+ )
269
+ )) AS payload
270
+ `,
271
+ };
272
+
273
+ export const INVESTIGATION_ANALYSES: AnalysisDefinition[] = [
274
+ errorTopOffenders,
275
+ latencyOutlierAttribution,
276
+ logAnomalySummary,
277
+ ];