railwatch 0.9.3 → 1.0.0

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 (454) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +37 -33
  3. data/CHANGELOG.md +66 -0
  4. data/README.md +51 -60
  5. data/config/routes.rb +0 -59
  6. data/docs/ai-and-mcp.md +9 -8
  7. data/docs/configuration.md +57 -71
  8. data/docs/faq.md +17 -25
  9. data/docs/getting-started.md +105 -117
  10. data/docs/replacing-nightwatch.md +8 -7
  11. data/docs/replacing-sentry.md +12 -16
  12. data/docs/security.md +14 -20
  13. data/docs/self-hosting.md +24 -22
  14. data/docs/testing.md +6 -14
  15. data/docs/troubleshooting.md +12 -46
  16. data/lib/generators/railwatch/install/install_generator.rb +12 -240
  17. data/lib/generators/railwatch/install/templates/initializer.rb.tt +10 -40
  18. data/lib/generators/railwatch/install/templates/post-deploy +0 -7
  19. data/lib/railwatch/authentication.rb +6 -5
  20. data/lib/railwatch/buffer.rb +9 -0
  21. data/lib/railwatch/configuration.rb +21 -151
  22. data/lib/railwatch/engine.rb +9 -118
  23. data/lib/railwatch/patches.rb +0 -11
  24. data/lib/railwatch/reporter.rb +92 -21
  25. data/lib/railwatch/sampler.rb +10 -2
  26. data/lib/railwatch/subscribers/process_info.rb +1 -2
  27. data/lib/railwatch/version.rb +1 -1
  28. data/lib/railwatch.rb +8 -64
  29. data/lib/tasks/railwatch_tasks.rake +3 -325
  30. data/llms.txt +23 -25
  31. metadata +3 -453
  32. data/app/channels/railwatch/environment_channel.rb +0 -62
  33. data/app/controllers/concerns/railwatch/telemetry_identity.rb +0 -25
  34. data/app/controllers/railwatch/alerts_controller.rb +0 -48
  35. data/app/controllers/railwatch/anomaly_rules_controller.rb +0 -31
  36. data/app/controllers/railwatch/attachments_controller.rb +0 -22
  37. data/app/controllers/railwatch/broadcasts_controller.rb +0 -15
  38. data/app/controllers/railwatch/cache_events_controller.rb +0 -30
  39. data/app/controllers/railwatch/commands_controller.rb +0 -16
  40. data/app/controllers/railwatch/comments_controller.rb +0 -11
  41. data/app/controllers/railwatch/dashboard_controller.rb +0 -70
  42. data/app/controllers/railwatch/deploys_controller.rb +0 -34
  43. data/app/controllers/railwatch/deprecations_controller.rb +0 -54
  44. data/app/controllers/railwatch/environment_scoped.rb +0 -81
  45. data/app/controllers/railwatch/exceptions_controller.rb +0 -52
  46. data/app/controllers/railwatch/executions_controller.rb +0 -13
  47. data/app/controllers/railwatch/issues_controller.rb +0 -258
  48. data/app/controllers/railwatch/jobs_controller.rb +0 -63
  49. data/app/controllers/railwatch/llm_calls_controller.rb +0 -124
  50. data/app/controllers/railwatch/logs_controller.rb +0 -40
  51. data/app/controllers/railwatch/mails_controller.rb +0 -19
  52. data/app/controllers/railwatch/notifications_controller.rb +0 -11
  53. data/app/controllers/railwatch/outgoing_requests_controller.rb +0 -19
  54. data/app/controllers/railwatch/overview_controller.rb +0 -38
  55. data/app/controllers/railwatch/people_controller.rb +0 -25
  56. data/app/controllers/railwatch/processes_controller.rb +0 -33
  57. data/app/controllers/railwatch/profiles_controller.rb +0 -65
  58. data/app/controllers/railwatch/queries_controller.rb +0 -59
  59. data/app/controllers/railwatch/releases_controller.rb +0 -75
  60. data/app/controllers/railwatch/requests_controller.rb +0 -37
  61. data/app/controllers/railwatch/saved_views_controller.rb +0 -53
  62. data/app/controllers/railwatch/scheduled_tasks_controller.rb +0 -45
  63. data/app/controllers/railwatch/spans_controller.rb +0 -47
  64. data/app/controllers/railwatch/storage_ops_controller.rb +0 -15
  65. data/app/controllers/railwatch/tenants_controller.rb +0 -44
  66. data/app/controllers/railwatch/thresholds_controller.rb +0 -47
  67. data/app/controllers/railwatch/traces_controller.rb +0 -72
  68. data/app/controllers/railwatch/transactions_controller.rb +0 -21
  69. data/app/controllers/railwatch/view_renders_controller.rb +0 -28
  70. data/app/controllers/railwatch/visits_controller.rb +0 -53
  71. data/app/frontend/components/app-content.tsx +0 -26
  72. data/app/frontend/components/app-logo-icon.tsx +0 -23
  73. data/app/frontend/components/app-logo.tsx +0 -14
  74. data/app/frontend/components/app-shell.tsx +0 -38
  75. data/app/frontend/components/app-sidebar-header.tsx +0 -54
  76. data/app/frontend/components/app-sidebar.tsx +0 -94
  77. data/app/frontend/components/app-wordmark.tsx +0 -29
  78. data/app/frontend/components/breadcrumbs.tsx +0 -46
  79. data/app/frontend/components/icon.tsx +0 -16
  80. data/app/frontend/components/input-error.tsx +0 -18
  81. data/app/frontend/components/nav-footer.tsx +0 -46
  82. data/app/frontend/components/nav-main.tsx +0 -41
  83. data/app/frontend/components/nav-user.tsx +0 -50
  84. data/app/frontend/components/railwatch/chart-hover.tsx +0 -34
  85. data/app/frontend/components/railwatch/chart-panel.tsx +0 -170
  86. data/app/frontend/components/railwatch/code.tsx +0 -28
  87. data/app/frontend/components/railwatch/command-palette.tsx +0 -132
  88. data/app/frontend/components/railwatch/copy-block.tsx +0 -52
  89. data/app/frontend/components/railwatch/copy-id.tsx +0 -49
  90. data/app/frontend/components/railwatch/cursor-load-more.tsx +0 -39
  91. data/app/frontend/components/railwatch/data-table.tsx +0 -195
  92. data/app/frontend/components/railwatch/digit-roll.tsx +0 -312
  93. data/app/frontend/components/railwatch/empty-state.tsx +0 -159
  94. data/app/frontend/components/railwatch/env-nav.tsx +0 -130
  95. data/app/frontend/components/railwatch/env-switcher.tsx +0 -145
  96. data/app/frontend/components/railwatch/filter-bar.tsx +0 -153
  97. data/app/frontend/components/railwatch/flamegraph.tsx +0 -324
  98. data/app/frontend/components/railwatch/frames.tsx +0 -295
  99. data/app/frontend/components/railwatch/json-viewer.tsx +0 -159
  100. data/app/frontend/components/railwatch/live-dot.tsx +0 -78
  101. data/app/frontend/components/railwatch/live-toggle.tsx +0 -76
  102. data/app/frontend/components/railwatch/metric-chart.tsx +0 -112
  103. data/app/frontend/components/railwatch/mobile-page-switcher.tsx +0 -89
  104. data/app/frontend/components/railwatch/onboarding.tsx +0 -91
  105. data/app/frontend/components/railwatch/origin-identity.tsx +0 -84
  106. data/app/frontend/components/railwatch/page-header.tsx +0 -42
  107. data/app/frontend/components/railwatch/percentile-picker.tsx +0 -18
  108. data/app/frontend/components/railwatch/relative-time.tsx +0 -18
  109. data/app/frontend/components/railwatch/saved-views.tsx +0 -232
  110. data/app/frontend/components/railwatch/segmented.tsx +0 -64
  111. data/app/frontend/components/railwatch/series-chart.tsx +0 -400
  112. data/app/frontend/components/railwatch/shortcuts-dialog.tsx +0 -89
  113. data/app/frontend/components/railwatch/signal-toast.tsx +0 -121
  114. data/app/frontend/components/railwatch/sort-header.tsx +0 -37
  115. data/app/frontend/components/railwatch/source-link.tsx +0 -57
  116. data/app/frontend/components/railwatch/sparkline-cell.tsx +0 -14
  117. data/app/frontend/components/railwatch/sql-block.tsx +0 -116
  118. data/app/frontend/components/railwatch/stat.tsx +0 -185
  119. data/app/frontend/components/railwatch/status-badge.tsx +0 -79
  120. data/app/frontend/components/railwatch/step-picker.tsx +0 -57
  121. data/app/frontend/components/railwatch/timeline.tsx +0 -405
  122. data/app/frontend/components/railwatch/utilisation-bar.tsx +0 -44
  123. data/app/frontend/components/railwatch/window-picker.tsx +0 -147
  124. data/app/frontend/components/ui/avatar.tsx +0 -51
  125. data/app/frontend/components/ui/badge.tsx +0 -46
  126. data/app/frontend/components/ui/breadcrumb.tsx +0 -109
  127. data/app/frontend/components/ui/button.tsx +0 -60
  128. data/app/frontend/components/ui/card.tsx +0 -92
  129. data/app/frontend/components/ui/chart.tsx +0 -374
  130. data/app/frontend/components/ui/checkbox.tsx +0 -32
  131. data/app/frontend/components/ui/collapsible.tsx +0 -31
  132. data/app/frontend/components/ui/command.tsx +0 -182
  133. data/app/frontend/components/ui/dialog.tsx +0 -158
  134. data/app/frontend/components/ui/dropdown-menu.tsx +0 -255
  135. data/app/frontend/components/ui/hover-card.tsx +0 -42
  136. data/app/frontend/components/ui/input.tsx +0 -21
  137. data/app/frontend/components/ui/label.tsx +0 -24
  138. data/app/frontend/components/ui/popover.tsx +0 -87
  139. data/app/frontend/components/ui/select.tsx +0 -185
  140. data/app/frontend/components/ui/separator.tsx +0 -28
  141. data/app/frontend/components/ui/sheet.tsx +0 -137
  142. data/app/frontend/components/ui/sidebar.tsx +0 -726
  143. data/app/frontend/components/ui/skeleton.tsx +0 -13
  144. data/app/frontend/components/ui/sonner.tsx +0 -41
  145. data/app/frontend/components/ui/table.tsx +0 -114
  146. data/app/frontend/components/ui/tabs.tsx +0 -91
  147. data/app/frontend/components/ui/textarea.tsx +0 -18
  148. data/app/frontend/components/ui/tooltip.tsx +0 -61
  149. data/app/frontend/components/user-info.tsx +0 -32
  150. data/app/frontend/components/user-menu-content.tsx +0 -94
  151. data/app/frontend/entrypoints/application.css +0 -244
  152. data/app/frontend/entrypoints/inertia.tsx +0 -92
  153. data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
  154. data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
  155. data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
  156. data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
  157. data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
  158. data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
  159. data/app/frontend/hooks/use-appearance.tsx +0 -67
  160. data/app/frontend/hooks/use-clipboard.ts +0 -32
  161. data/app/frontend/hooks/use-flash.tsx +0 -23
  162. data/app/frontend/hooks/use-hotkeys.ts +0 -63
  163. data/app/frontend/hooks/use-initials.tsx +0 -15
  164. data/app/frontend/hooks/use-live.ts +0 -205
  165. data/app/frontend/hooks/use-mobile-navigation.ts +0 -8
  166. data/app/frontend/hooks/use-mobile.ts +0 -21
  167. data/app/frontend/hooks/use-percentile.ts +0 -24
  168. data/app/frontend/hooks/use-reduced-motion.ts +0 -25
  169. data/app/frontend/hooks/use-row-nav.ts +0 -106
  170. data/app/frontend/hooks/use-step.ts +0 -27
  171. data/app/frontend/hooks/use-ticker.ts +0 -12
  172. data/app/frontend/hooks/use-window.ts +0 -58
  173. data/app/frontend/layouts/app/app-sidebar-layout.tsx +0 -107
  174. data/app/frontend/layouts/app-layout.tsx +0 -21
  175. data/app/frontend/layouts/env-layout.tsx +0 -53
  176. data/app/frontend/layouts/persistent-layout.tsx +0 -18
  177. data/app/frontend/lib/cable.ts +0 -10
  178. data/app/frontend/lib/env-pages.ts +0 -236
  179. data/app/frontend/lib/execution-path.ts +0 -55
  180. data/app/frontend/lib/explain.ts +0 -50
  181. data/app/frontend/lib/filter.ts +0 -25
  182. data/app/frontend/lib/flamegraph.ts +0 -155
  183. data/app/frontend/lib/format.ts +0 -71
  184. data/app/frontend/lib/keyboard.ts +0 -18
  185. data/app/frontend/lib/mount.ts +0 -23
  186. data/app/frontend/lib/source-link.ts +0 -217
  187. data/app/frontend/lib/utils.ts +0 -6
  188. data/app/frontend/pages/alerts/index.tsx +0 -171
  189. data/app/frontend/pages/broadcasts/index.tsx +0 -171
  190. data/app/frontend/pages/cache_events/index.tsx +0 -127
  191. data/app/frontend/pages/commands/index.tsx +0 -143
  192. data/app/frontend/pages/deploys/index.tsx +0 -94
  193. data/app/frontend/pages/deploys/show.tsx +0 -309
  194. data/app/frontend/pages/deprecations/index.tsx +0 -176
  195. data/app/frontend/pages/exceptions/index.tsx +0 -271
  196. data/app/frontend/pages/executions/show.tsx +0 -821
  197. data/app/frontend/pages/issues/index.tsx +0 -399
  198. data/app/frontend/pages/issues/show.tsx +0 -1367
  199. data/app/frontend/pages/jobs/index.tsx +0 -381
  200. data/app/frontend/pages/jobs/klass.tsx +0 -215
  201. data/app/frontend/pages/llm_calls/index.tsx +0 -664
  202. data/app/frontend/pages/logs/index.tsx +0 -303
  203. data/app/frontend/pages/mails/index.tsx +0 -218
  204. data/app/frontend/pages/notifications/index.tsx +0 -161
  205. data/app/frontend/pages/outgoing_requests/index.tsx +0 -309
  206. data/app/frontend/pages/overview/show.tsx +0 -333
  207. data/app/frontend/pages/people/index.tsx +0 -102
  208. data/app/frontend/pages/people/show.tsx +0 -149
  209. data/app/frontend/pages/processes/index.tsx +0 -335
  210. data/app/frontend/pages/profiles/index.tsx +0 -110
  211. data/app/frontend/pages/profiles/show.tsx +0 -105
  212. data/app/frontend/pages/queries/index.tsx +0 -492
  213. data/app/frontend/pages/queries/show.tsx +0 -276
  214. data/app/frontend/pages/releases/index.tsx +0 -190
  215. data/app/frontend/pages/releases/release-health.tsx +0 -68
  216. data/app/frontend/pages/releases/release-path.ts +0 -25
  217. data/app/frontend/pages/releases/show.tsx +0 -262
  218. data/app/frontend/pages/requests/index.tsx +0 -235
  219. data/app/frontend/pages/requests/route.tsx +0 -159
  220. data/app/frontend/pages/scheduled_tasks/index.tsx +0 -220
  221. data/app/frontend/pages/spans/index.tsx +0 -229
  222. data/app/frontend/pages/spans/show.tsx +0 -220
  223. data/app/frontend/pages/storage_ops/index.tsx +0 -150
  224. data/app/frontend/pages/tenants/index.tsx +0 -236
  225. data/app/frontend/pages/tenants/show.tsx +0 -477
  226. data/app/frontend/pages/tenants/tenant-path.ts +0 -26
  227. data/app/frontend/pages/thresholds/index.tsx +0 -414
  228. data/app/frontend/pages/traces/show.tsx +0 -208
  229. data/app/frontend/pages/transactions/index.tsx +0 -220
  230. data/app/frontend/pages/view_renders/index.tsx +0 -241
  231. data/app/frontend/pages/visits/index.tsx +0 -321
  232. data/app/frontend/public/icon.png +0 -0
  233. data/app/frontend/public/icon.svg +0 -5
  234. data/app/frontend/routes/index.d.ts +0 -2136
  235. data/app/frontend/routes/index.js +0 -2112
  236. data/app/frontend/types/globals.d.ts +0 -9
  237. data/app/frontend/types/index.ts +0 -288
  238. data/app/frontend/types/vite-env.d.ts +0 -9
  239. data/app/helpers/railwatch/assets_helper.rb +0 -52
  240. data/app/jobs/railwatch/anomaly_scan_job.rb +0 -11
  241. data/app/jobs/railwatch/application_job.rb +0 -7
  242. data/app/jobs/railwatch/auto_resolve_issues_job.rb +0 -19
  243. data/app/jobs/railwatch/check_scheduled_tasks_job.rb +0 -113
  244. data/app/jobs/railwatch/detect_anomalies_job.rb +0 -171
  245. data/app/jobs/railwatch/detect_performance_issues_job.rb +0 -95
  246. data/app/jobs/railwatch/group_exceptions_job.rb +0 -91
  247. data/app/jobs/railwatch/optimize_telemetry_job.rb +0 -23
  248. data/app/jobs/railwatch/performance_scan_job.rb +0 -11
  249. data/app/jobs/railwatch/prune_telemetry_job.rb +0 -129
  250. data/app/jobs/railwatch/release_health_rollup_job.rb +0 -64
  251. data/app/jobs/railwatch/rollup_catchup_job.rb +0 -21
  252. data/app/jobs/railwatch/rollup_job.rb +0 -132
  253. data/app/jobs/railwatch/scheduled_task_scan_job.rb +0 -11
  254. data/app/models/concerns/railwatch/detection_snapshotting.rb +0 -20
  255. data/app/models/railwatch/alert.rb +0 -229
  256. data/app/models/railwatch/alert_rule.rb +0 -121
  257. data/app/models/railwatch/anomaly_rule.rb +0 -51
  258. data/app/models/railwatch/application.rb +0 -44
  259. data/app/models/railwatch/application_record.rb +0 -33
  260. data/app/models/railwatch/comment.rb +0 -36
  261. data/app/models/railwatch/deploy.rb +0 -67
  262. data/app/models/railwatch/environment.rb +0 -68
  263. data/app/models/railwatch/execution_presenter.rb +0 -185
  264. data/app/models/railwatch/filter_query.rb +0 -184
  265. data/app/models/railwatch/followup_receipt.rb +0 -27
  266. data/app/models/railwatch/ingest/batch.rb +0 -355
  267. data/app/models/railwatch/ingest/mapper.rb +0 -585
  268. data/app/models/railwatch/ingest/payload.rb +0 -96
  269. data/app/models/railwatch/ingest/rollup_absorber.rb +0 -170
  270. data/app/models/railwatch/ingest/writer.rb +0 -137
  271. data/app/models/railwatch/issue.rb +0 -277
  272. data/app/models/railwatch/issue_activity.rb +0 -23
  273. data/app/models/railwatch/issue_detection_presenter.rb +0 -309
  274. data/app/models/railwatch/issue_detection_snapshot.rb +0 -77
  275. data/app/models/railwatch/maintenance_task.rb +0 -53
  276. data/app/models/railwatch/saved_view.rb +0 -63
  277. data/app/models/railwatch/telemetry/aggregations.rb +0 -251
  278. data/app/models/railwatch/telemetry/attachment.rb +0 -31
  279. data/app/models/railwatch/telemetry/bounded_gzip.rb +0 -101
  280. data/app/models/railwatch/telemetry/broadcast.rb +0 -13
  281. data/app/models/railwatch/telemetry/cache_event.rb +0 -16
  282. data/app/models/railwatch/telemetry/child.rb +0 -53
  283. data/app/models/railwatch/telemetry/cursor_page.rb +0 -99
  284. data/app/models/railwatch/telemetry/deprecation.rb +0 -13
  285. data/app/models/railwatch/telemetry/enqueued_job.rb +0 -13
  286. data/app/models/railwatch/telemetry/exception.rb +0 -28
  287. data/app/models/railwatch/telemetry/execution.rb +0 -152
  288. data/app/models/railwatch/telemetry/export_delivery.rb +0 -32
  289. data/app/models/railwatch/telemetry/export_destination.rb +0 -66
  290. data/app/models/railwatch/telemetry/health_sample.rb +0 -72
  291. data/app/models/railwatch/telemetry/ingest_batch.rb +0 -31
  292. data/app/models/railwatch/telemetry/llm_call.rb +0 -37
  293. data/app/models/railwatch/telemetry/log.rb +0 -91
  294. data/app/models/railwatch/telemetry/mail.rb +0 -13
  295. data/app/models/railwatch/telemetry/n_plus_one.rb +0 -92
  296. data/app/models/railwatch/telemetry/notification.rb +0 -13
  297. data/app/models/railwatch/telemetry/outgoing_request.rb +0 -13
  298. data/app/models/railwatch/telemetry/person.rb +0 -50
  299. data/app/models/railwatch/telemetry/process.rb +0 -10
  300. data/app/models/railwatch/telemetry/profile.rb +0 -37
  301. data/app/models/railwatch/telemetry/query.rb +0 -57
  302. data/app/models/railwatch/telemetry/query_shape.rb +0 -45
  303. data/app/models/railwatch/telemetry/release_health.rb +0 -69
  304. data/app/models/railwatch/telemetry/rollup.rb +0 -157
  305. data/app/models/railwatch/telemetry/session.rb +0 -24
  306. data/app/models/railwatch/telemetry/span.rb +0 -19
  307. data/app/models/railwatch/telemetry/storage_op.rb +0 -13
  308. data/app/models/railwatch/telemetry/tenant.rb +0 -240
  309. data/app/models/railwatch/telemetry/transaction.rb +0 -13
  310. data/app/models/railwatch/telemetry/view_render.rb +0 -13
  311. data/app/models/railwatch/telemetry/visit.rb +0 -24
  312. data/app/models/railwatch/telemetry_record.rb +0 -107
  313. data/app/models/railwatch/threshold.rb +0 -66
  314. data/app/models/railwatch/user.rb +0 -47
  315. data/app/models/railwatch/viewer.rb +0 -13
  316. data/app/models/railwatch/window.rb +0 -66
  317. data/app/views/layouts/railwatch/dashboard.html.erb +0 -25
  318. data/db/railwatch_migrate/20260916000000_create_railwatch_tables.rb +0 -151
  319. data/db/railwatch_migrate/20260917000000_create_railwatch_maintenance_tasks.rb +0 -18
  320. data/db/railwatch_migrate/20260917120000_create_railwatch_followup_receipts.rb +0 -20
  321. data/db/railwatch_migrate/20260918120000_widen_host_user_ids.rb +0 -71
  322. data/db/railwatch_telemetry_migrate/20260903000000_enable_incremental_vacuum.rb +0 -62
  323. data/db/railwatch_telemetry_migrate/20260903000001_create_telemetry.rb +0 -481
  324. data/db/railwatch_telemetry_migrate/20260903000002_rename_tenant_to_app_tenant.rb +0 -14
  325. data/db/railwatch_telemetry_migrate/20260903000003_add_statement_count_to_transactions.rb +0 -9
  326. data/db/railwatch_telemetry_migrate/20260903000004_add_role_and_channel.rb +0 -10
  327. data/db/railwatch_telemetry_migrate/20260903000005_add_locals_to_exceptions.rb +0 -9
  328. data/db/railwatch_telemetry_migrate/20260903000006_add_spans_health_vitals_and_fts.rb +0 -82
  329. data/db/railwatch_telemetry_migrate/20260903000007_rename_span_attributes_to_payload.rb +0 -10
  330. data/db/railwatch_telemetry_migrate/20260903000008_add_profiles_and_attachments.rb +0 -60
  331. data/db/railwatch_telemetry_migrate/20260903000009_add_truncated_to_attachments.rb +0 -9
  332. data/db/railwatch_telemetry_migrate/20260903000010_create_sessions_and_release_health.rb +0 -51
  333. data/db/railwatch_telemetry_migrate/20260903000011_add_fingerprint_to_exceptions.rb +0 -11
  334. data/db/railwatch_telemetry_migrate/20260904000012_add_failed_to_broadcasts.rb +0 -10
  335. data/db/railwatch_telemetry_migrate/20260904010000_add_filter_cursor_indexes.rb +0 -37
  336. data/db/railwatch_telemetry_migrate/20260904020000_add_n_plus_ones_execution_id_index.rb +0 -11
  337. data/db/railwatch_telemetry_migrate/20260904120000_create_query_shapes.rb +0 -15
  338. data/db/railwatch_telemetry_migrate/20260906120000_add_backpressure_factor_to_ingest_batches.rb +0 -9
  339. data/db/railwatch_telemetry_migrate/20260907000000_rename_lantern_version_on_processes.rb +0 -10
  340. data/db/railwatch_telemetry_migrate/20260913000000_rename_nightrail_version_on_processes.rb +0 -9
  341. data/db/railwatch_telemetry_migrate/20260914000000_drop_orphan_durable_ingest_tables.rb +0 -18
  342. data/db/railwatch_telemetry_migrate/20260914010000_drop_orphan_durable_ingest_columns.rb +0 -25
  343. data/db/railwatch_telemetry_migrate/20260915000000_create_llm_calls.rb +0 -55
  344. data/db/railwatch_telemetry_migrate/20260915120000_add_detail_to_llm_calls.rb +0 -21
  345. data/db/railwatch_telemetry_migrate/20260915200000_add_explained_index_to_queries.rb +0 -16
  346. data/db/railwatch_telemetry_migrate/20260915210000_add_slowest_index_to_queries.rb +0 -18
  347. data/db/railwatch_telemetry_migrate/20260917010000_add_batch_ledger_to_ingest_batches.rb +0 -16
  348. data/db/railwatch_telemetry_migrate/20260919000100_create_export_queue.rb +0 -146
  349. data/db/railwatch_telemetry_migrate/20260922000000_add_covering_indexes_for_dashboard_aggregates.rb +0 -34
  350. data/db/railwatch_telemetry_migrate/20260923000000_add_people_index.rb +0 -15
  351. data/db/railwatch_telemetry_migrate/20260925000000_add_tenant_summary_index.rb +0 -17
  352. data/db/railwatch_telemetry_migrate/20261008000000_add_in_transaction_to_outgoing_requests.rb +0 -10
  353. data/docs/embedded.md +0 -503
  354. data/lib/puma/plugin/railwatch.rb +0 -215
  355. data/lib/railwatch/dashboard_assets.rb +0 -45
  356. data/lib/railwatch/embedded.rb +0 -50
  357. data/lib/railwatch/export/client.rb +0 -124
  358. data/lib/railwatch/export/lease.rb +0 -56
  359. data/lib/railwatch/export/outbox.rb +0 -361
  360. data/lib/railwatch/export/policy.rb +0 -79
  361. data/lib/railwatch/export/sender.rb +0 -130
  362. data/lib/railwatch/ingest_request_body_limit.rb +0 -10
  363. data/lib/railwatch/maintenance.rb +0 -194
  364. data/lib/railwatch/patches/migration_busy_timeout.rb +0 -73
  365. data/lib/railwatch/transport/local.rb +0 -78
  366. data/lib/railwatch/transport/socket.rb +0 -183
  367. data/lib/railwatch/writer.rb +0 -370
  368. data/public/railwatch/assets/CommitMono-Bold-D6h61ieg.woff2 +0 -0
  369. data/public/railwatch/assets/CommitMono-Regular-zr8w7Obm.woff2 +0 -0
  370. data/public/railwatch/assets/Roboto-Black-auA4GeOK.woff2 +0 -0
  371. data/public/railwatch/assets/Roboto-Bold-CJLnO8j1.woff2 +0 -0
  372. data/public/railwatch/assets/Roboto-Medium-Cm2bwKpj.woff2 +0 -0
  373. data/public/railwatch/assets/Roboto-Regular-Chaq1-PV.woff2 +0 -0
  374. data/public/railwatch/assets/app-layout-by7G3sa1.js +0 -41
  375. data/public/railwatch/assets/application-Cc5v_Vk9.css +0 -1
  376. data/public/railwatch/assets/arrow-up-DCZ7emux.js +0 -1
  377. data/public/railwatch/assets/braces-CykT5_5u.js +0 -1
  378. data/public/railwatch/assets/card-C-PPSA0g.js +0 -1
  379. data/public/railwatch/assets/chart-Cov79YC5.js +0 -39
  380. data/public/railwatch/assets/chart-hover-DdK4E-2h.js +0 -1
  381. data/public/railwatch/assets/chart-panel-BPaYRXJB.js +0 -1
  382. data/public/railwatch/assets/code-BYjC4YZ_.js +0 -1
  383. data/public/railwatch/assets/copy-id-LPfoj9cC.js +0 -1
  384. data/public/railwatch/assets/cursor-load-more-DSdhgpGl.js +0 -1
  385. data/public/railwatch/assets/data-table-CQ_YRWQs.js +0 -1
  386. data/public/railwatch/assets/env-layout-CdMWbUkm.js +0 -1
  387. data/public/railwatch/assets/execution-path-7MYt-Qbn.js +0 -1
  388. data/public/railwatch/assets/filter-bar-CrzpQ_vD.js +0 -1
  389. data/public/railwatch/assets/flamegraph-BIFEdTAg.js +0 -2
  390. data/public/railwatch/assets/format-B9SDkrWj.js +0 -1
  391. data/public/railwatch/assets/frames-C5t-x4f0.js +0 -1
  392. data/public/railwatch/assets/index--NQ7us-L.js +0 -1
  393. data/public/railwatch/assets/index-2kgYe_99.js +0 -1
  394. data/public/railwatch/assets/index-4YEPHxtO.js +0 -1
  395. data/public/railwatch/assets/index-AOCC1GDJ.js +0 -1
  396. data/public/railwatch/assets/index-BEo-u0eO.js +0 -1
  397. data/public/railwatch/assets/index-BJ1V3w2N.js +0 -1
  398. data/public/railwatch/assets/index-BPoHsBKb.js +0 -1
  399. data/public/railwatch/assets/index-BZzyGUM3.js +0 -1
  400. data/public/railwatch/assets/index-BgVq7yKL.js +0 -1
  401. data/public/railwatch/assets/index-BqFGEZvW.js +0 -1
  402. data/public/railwatch/assets/index-ByuHJ1CH.js +0 -1
  403. data/public/railwatch/assets/index-C2j50AL2.js +0 -1
  404. data/public/railwatch/assets/index-CLEdTkvW.js +0 -1
  405. data/public/railwatch/assets/index-CWgq8qBu.js +0 -1
  406. data/public/railwatch/assets/index-CXQ7_oVj.js +0 -2
  407. data/public/railwatch/assets/index-CmyYrjS2.js +0 -1
  408. data/public/railwatch/assets/index-Cn9_HjDo.js +0 -1
  409. data/public/railwatch/assets/index-CsMAm9UK.js +0 -1
  410. data/public/railwatch/assets/index-D0Kk_oGY.js +0 -1
  411. data/public/railwatch/assets/index-DC9ZptJh.js +0 -1
  412. data/public/railwatch/assets/index-DNzg7jmV.js +0 -1
  413. data/public/railwatch/assets/index-DQ8uJO5f.js +0 -1
  414. data/public/railwatch/assets/index-Deq14BxB.js +0 -1
  415. data/public/railwatch/assets/index-IaV3Blzp.js +0 -1
  416. data/public/railwatch/assets/index-TYNWcJQs.js +0 -1
  417. data/public/railwatch/assets/index-WonH2fcZ.js +0 -1
  418. data/public/railwatch/assets/index-WxWUaH_d.js +0 -1
  419. data/public/railwatch/assets/index-mUNrl4ky.js +0 -1
  420. data/public/railwatch/assets/inertia-DfNRARuA.js +0 -97
  421. data/public/railwatch/assets/json-viewer-DMsF5glp.js +0 -1
  422. data/public/railwatch/assets/klass-CmHYk5rz.js +0 -1
  423. data/public/railwatch/assets/origin-identity-1R3bqa3R.js +0 -1
  424. data/public/railwatch/assets/percentile-picker-73z5PAps.js +0 -1
  425. data/public/railwatch/assets/relative-time-Dsc_cCdX.js +0 -1
  426. data/public/railwatch/assets/release-health-BV5OaOWd.js +0 -1
  427. data/public/railwatch/assets/route-CWh58C7K.js +0 -1
  428. data/public/railwatch/assets/segmented-DB4zh-Am.js +0 -1
  429. data/public/railwatch/assets/select-DBKjFlOX.js +0 -1
  430. data/public/railwatch/assets/series-chart-BaiDG795.js +0 -1
  431. data/public/railwatch/assets/show-B4ICCFGh.js +0 -1
  432. data/public/railwatch/assets/show-B5lOU9Gv.js +0 -1
  433. data/public/railwatch/assets/show-BYPB74t4.js +0 -2
  434. data/public/railwatch/assets/show-C7A0m2M8.js +0 -1
  435. data/public/railwatch/assets/show-DT5X1zqE.js +0 -1
  436. data/public/railwatch/assets/show-D_NBdXsn.js +0 -2
  437. data/public/railwatch/assets/show-DgPCnRg8.js +0 -1
  438. data/public/railwatch/assets/show-Yl69HF0x.js +0 -1
  439. data/public/railwatch/assets/show-_QxuUQCm.js +0 -3
  440. data/public/railwatch/assets/show-g__jnN36.js +0 -1
  441. data/public/railwatch/assets/show-l4ztfjl9.js +0 -1
  442. data/public/railwatch/assets/sort-header-BcG8ulEH.js +0 -1
  443. data/public/railwatch/assets/source-link-B2183i2-.js +0 -1
  444. data/public/railwatch/assets/sparkline-cell-B47ipscB.js +0 -1
  445. data/public/railwatch/assets/stat-DT2hhsqS.js +0 -1
  446. data/public/railwatch/assets/status-badge-CmbRKlY7.js +0 -1
  447. data/public/railwatch/assets/tenant-path-C4rNGauE.js +0 -1
  448. data/public/railwatch/assets/use-clipboard-oTNeP4ax.js +0 -1
  449. data/public/railwatch/assets/use-live-CLZZj245.js +0 -1
  450. data/public/railwatch/icon.png +0 -0
  451. data/public/railwatch/icon.svg +0 -5
  452. data/public/railwatch/manifest.json +0 -1245
  453. data/public/railwatch/rails-vite.json +0 -1
  454. /data/{app/frontend/lib → lib/generators/railwatch/install/templates}/railwatch.ts +0 -0
@@ -1,309 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # Builds the same discriminated performance/anomaly detail contract for the
5
- # Inertia page, REST API, and MCP. The durable detector snapshot remains useful
6
- # after raw data is pruned; live rollups add trend and deploy comparison when
7
- # they are still available.
8
- class IssueDetectionPresenter
9
- TREND_DAYS = 30
10
- DEPLOY_LIMIT = 20
11
- EXECUTION_TYPES = IssueDetectionSnapshot::EXECUTION_TYPES
12
- SCHEMA_VERSION = 1
13
- NUMERIC_FIELDS = {
14
- "measurement" => %w[value limit],
15
- "baseline" => %w[mean stddev sigmas days deviation]
16
- }.freeze
17
- RULE_STRING_FIELDS = %w[type target_kind target task_key schedule last_run expected].freeze
18
- EXECUTION_SOURCES = %w[request job job_attempt scheduled_task command].freeze
19
- CHILD_TYPES = %w[query outgoing_request].freeze
20
- TARGET_KIND_FOR = {
21
- "request" => "requests", "job_attempt" => "jobs", "command" => "commands",
22
- "query" => "queries", "scheduled_task" => "scheduled_tasks",
23
- "outgoing_request" => "outgoing_requests"
24
- }.freeze
25
-
26
- def initialize(issue)
27
- @issue = issue
28
- @details = issue.sample["detection"]
29
- end
30
-
31
- def as_json
32
- return unless @issue.kind.in?(%w[performance anomaly])
33
- return unavailable_json unless @details.is_a?(Hash)
34
-
35
- validate_details!
36
- live = @issue.environment.with_telemetry { live_details }
37
- @details.deep_symbolize_keys.merge(
38
- available: true,
39
- count_label: "Breached evaluation windows",
40
- breached_windows: @issue.occurrences,
41
- event_count: live[:event_count],
42
- trend: live[:trend],
43
- deploy_comparison: live[:deploy_comparison],
44
- representative_references: representative_references,
45
- representative_records: representatives
46
- )
47
- rescue KeyError, ArgumentError, TypeError
48
- unavailable_json
49
- end
50
-
51
- private
52
-
53
- def unavailable_json
54
- {
55
- kind: @issue.kind, available: false,
56
- count_label: "Breached evaluation windows", breached_windows: @issue.occurrences,
57
- message: "Typed detector context was not captured for this historical issue."
58
- }
59
- end
60
-
61
- def validate_details!
62
- raise ArgumentError unless @details.fetch("schema_version") == SCHEMA_VERSION
63
- raise ArgumentError unless @details.fetch("kind") == @issue.kind
64
- telemetry_type = @details.fetch("telemetry_type")
65
- raise ArgumentError unless telemetry_type.in?(EXECUTION_TYPES + %w[query outgoing_request])
66
- target_kind = TARGET_KIND_FOR.fetch(telemetry_type)
67
- allowed_target_kinds = @issue.kind == "anomaly" ? AnomalyRule::TARGET_KINDS : Threshold::TARGET_KINDS
68
- raise ArgumentError unless target_kind.in?(allowed_target_kinds)
69
- %w[telemetry_group_hash target metric unit].each do |key|
70
- raise ArgumentError unless @details.fetch(key).is_a?(String) && @details.fetch(key).present?
71
- end
72
- metric = @details.fetch("metric")
73
- allowed_metrics = @issue.kind == "anomaly" ? AnomalyRule::METRICS : Threshold::METRICS
74
- raise ArgumentError unless metric.in?(allowed_metrics)
75
- raise ArgumentError if metric == "missed" && @details.fetch("telemetry_type") != "scheduled_task"
76
- expected_unit = IssueDetectionSnapshot::UNITS[metric]
77
- raise ArgumentError unless expected_unit && @details.fetch("unit") == expected_unit
78
- validate_window!
79
- NUMERIC_FIELDS.each { |key, fields| validate_numeric_object!(key, fields) }
80
- validate_rule!
81
- validate_detector_shape!
82
- validate_representatives!
83
- end
84
-
85
- def validate_window!
86
- window = @details.fetch("window")
87
- raise ArgumentError unless window.is_a?(Hash)
88
- from = Time.iso8601(window.fetch("from"))
89
- to = Time.iso8601(window.fetch("to"))
90
- minutes = window.fetch("minutes")
91
- raise ArgumentError unless finite_number?(minutes) && minutes.positive? && to > from
92
- raise ArgumentError unless (minutes - ((to - from) / 60.0).round(2)).abs <= 0.01
93
- return if @details.fetch("metric") == "missed"
94
-
95
- raise ArgumentError unless minutes == minutes.to_i
96
- maximum = @issue.kind == "anomaly" ? AnomalyRule::MAX_WINDOW_MINUTES : Threshold::MAX_WINDOW_MINUTES
97
- raise ArgumentError if minutes > maximum
98
- end
99
-
100
- def validate_numeric_object!(key, fields)
101
- value = @details[key]
102
- return if value.nil?
103
- raise ArgumentError unless value.is_a?(Hash)
104
- fields.each { |field| raise ArgumentError if value.key?(field) && !finite_number?(value[field]) }
105
- if key == "measurement"
106
- %w[value limit].each { |field| raise ArgumentError if value.key?(field) && value[field].negative? }
107
- raise ArgumentError if value.key?("limit") && !value["limit"].positive?
108
- else
109
- %w[mean stddev].each { |field| raise ArgumentError if value.key?(field) && value[field].negative? }
110
- raise ArgumentError if value.key?("days") && (!value["days"].is_a?(Integer) || !value["days"].positive?)
111
- raise ArgumentError if value.key?("deviation") && !value["deviation"].positive?
112
- end
113
- end
114
-
115
- def validate_rule!
116
- rule = @details["rule"]
117
- if rule.nil?
118
- raise ArgumentError if @details.fetch("metric") == "missed"
119
- return
120
- end
121
- raise ArgumentError unless rule.is_a?(Hash)
122
- RULE_STRING_FIELDS.each do |field|
123
- raise ArgumentError if rule.key?(field) && (!rule[field].is_a?(String) || rule[field].blank?)
124
- end
125
- raise ArgumentError if rule.key?("id") && (!rule["id"].is_a?(Integer) || !rule["id"].positive?)
126
- %w[last_run expected].each { |field| Time.iso8601(rule[field]) if rule.key?(field) }
127
- return unless @details.fetch("metric") == "missed"
128
-
129
- raise ArgumentError unless rule["type"] == "schedule"
130
- %w[task_key schedule last_run expected].each { |field| raise ArgumentError unless rule[field].is_a?(String) && rule[field].present? }
131
- end
132
-
133
- def validate_detector_shape!
134
- if @issue.kind == "anomaly"
135
- require_numeric_fields!("measurement", %w[value])
136
- baseline = require_numeric_fields!("baseline", %w[mean stddev sigmas days deviation])
137
- raise ArgumentError unless baseline.fetch("sigmas").positive?
138
- raise ArgumentError unless baseline.fetch("days").between?(
139
- AnomalyRule::MIN_BASELINE_DAYS, AnomalyRule::MAX_BASELINE_DAYS
140
- )
141
- raise ArgumentError if baseline.fetch("deviation") > AnomalyRule::MAX_DEVIATION
142
- require_detector_rule!("anomaly")
143
- elsif @details.fetch("metric") != "missed"
144
- measurement = require_numeric_fields!("measurement", %w[value limit])
145
- raise ArgumentError if measurement.fetch("limit") > Threshold::MAX_LIMIT
146
- raise ArgumentError if @details.fetch("metric").end_with?("rate") && measurement.fetch("limit") > 100
147
- require_detector_rule!("threshold")
148
- end
149
- end
150
-
151
- def require_numeric_fields!(key, fields)
152
- object = @details.fetch(key)
153
- raise ArgumentError unless object.is_a?(Hash)
154
- fields.each { |field| raise ArgumentError unless finite_number?(object.fetch(field)) }
155
- object
156
- end
157
-
158
- def require_detector_rule!(type)
159
- rule = @details.fetch("rule")
160
- raise ArgumentError unless rule.is_a?(Hash) && rule.fetch("type") == type
161
- raise ArgumentError unless rule.fetch("id").is_a?(Integer) && rule.fetch("id").positive?
162
- raise ArgumentError unless rule.fetch("target").is_a?(String) && rule.fetch("target").present?
163
- expected_target_kind = TARGET_KIND_FOR.fetch(@details.fetch("telemetry_type"))
164
- raise ArgumentError unless rule.fetch("target_kind") == expected_target_kind
165
- end
166
-
167
- def validate_representatives!
168
- references = @details.fetch("representative_records", [])
169
- raise ArgumentError unless references.is_a?(Array) && references.size <= IssueDetectionSnapshot::REPRESENTATIVE_LIMIT
170
-
171
- references.each do |reference|
172
- raise ArgumentError unless reference.is_a?(Hash)
173
- raise ArgumentError unless reference["record_type"] == @details.fetch("telemetry_type")
174
- raise ArgumentError unless reference["record_id"].is_a?(Integer) && reference["record_id"].positive?
175
- raise ArgumentError unless reference["group_hash"] == @details.fetch("telemetry_group_hash")
176
- execution_id = reference["execution_id"]
177
- raise ArgumentError unless execution_id.nil? || (execution_id.is_a?(String) && execution_id.present?)
178
- raise ArgumentError if reference["record_type"] != "query" && execution_id.blank?
179
- source = reference["execution_source"]
180
- if CHILD_TYPES.include?(reference["record_type"])
181
- raise ArgumentError unless source.is_a?(String) && source.present? && source.in?(EXECUTION_SOURCES)
182
- else
183
- raise ArgumentError unless source == reference["record_type"]
184
- end
185
- end
186
- end
187
-
188
- def finite_number?(value)
189
- value.is_a?(Numeric) && (!value.respond_to?(:finite?) || value.finite?)
190
- end
191
-
192
- def live_details
193
- type = @details.fetch("telemetry_type")
194
- group_hash = @details.fetch("telemetry_group_hash")
195
- window = @details.fetch("window")
196
- from = Time.iso8601(window.fetch("from"))
197
- to = Time.iso8601(window.fetch("to"))
198
- rollups = Telemetry::Rollup.for_type(type).where(group_hash: group_hash)
199
- window_summary = Telemetry::Rollup.summarize(rollups.between(from, to))
200
- now = Time.current
201
- # Rollup.between includes the whole lower-bound hour, so the denominator
202
- # for count rates must begin at that same boundary.
203
- trend_from = (now - TREND_DAYS.days).beginning_of_hour
204
- trend_rows = rollups.between(trend_from, now).order(:bucket).to_a.group_by { |row| row.bucket.to_date }
205
- {
206
- event_count: window_summary[:count],
207
- trend: trend_rows.map { |day, rows| trend_point(day, rows, trend_from, now) },
208
- deploy_comparison: deploy_comparison(type, group_hash)
209
- }
210
- end
211
-
212
- def trend_point(day, rows, trend_from, now)
213
- summary = Telemetry::Rollup.summarize(rows)
214
- day_start = day.in_time_zone.beginning_of_day
215
- minutes = ([ day_start + 1.day, now ].min - [ day_start, trend_from ].max) / 60.0
216
- { day: day.iso8601, value: metric_value(@details.fetch("metric"), summary, minutes: minutes), count: summary[:count] }
217
- end
218
-
219
- def metric_value(metric, summary, minutes: nil)
220
- case metric
221
- when "p95" then milliseconds(summary[:p95])
222
- when "max" then milliseconds(summary[:max])
223
- when "avg" then milliseconds(summary[:avg])
224
- when "error_rate", "failure_rate"
225
- summary[:count].zero? ? nil : (summary[:errors] * 100.0 / summary[:count]).round(2)
226
- when "count" then minutes&.positive? ? (summary[:count] / minutes).round(2) : nil
227
- end
228
- end
229
-
230
- def deploy_comparison(type, group_hash)
231
- scope = record_scope(type, group_hash).where(occurred_at: TREND_DAYS.days.ago..).where.not(deploy: nil)
232
- scope.group(:deploy).pluck(:deploy, Arel.sql("COUNT(*)"), Arel.sql("AVG(duration)"), Arel.sql("MAX(duration)"))
233
- .map { |deploy, count, avg, max| { deploy: deploy, events: count, avg_ms: milliseconds(avg), max_ms: milliseconds(max) } }
234
- .sort_by { |row| -row[:events] }.first(DEPLOY_LIMIT)
235
- end
236
-
237
- def record_scope(type, group_hash)
238
- if type.in?(EXECUTION_TYPES)
239
- Telemetry::Execution.of_kind(type).where(group_hash: group_hash)
240
- elsif type == "query"
241
- Telemetry::Query.where(group_hash: group_hash).with_sql
242
- elsif type == "outgoing_request"
243
- Telemetry::OutgoingRequest.where(group_hash: group_hash)
244
- else
245
- raise ArgumentError, "unsupported detection telemetry type: #{type}"
246
- end
247
- end
248
-
249
- def representatives
250
- references = Array(@details["representative_records"]).select { |record| record.is_a?(Hash) }
251
- return [] if references.empty?
252
-
253
- type = @details.fetch("telemetry_type")
254
- group_hash = @details.fetch("telemetry_group_hash")
255
- records = @issue.environment.with_telemetry do
256
- record_scope(type, group_hash).where(id: references.filter_map { |record| record["record_id"] }).index_by(&:id)
257
- end
258
- references.filter_map do |reference|
259
- record = records[reference["record_id"]]
260
- record_json(record, type)&.merge(drilldown: drilldown(reference))
261
- end
262
- end
263
-
264
- def representative_references
265
- Array(@details["representative_records"]).filter_map do |reference|
266
- next unless reference.is_a?(Hash)
267
- reference.slice("record_type", "record_id", "group_hash", "execution_id", "execution_source").deep_symbolize_keys
268
- .merge(drilldown: drilldown(reference))
269
- end
270
- end
271
-
272
- def record_json(record, type)
273
- return unless record
274
-
275
- common = {
276
- record_type: type, record_id: record.id, group_hash: record.group_hash,
277
- execution_id: record.execution_id, execution_source: record.execution_source,
278
- execution_preview: record.execution_preview, occurred_at: record.occurred_at,
279
- duration_ms: milliseconds(record.duration), deploy: record.deploy,
280
- user_ref: record.user_ref, tenant: record.app_tenant
281
- }
282
- case record
283
- when Telemetry::Execution
284
- common.merge(name: record.name, status: record.status, outcome: record.outcome)
285
- when Telemetry::Query
286
- common.merge(name: record.sql.to_s.first(300), source: record.source,
287
- connection: record.connection, role: record.role)
288
- when Telemetry::OutgoingRequest
289
- common.merge(name: "#{record.method} #{record.host}", status: record.status_code,
290
- source: record.source)
291
- end
292
- end
293
-
294
- # Identifiers only: app/frontend/lib/execution-path.ts turns these into
295
- # links, and it already accepts both the "job" child wire name and the
296
- # "job_attempt" parent kind.
297
- def drilldown(record)
298
- if record["record_type"] == "query"
299
- { kind: "query", group_hash: record["group_hash"] }
300
- elsif record["execution_id"].present?
301
- { kind: record["execution_source"].presence || record["record_type"], execution_id: record["execution_id"] }
302
- end
303
- end
304
-
305
- def milliseconds(value)
306
- value.nil? ? nil : (value.to_f / 1000.0).round(3)
307
- end
308
- end
309
- end
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # Durable, typed context for synthetic performance/anomaly issues. Detector
5
- # group hashes identify the rule, not the telemetry rows behind the breach;
6
- # this snapshot preserves that original identity and a bounded set of
7
- # drill-down identifiers even after raw telemetry is pruned. Record payloads
8
- # remain subject to the telemetry retention policy and are never copied into
9
- # the primary database.
10
- class IssueDetectionSnapshot
11
- REPRESENTATIVE_LIMIT = 10
12
- EXECUTION_TYPES = %w[request job_attempt scheduled_task command].freeze
13
- UNITS = {
14
- "p95" => "milliseconds", "max" => "milliseconds", "avg" => "milliseconds",
15
- "error_rate" => "percent", "failure_rate" => "percent", "count" => "events_per_minute",
16
- "missed" => "schedule"
17
- }.freeze
18
-
19
- def self.capture(environment:, kind:, telemetry_type:, telemetry_group_hash:, target:, metric:, from:, to:,
20
- value: nil, limit: nil, baseline: nil, rule: {}, representative_from: nil)
21
- new(environment, telemetry_type, telemetry_group_hash).capture(
22
- kind: kind, target: target, metric: metric, from: from, to: to,
23
- value: value, limit: limit, baseline: baseline, rule: rule, representative_from: representative_from
24
- )
25
- end
26
-
27
- def initialize(environment, telemetry_type, telemetry_group_hash)
28
- @environment = environment
29
- @telemetry_type = telemetry_type
30
- @telemetry_group_hash = telemetry_group_hash
31
- end
32
-
33
- def capture(kind:, target:, metric:, from:, to:, value:, limit:, baseline:, rule:, representative_from:)
34
- representatives = @environment.with_telemetry do
35
- representative_scope(representative_from || from, to).order(duration: :desc).limit(REPRESENTATIVE_LIMIT)
36
- .map { |record| record_json(record) }
37
- end
38
- {
39
- schema_version: 1, kind: kind, telemetry_type: @telemetry_type,
40
- telemetry_group_hash: @telemetry_group_hash, target: target, metric: metric,
41
- unit: UNITS.fetch(metric, "value"),
42
- window: { from: from.iso8601(6), to: to.iso8601(6), minutes: ((to - from) / 60.0).round(2) },
43
- measurement: { value: value, limit: limit }.compact,
44
- baseline: baseline&.compact,
45
- rule: rule.compact,
46
- representative_records: representatives
47
- }.compact
48
- end
49
-
50
- private
51
-
52
- def representative_scope(from, to)
53
- model = record_model
54
- # SQLite compares its stored six-digit timestamp strings lexically. A
55
- # frozen whole-second upper bound serializes without the fraction and
56
- # would otherwise exclude a record at that exact instant.
57
- scope = model.where(group_hash: @telemetry_group_hash, occurred_at: from...(to + 1.second))
58
- EXECUTION_TYPES.include?(@telemetry_type) ? scope.of_kind(@telemetry_type) : scope
59
- end
60
-
61
- def record_model
62
- return Telemetry::Execution if EXECUTION_TYPES.include?(@telemetry_type)
63
- return Telemetry::Query if @telemetry_type == "query"
64
- return Telemetry::OutgoingRequest if @telemetry_type == "outgoing_request"
65
-
66
- raise ArgumentError, "unsupported detection telemetry type: #{@telemetry_type}"
67
- end
68
-
69
- def record_json(record)
70
- {
71
- record_type: @telemetry_type, record_id: record.id, group_hash: record.group_hash,
72
- execution_id: record.execution_id,
73
- execution_source: record.is_a?(Telemetry::Execution) ? record.kind : record.execution_source
74
- }
75
- end
76
- end
77
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # One row per maintenance task (Railwatch::Maintenance::TASKS): when it last
5
- # ran and who holds it now. Every process on the host runs the maintenance
6
- # clock; this is how exactly one of them runs each task. The claim is a
7
- # single conditional UPDATE, so SQLite's write lock is the arbiter and there
8
- # is no window between checking and taking the lease.
9
- class MaintenanceTask < ApplicationRecord
10
- self.table_name = "railwatch_maintenance_tasks"
11
-
12
- # The token for a claim this process won, or nil: the task is due
13
- # (last_run_at older than `every`, or never) and nobody holds a live
14
- # lease on it. A lease left by a process that died expires on its own.
15
- # The token is per claim, not per process, so a claim that outlived its
16
- # lease cannot later release the lease the next claimant holds.
17
- def self.claim(name, every:, lease:, owner:, now: Time.current)
18
- ensure_row(name)
19
- token = "#{owner}:#{SecureRandom.hex(8)}"
20
- won = where(name: name)
21
- .where("lease_expires_at IS NULL OR lease_expires_at < ?", now)
22
- .where("last_run_at IS NULL OR last_run_at <= ?", now - every)
23
- .update_all(lease_owner: token, lease_expires_at: now + lease, updated_at: now) == 1
24
- won ? token : nil
25
- end
26
-
27
- # Releases only the lease this token holds. A task that succeeded records
28
- # the run so the interval starts again; one that failed records nothing,
29
- # so it is eligible on the next tick rather than a full interval later.
30
- def self.release(name, token:, ran_at:, succeeded:)
31
- changes = { lease_owner: nil, lease_expires_at: nil, updated_at: ran_at }
32
- changes[:last_run_at] = ran_at if succeeded
33
- where(name: name, lease_owner: token).update_all(changes)
34
- end
35
-
36
- # The newest tick across every task: what the doctor reports as "last
37
- # maintenance", and the signal that the clock is alive somewhere.
38
- def self.last_tick_at
39
- maximum(:last_run_at)
40
- end
41
-
42
- # Two processes racing to create the same row: one loses on the unique
43
- # index, and the row it wanted now exists.
44
- def self.ensure_row(name)
45
- return if exists?(name: name)
46
-
47
- create!(name: name)
48
- rescue ActiveRecord::RecordNotUnique
49
- nil
50
- end
51
- private_class_method :ensure_row
52
- end
53
- end
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # A named, shareable filter on one environment page: "5xx checkout requests",
5
- # "acme tenant logs". Pinned views appear in the sidebar under the page.
6
- class SavedView < ApplicationRecord
7
- self.table_name = "railwatch_saved_views"
8
- # page => the index route helper the view links back to. The link is built
9
- # server-side so the sidebar, the menu, and a pasted URL all agree.
10
- PAGE_PATHS = {
11
- "requests" => :application_environment_requests_path,
12
- "jobs" => :application_environment_jobs_path,
13
- "scheduled_tasks" => :application_environment_scheduled_tasks_path,
14
- "commands" => :application_environment_commands_path,
15
- "exceptions" => :application_environment_exceptions_path,
16
- "queries" => :application_environment_queries_path,
17
- "spans" => :application_environment_spans_path,
18
- "logs" => :application_environment_logs_path,
19
- "visits" => :application_environment_visits_path,
20
- "people" => :application_environment_people_path,
21
- "tenants" => :application_environment_tenants_path,
22
- "llm_calls" => :application_environment_llm_calls_path,
23
- "outgoing_requests" => :application_environment_outgoing_requests_path,
24
- "cache_events" => :application_environment_cache_events_path,
25
- "mails" => :application_environment_mails_path,
26
- "notifications" => :application_environment_notifications_path
27
- }.freeze
28
- PAGES = PAGE_PATHS.keys.freeze
29
-
30
- def environment = Environment.current
31
- def user
32
- User.find_by(id: viewer_id)
33
- end
34
-
35
- def user=(u)
36
- self.viewer_id = u&.id&.to_s
37
- end
38
-
39
- validates :name, presence: true, length: { maximum: 80 }
40
- validates :page, inclusion: { in: PAGES }
41
-
42
- scope :pinned, -> { where(pinned: true) }
43
- scope :visible_to, ->(user) { where(shared: true).or(where(viewer_id: user.id.to_s)) }
44
-
45
- # The props every environment page shares, ready for the sidebar and the
46
- # Views menu: one entry per view this user is allowed to see.
47
- def self.props_for(environment, user)
48
- where(environment_id: environment.id).visible_to(user).order(:name).map { |view| view.props(environment, user) }
49
- end
50
-
51
- def props(environment, user)
52
- { id: id, name: name, page: page, query: query, window: window, params: params, pinned: pinned, shared: shared,
53
- mine: viewer_id == user&.id, url: path_for(environment) }
54
- end
55
-
56
- # This page's index path with the view's window, query, and extra params
57
- # (sort/dir) applied -- the shareable URL.
58
- def path_for(environment)
59
- Railwatch.url_helpers.public_send(PAGE_PATHS.fetch(page), environment.application_id, environment.id,
60
- { window: window.presence, q: query.presence, **params.to_h.symbolize_keys }.compact)
61
- end
62
- end
63
- end