@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,568 @@
1
+ // RFC 0002 Stage 1 — Tier 0 universal analyses.
2
+ //
3
+ // These six analyses apply to any install regardless of detected app shape.
4
+ // Each `sql` string is executable as-is by the runner (Agent 1). The runner
5
+ // substitutes the literal token `{{PROJECT_ID}}` with the active project's
6
+ // id before execution; everything else (time windows, thresholds) is baked
7
+ // into the SQL itself.
8
+ //
9
+ // The runner expects a single-row result with these columns:
10
+ // status TEXT — 'ok' | 'warn' | 'critical' | 'unknown'
11
+ // primary_value REAL — the headline number (NULL allowed)
12
+ // baseline_value REAL — the comparison number (NULL allowed)
13
+ // payload TEXT — JSON string built with json_object(...) /
14
+ // json_group_array(...) for charts, contributors, etc.
15
+ //
16
+ // delta_pct is precomputed by the runner from primary/baseline.
17
+
18
+ import type { AnalysisDefinition } from "@obsunified/types";
19
+
20
+ // ── overall_error_rate ──────────────────────────────────────────────────────
21
+ // Error spans in the last 5 minutes vs. the trailing 1h baseline rate.
22
+ // Status: critical if rate>5%, warn if rate>1%, ok otherwise. unknown if
23
+ // no traffic in either window.
24
+ const overallErrorRate: AnalysisDefinition = {
25
+ id: "overall_error_rate",
26
+ title: "Overall error rate",
27
+ group: "Health",
28
+ source: "tier0",
29
+ view: "tile",
30
+ refreshSeconds: 60,
31
+ narrate: {
32
+ prompt:
33
+ "Errors moved to {{primary}} (was {{baseline}}, {{delta_pct}}). Name the dominant offending span name or service if visible in the payload, with a time anchor.",
34
+ only_when: "status_changed || delta_pct>20",
35
+ },
36
+ sql: `
37
+ WITH now_window AS (
38
+ SELECT
39
+ COUNT(*) AS total,
40
+ SUM(CASE WHEN status_code = 2 THEN 1 ELSE 0 END) AS errors
41
+ FROM telemetry_spans
42
+ WHERE project_id = '{{PROJECT_ID}}'
43
+ AND received_at >= datetime('now', '-5 minutes')
44
+ ),
45
+ base_window AS (
46
+ SELECT
47
+ COUNT(*) AS total,
48
+ SUM(CASE WHEN status_code = 2 THEN 1 ELSE 0 END) AS errors
49
+ FROM telemetry_spans
50
+ WHERE project_id = '{{PROJECT_ID}}'
51
+ AND received_at >= datetime('now', '-1 hour')
52
+ AND received_at < datetime('now', '-5 minutes')
53
+ ),
54
+ spark AS (
55
+ -- per-minute error rate over the last 30 minutes for sparkline rendering
56
+ SELECT
57
+ strftime('%Y-%m-%dT%H:%M:00Z', received_at) AS minute,
58
+ COUNT(*) AS total,
59
+ SUM(CASE WHEN status_code = 2 THEN 1 ELSE 0 END) AS errors
60
+ FROM telemetry_spans
61
+ WHERE project_id = '{{PROJECT_ID}}'
62
+ AND received_at >= datetime('now', '-30 minutes')
63
+ GROUP BY minute
64
+ ORDER BY minute ASC
65
+ )
66
+ SELECT
67
+ CASE
68
+ WHEN (SELECT total FROM now_window) = 0 THEN 'unknown'
69
+ WHEN (CAST((SELECT errors FROM now_window) AS REAL)
70
+ / (SELECT total FROM now_window)) > 0.05 THEN 'critical'
71
+ WHEN (CAST((SELECT errors FROM now_window) AS REAL)
72
+ / (SELECT total FROM now_window)) > 0.01 THEN 'warn'
73
+ ELSE 'ok'
74
+ END AS status,
75
+ CASE WHEN (SELECT total FROM now_window) = 0 THEN NULL
76
+ ELSE CAST((SELECT errors FROM now_window) AS REAL)
77
+ / (SELECT total FROM now_window) END AS primary_value,
78
+ CASE WHEN (SELECT total FROM base_window) = 0 THEN NULL
79
+ ELSE CAST((SELECT errors FROM base_window) AS REAL)
80
+ / (SELECT total FROM base_window) END AS baseline_value,
81
+ (SELECT json_object(
82
+ 'window', '5m',
83
+ 'baselineWindow', '1h',
84
+ 'currentErrors', (SELECT errors FROM now_window),
85
+ 'currentTotal', (SELECT total FROM now_window),
86
+ 'baselineErrors',(SELECT errors FROM base_window),
87
+ 'baselineTotal', (SELECT total FROM base_window),
88
+ 'sparkline', (
89
+ SELECT json_group_array(json_object(
90
+ 'minute', minute,
91
+ 'total', total,
92
+ 'errors', errors,
93
+ 'rate', CASE WHEN total = 0 THEN 0
94
+ ELSE CAST(errors AS REAL) / total END
95
+ )) FROM spark
96
+ )
97
+ )) AS payload
98
+ `,
99
+ };
100
+
101
+ // ── top_error_services ──────────────────────────────────────────────────────
102
+ // Top 5 services by error count in the last 5 minutes.
103
+ // Status: critical if any service has >10 errors/min (i.e. >50 in 5min),
104
+ // warn if any has >2 errors/min (i.e. >10 in 5min), ok otherwise. unknown
105
+ // if no errors at all.
106
+ const topErrorServices: AnalysisDefinition = {
107
+ id: "top_error_services",
108
+ title: "Top error services",
109
+ group: "Health",
110
+ source: "tier0",
111
+ view: "tile",
112
+ refreshSeconds: 60,
113
+ narrate: {
114
+ prompt:
115
+ "List the top 1–2 erroring services and their counts (from payload). Include a time anchor like 'in the last 5 minutes'.",
116
+ only_when: "signature_changed",
117
+ },
118
+ sql: `
119
+ WITH per_service AS (
120
+ SELECT
121
+ COALESCE(service_name, '<unknown>') AS service,
122
+ COUNT(*) AS total,
123
+ SUM(CASE WHEN status_code = 2 THEN 1 ELSE 0 END) AS errors
124
+ FROM telemetry_spans
125
+ WHERE project_id = '{{PROJECT_ID}}'
126
+ AND received_at >= datetime('now', '-5 minutes')
127
+ GROUP BY service
128
+ ),
129
+ top5 AS (
130
+ SELECT service, total, errors
131
+ FROM per_service
132
+ WHERE errors > 0
133
+ ORDER BY errors DESC
134
+ LIMIT 5
135
+ ),
136
+ max_err AS (SELECT COALESCE(MAX(errors), 0) AS m FROM top5)
137
+ SELECT
138
+ CASE
139
+ WHEN (SELECT m FROM max_err) = 0 THEN 'unknown'
140
+ -- thresholds are per-minute averaged over the 5-minute window
141
+ WHEN (SELECT m FROM max_err) > 50 THEN 'critical'
142
+ WHEN (SELECT m FROM max_err) > 10 THEN 'warn'
143
+ ELSE 'ok'
144
+ END AS status,
145
+ (SELECT m FROM max_err) AS primary_value,
146
+ NULL AS baseline_value,
147
+ (SELECT json_object(
148
+ 'window', '5m',
149
+ 'services', (
150
+ SELECT json_group_array(json_object(
151
+ 'service', service,
152
+ 'errors', errors,
153
+ 'total', total
154
+ )) FROM top5
155
+ )
156
+ )) AS payload
157
+ `,
158
+ };
159
+
160
+ // ── latency_p95_overall ─────────────────────────────────────────────────────
161
+ // p95 across all spans this hour vs. the same hour yesterday.
162
+ // SQLite has no native percentile, so we approximate via ROW_NUMBER ordering:
163
+ // pick the row at offset CEIL(0.95 * n) - 1 in ascending duration order.
164
+ // Status by delta_pct: critical>50%, warn>20%, ok else. unknown when either
165
+ // window is empty.
166
+ const latencyP95Overall: AnalysisDefinition = {
167
+ id: "latency_p95_overall",
168
+ title: "Latency p95 — overall",
169
+ group: "Health",
170
+ source: "tier0",
171
+ view: "tile",
172
+ // Latency is one of the primary signals; 60s matches Tier 0 spec.
173
+ refreshSeconds: 60,
174
+ narrate: {
175
+ prompt:
176
+ "p95 moved to {{primary}}ms from {{baseline}}ms ({{delta_pct}}). Name the slowest service or route if visible in the payload, with a time anchor.",
177
+ only_when: "status_changed || delta_pct>25",
178
+ },
179
+ sql: `
180
+ WITH cur AS (
181
+ SELECT duration_ms,
182
+ ROW_NUMBER() OVER (ORDER BY duration_ms ASC) AS rn,
183
+ COUNT(*) OVER () AS n
184
+ FROM telemetry_spans
185
+ WHERE project_id = '{{PROJECT_ID}}'
186
+ AND received_at >= datetime('now', '-1 hour')
187
+ ),
188
+ base AS (
189
+ SELECT duration_ms,
190
+ ROW_NUMBER() OVER (ORDER BY duration_ms ASC) AS rn,
191
+ COUNT(*) OVER () AS n
192
+ FROM telemetry_spans
193
+ WHERE project_id = '{{PROJECT_ID}}'
194
+ AND received_at >= datetime('now', '-25 hours')
195
+ AND received_at < datetime('now', '-24 hours')
196
+ ),
197
+ cur_p95 AS (
198
+ SELECT duration_ms FROM cur
199
+ WHERE rn = MAX(1, (95 * n + 99) / 100)
200
+ LIMIT 1
201
+ ),
202
+ base_p95 AS (
203
+ SELECT duration_ms FROM base
204
+ WHERE rn = MAX(1, (95 * n + 99) / 100)
205
+ LIMIT 1
206
+ ),
207
+ spark AS (
208
+ -- per-5-minute approx-p95 buckets across the current hour for sparkline
209
+ SELECT
210
+ strftime('%Y-%m-%dT%H:%M:00Z',
211
+ datetime((CAST(strftime('%s', received_at) AS INTEGER) / 300) * 300, 'unixepoch')
212
+ ) AS bucket,
213
+ MAX(duration_ms) AS approx_max,
214
+ COUNT(*) AS n
215
+ FROM telemetry_spans
216
+ WHERE project_id = '{{PROJECT_ID}}'
217
+ AND received_at >= datetime('now', '-1 hour')
218
+ GROUP BY bucket
219
+ ORDER BY bucket ASC
220
+ ),
221
+ cur_val AS (SELECT (SELECT duration_ms FROM cur_p95) AS v),
222
+ base_val AS (SELECT (SELECT duration_ms FROM base_p95) AS v),
223
+ delta AS (
224
+ SELECT CASE
225
+ WHEN (SELECT v FROM cur_val) IS NULL THEN NULL
226
+ WHEN (SELECT v FROM base_val) IS NULL OR (SELECT v FROM base_val) = 0 THEN NULL
227
+ ELSE ((SELECT v FROM cur_val) - (SELECT v FROM base_val))
228
+ * 100.0 / (SELECT v FROM base_val)
229
+ END AS pct
230
+ )
231
+ SELECT
232
+ CASE
233
+ WHEN (SELECT v FROM cur_val) IS NULL THEN 'unknown'
234
+ WHEN (SELECT pct FROM delta) IS NULL THEN 'ok'
235
+ WHEN ABS((SELECT pct FROM delta)) > 50 THEN 'critical'
236
+ WHEN ABS((SELECT pct FROM delta)) > 20 THEN 'warn'
237
+ ELSE 'ok'
238
+ END AS status,
239
+ (SELECT v FROM cur_val) AS primary_value,
240
+ (SELECT v FROM base_val) AS baseline_value,
241
+ (SELECT json_object(
242
+ 'window', '1h',
243
+ 'baselineWindow', 'same hour yesterday',
244
+ 'unit', 'ms',
245
+ 'sparkline', (
246
+ SELECT json_group_array(json_object(
247
+ 'bucket', bucket,
248
+ 'approxP95Ms', approx_max,
249
+ 'samples', n
250
+ )) FROM spark
251
+ )
252
+ )) AS payload
253
+ `,
254
+ };
255
+
256
+ // ── throughput_slope ────────────────────────────────────────────────────────
257
+ // Spans/minute trend: last 30 minutes vs the prior 30 minutes.
258
+ // Status: warn if slope > 1.5x AND p99 trending up (current p99 > prior p99);
259
+ // ok otherwise. unknown if both windows empty.
260
+ const throughputSlope: AnalysisDefinition = {
261
+ id: "throughput_slope",
262
+ title: "Throughput slope",
263
+ group: "Health",
264
+ source: "tier0",
265
+ view: "tile",
266
+ refreshSeconds: 60,
267
+ sql: `
268
+ WITH cur AS (
269
+ SELECT duration_ms,
270
+ ROW_NUMBER() OVER (ORDER BY duration_ms ASC) AS rn,
271
+ COUNT(*) OVER () AS n
272
+ FROM telemetry_spans
273
+ WHERE project_id = '{{PROJECT_ID}}'
274
+ AND received_at >= datetime('now', '-30 minutes')
275
+ ),
276
+ prev AS (
277
+ SELECT duration_ms,
278
+ ROW_NUMBER() OVER (ORDER BY duration_ms ASC) AS rn,
279
+ COUNT(*) OVER () AS n
280
+ FROM telemetry_spans
281
+ WHERE project_id = '{{PROJECT_ID}}'
282
+ AND received_at >= datetime('now', '-60 minutes')
283
+ AND received_at < datetime('now', '-30 minutes')
284
+ ),
285
+ cur_n AS (SELECT COALESCE((SELECT n FROM cur LIMIT 1), 0) AS n),
286
+ prev_n AS (SELECT COALESCE((SELECT n FROM prev LIMIT 1), 0) AS n),
287
+ cur_p99 AS (
288
+ SELECT duration_ms FROM cur
289
+ WHERE rn = MAX(1, (99 * n + 99) / 100)
290
+ LIMIT 1
291
+ ),
292
+ prev_p99 AS (
293
+ SELECT duration_ms FROM prev
294
+ WHERE rn = MAX(1, (99 * n + 99) / 100)
295
+ LIMIT 1
296
+ ),
297
+ rates AS (
298
+ SELECT
299
+ CAST((SELECT n FROM cur_n) AS REAL) / 30.0 AS current_rpm,
300
+ CAST((SELECT n FROM prev_n) AS REAL) / 30.0 AS prior_rpm
301
+ ),
302
+ slope_calc AS (
303
+ SELECT
304
+ current_rpm,
305
+ prior_rpm,
306
+ CASE WHEN prior_rpm = 0 THEN NULL
307
+ ELSE current_rpm / prior_rpm END AS slope
308
+ FROM rates
309
+ )
310
+ SELECT
311
+ CASE
312
+ WHEN (SELECT n FROM cur_n) = 0 AND (SELECT n FROM prev_n) = 0 THEN 'unknown'
313
+ WHEN (SELECT slope FROM slope_calc) IS NOT NULL
314
+ AND (SELECT slope FROM slope_calc) > 1.5
315
+ AND COALESCE((SELECT duration_ms FROM cur_p99), 0)
316
+ > COALESCE((SELECT duration_ms FROM prev_p99), 0)
317
+ THEN 'warn'
318
+ ELSE 'ok'
319
+ END AS status,
320
+ (SELECT current_rpm FROM rates) AS primary_value,
321
+ (SELECT prior_rpm FROM rates) AS baseline_value,
322
+ (SELECT json_object(
323
+ 'currentRpm', (SELECT current_rpm FROM rates),
324
+ 'priorRpm', (SELECT prior_rpm FROM rates),
325
+ 'slope', (SELECT slope FROM slope_calc),
326
+ 'currentP99Ms', (SELECT duration_ms FROM cur_p99),
327
+ 'priorP99Ms', (SELECT duration_ms FROM prev_p99),
328
+ 'window', '30m',
329
+ 'baselineWindow', 'prior 30m'
330
+ )) AS payload
331
+ `,
332
+ };
333
+
334
+ // ── active_sessions ─────────────────────────────────────────────────────────
335
+ // Distinct frontend sessions in usage_events for the last 15 minutes plus
336
+ // per-hour breakdown across the trailing 24h. Always status: 'ok' — this is
337
+ // informational. unknown only if the table is missing data entirely.
338
+ const activeSessions: AnalysisDefinition = {
339
+ id: "active_sessions",
340
+ title: "Active sessions",
341
+ group: "Frontend",
342
+ source: "tier0",
343
+ view: "tile",
344
+ refreshSeconds: 60,
345
+ sql: `
346
+ WITH active AS (
347
+ SELECT COUNT(DISTINCT session_id) AS n
348
+ FROM usage_events
349
+ WHERE project_id = '{{PROJECT_ID}}'
350
+ AND received_at >= datetime('now', '-15 minutes')
351
+ ),
352
+ hourly AS (
353
+ SELECT
354
+ strftime('%Y-%m-%dT%H:00:00Z', received_at) AS hour,
355
+ COUNT(DISTINCT session_id) AS sessions
356
+ FROM usage_events
357
+ WHERE project_id = '{{PROJECT_ID}}'
358
+ AND received_at >= datetime('now', '-24 hours')
359
+ GROUP BY hour
360
+ ORDER BY hour ASC
361
+ )
362
+ SELECT
363
+ 'ok' AS status,
364
+ CAST((SELECT n FROM active) AS REAL) AS primary_value,
365
+ NULL AS baseline_value,
366
+ (SELECT json_object(
367
+ 'count', (SELECT n FROM active),
368
+ 'window', '15m',
369
+ 'hourlyBreakdown', (
370
+ SELECT json_group_array(json_object(
371
+ 'hour', hour,
372
+ 'sessions', sessions
373
+ )) FROM hourly
374
+ )
375
+ )) AS payload
376
+ `,
377
+ };
378
+
379
+ // ── log_error_rate ──────────────────────────────────────────────────────────
380
+ // Error+fatal logs in the last 5 minutes vs. the trailing 1h baseline rate.
381
+ // severity_number follows OTel: ERROR=17-20, FATAL=21-24.
382
+ // Status: critical if rate>5%, warn if rate>1%, ok otherwise. unknown if
383
+ // no logs in either window.
384
+ const logErrorRate: AnalysisDefinition = {
385
+ id: "log_error_rate",
386
+ narrate: {
387
+ prompt:
388
+ "Log error rate is {{primary}} (was {{baseline}}, {{delta_pct}}). If a logger or message pattern dominates the payload, name it. Time anchor.",
389
+ only_when: "status_changed || delta_pct>25",
390
+ },
391
+ title: "Log error rate",
392
+ group: "Health",
393
+ source: "tier0",
394
+ view: "tile",
395
+ refreshSeconds: 60,
396
+ sql: `
397
+ WITH now_window AS (
398
+ SELECT
399
+ COUNT(*) AS total,
400
+ SUM(CASE WHEN severity_number >= 17 THEN 1 ELSE 0 END) AS errors
401
+ FROM logs
402
+ WHERE project_id = '{{PROJECT_ID}}'
403
+ AND received_at >= datetime('now', '-5 minutes')
404
+ ),
405
+ base_window AS (
406
+ SELECT
407
+ COUNT(*) AS total,
408
+ SUM(CASE WHEN severity_number >= 17 THEN 1 ELSE 0 END) AS errors
409
+ FROM logs
410
+ WHERE project_id = '{{PROJECT_ID}}'
411
+ AND received_at >= datetime('now', '-1 hour')
412
+ AND received_at < datetime('now', '-5 minutes')
413
+ ),
414
+ spark AS (
415
+ SELECT
416
+ strftime('%Y-%m-%dT%H:%M:00Z', received_at) AS minute,
417
+ COUNT(*) AS total,
418
+ SUM(CASE WHEN severity_number >= 17 THEN 1 ELSE 0 END) AS errors
419
+ FROM logs
420
+ WHERE project_id = '{{PROJECT_ID}}'
421
+ AND received_at >= datetime('now', '-30 minutes')
422
+ GROUP BY minute
423
+ ORDER BY minute ASC
424
+ )
425
+ SELECT
426
+ CASE
427
+ WHEN (SELECT total FROM now_window) = 0 THEN 'unknown'
428
+ WHEN (CAST((SELECT errors FROM now_window) AS REAL)
429
+ / (SELECT total FROM now_window)) > 0.05 THEN 'critical'
430
+ WHEN (CAST((SELECT errors FROM now_window) AS REAL)
431
+ / (SELECT total FROM now_window)) > 0.01 THEN 'warn'
432
+ ELSE 'ok'
433
+ END AS status,
434
+ CASE WHEN (SELECT total FROM now_window) = 0 THEN NULL
435
+ ELSE CAST((SELECT errors FROM now_window) AS REAL)
436
+ / (SELECT total FROM now_window) END AS primary_value,
437
+ CASE WHEN (SELECT total FROM base_window) = 0 THEN NULL
438
+ ELSE CAST((SELECT errors FROM base_window) AS REAL)
439
+ / (SELECT total FROM base_window) END AS baseline_value,
440
+ (SELECT json_object(
441
+ 'window', '5m',
442
+ 'baselineWindow', '1h',
443
+ 'currentErrors', (SELECT errors FROM now_window),
444
+ 'currentTotal', (SELECT total FROM now_window),
445
+ 'baselineErrors',(SELECT errors FROM base_window),
446
+ 'baselineTotal', (SELECT total FROM base_window),
447
+ 'sparkline', (
448
+ SELECT json_group_array(json_object(
449
+ 'minute', minute,
450
+ 'total', total,
451
+ 'errors', errors,
452
+ 'rate', CASE WHEN total = 0 THEN 0
453
+ ELSE CAST(errors AS REAL) / total END
454
+ )) FROM spark
455
+ )
456
+ )) AS payload
457
+ `,
458
+ };
459
+
460
+ // ── service_cpu_utilization ─────────────────────────────────────────────────
461
+ // RFC 0005 Phase 2.6 — fleet-wide process CPU utilization tile.
462
+ // Reads `process.cpu.utilization` gauge points emitted by
463
+ // @obsunified/telemetry-sdk's enableProcessMetrics() helper (or any other OTel
464
+ // runtime-metrics exporter using the standard semconv).
465
+ //
466
+ // Status: critical if any service averaged >0.9 over the last 5 min,
467
+ // warn if >0.7, ok otherwise. unknown if no process.cpu.* series at all
468
+ // (the typical state on a fresh install before enableProcessMetrics
469
+ // is wired into the user's services).
470
+ const serviceCpuUtilization: AnalysisDefinition = {
471
+ id: "service_cpu_utilization",
472
+ title: "Service CPU utilization",
473
+ group: "Services",
474
+ source: "tier0",
475
+ view: "tile",
476
+ refreshSeconds: 60,
477
+ narrate: {
478
+ prompt:
479
+ "Name the busiest service and its average utilization over the last 5 minutes. Mention if it crossed a critical (>0.9) or warn (>0.7) threshold.",
480
+ only_when: "status_changed",
481
+ },
482
+ sql: `
483
+ WITH cpu_points AS (
484
+ SELECT
485
+ s.service_name,
486
+ p.value,
487
+ p.received_at
488
+ FROM metric_point p
489
+ JOIN metric_series s ON s.id = p.series_id
490
+ WHERE s.project_id = '{{PROJECT_ID}}'
491
+ AND s.name = 'process.cpu.utilization'
492
+ AND s.service_name IS NOT NULL
493
+ AND p.received_at >= datetime('now', '-30 minutes')
494
+ ),
495
+ now_window AS (
496
+ SELECT service_name, AVG(value) AS avg_util
497
+ FROM cpu_points
498
+ WHERE received_at >= datetime('now', '-5 minutes')
499
+ GROUP BY service_name
500
+ ),
501
+ base_window AS (
502
+ SELECT service_name, AVG(value) AS avg_util
503
+ FROM cpu_points
504
+ WHERE received_at >= datetime('now', '-30 minutes')
505
+ AND received_at < datetime('now', '-5 minutes')
506
+ GROUP BY service_name
507
+ ),
508
+ top_now AS (
509
+ SELECT service_name, avg_util
510
+ FROM now_window
511
+ ORDER BY avg_util DESC
512
+ LIMIT 5
513
+ ),
514
+ fleet_max AS (
515
+ SELECT COALESCE(MAX(avg_util), 0) AS m FROM now_window
516
+ ),
517
+ top_service AS (
518
+ SELECT service_name
519
+ FROM now_window
520
+ ORDER BY avg_util DESC
521
+ LIMIT 1
522
+ ),
523
+ spark AS (
524
+ SELECT
525
+ strftime('%Y-%m-%dT%H:%M:00Z', received_at) AS minute,
526
+ AVG(value) AS avg_util
527
+ FROM cpu_points
528
+ WHERE service_name = (SELECT service_name FROM top_service)
529
+ GROUP BY minute
530
+ ORDER BY minute ASC
531
+ )
532
+ SELECT
533
+ CASE
534
+ WHEN NOT EXISTS (SELECT 1 FROM cpu_points) THEN 'unknown'
535
+ WHEN (SELECT m FROM fleet_max) > 0.9 THEN 'critical'
536
+ WHEN (SELECT m FROM fleet_max) > 0.7 THEN 'warn'
537
+ ELSE 'ok'
538
+ END AS status,
539
+ (SELECT m FROM fleet_max) AS primary_value,
540
+ (SELECT AVG(avg_util) FROM base_window) AS baseline_value,
541
+ (SELECT json_object(
542
+ 'window', '5m',
543
+ 'baselineWindow', '25m',
544
+ 'topServices', (
545
+ SELECT json_group_array(json_object(
546
+ 'service', service_name,
547
+ 'avgUtilization', avg_util
548
+ )) FROM top_now
549
+ ),
550
+ 'sparkline', (
551
+ SELECT json_group_array(json_object(
552
+ 'minute', minute,
553
+ 'avgUtilization', avg_util
554
+ )) FROM spark
555
+ )
556
+ )) AS payload
557
+ `,
558
+ };
559
+
560
+ export const TIER0_ANALYSES: AnalysisDefinition[] = [
561
+ overallErrorRate,
562
+ topErrorServices,
563
+ latencyP95Overall,
564
+ throughputSlope,
565
+ activeSessions,
566
+ logErrorRate,
567
+ serviceCpuUtilization,
568
+ ];