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,229 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class Alert < ApplicationRecord
5
- self.table_name = "railwatch_alerts"
6
- MAX_DELIVERY_ATTEMPTS = 5
7
- DELIVERY_LEASE = 5.minutes
8
- ENQUEUE_LEASE = 5.minutes
9
- RETRY_DELAYS = [ 1.minute, 5.minutes, 15.minutes, 1.hour ].freeze
10
-
11
- # An alert is always addressed to an integration; the rule is only why it
12
- # fired, and a "Send test" delivery has no rule behind it at all.
13
- # No integrations in an embedded install yet: alerts are recorded, not delivered.
14
- def integration = nil
15
-
16
- def integration=(_)
17
- nil
18
- end
19
- belongs_to :alert_rule, optional: true
20
- belongs_to :issue, optional: true
21
- belongs_to :summary_alert, class_name: "Alert", optional: true
22
- has_many :summarized_alerts, class_name: "Alert", foreign_key: :summary_alert_id,
23
- dependent: :nullify, inverse_of: :summary_alert
24
-
25
- # `delivering` is a leased claim. A process that disappears while holding
26
- # one is recovered by ReconcileAlertDeliveriesJob after DELIVERY_LEASE.
27
- STATUSES = %w[pending delivering sent failed skipped collapsed summarized].freeze
28
- TERMINAL_STATUSES = %w[sent failed skipped summarized].freeze
29
-
30
- attr_accessor :suppress_auto_enqueue
31
-
32
- before_validation :inherit_integration, on: :create
33
- before_validation :assign_delivery_metadata, on: :create
34
- after_create_commit :enqueue_initial_work, unless: :suppress_auto_enqueue
35
-
36
- # How far back reconciliation looks. An alert older than this that is
37
- # still pending or collapsed predates the outbox (a row an old container
38
- # wrote, or one left over from before this shipped); delivering it now
39
- # would page someone about something that happened hours ago.
40
- RECONCILE_WINDOW = 1.hour
41
-
42
- scope :due_for_delivery, ->(now = Time.current) {
43
- where(status: "pending").where(created_at: (now - RECONCILE_WINDOW)..)
44
- .where("next_delivery_at IS NULL OR next_delivery_at <= ?", now)
45
- .where("delivery_enqueued_until IS NULL OR delivery_enqueued_until <= ?", now)
46
- }
47
- scope :with_stale_delivery_lease, ->(now = Time.current) {
48
- where(status: "delivering").where("delivery_lease_expires_at <= ?", now)
49
- }
50
- scope :overdue_collapsed, ->(now = Time.current) {
51
- where(status: "collapsed").where(created_at: (now - RECONCILE_WINDOW)..(now - AlertRule::BURST_WINDOW))
52
- }
53
-
54
- # Reserve a queue hand-off. If the process dies after this write and before
55
- # perform_later, reconciliation retries once this short reservation expires.
56
- def enqueue_delivery!(wait_until: nil)
57
- reserved_until = nil
58
- target = nil
59
- with_lock do
60
- return false unless status == "pending"
61
-
62
- target = [ wait_until, next_delivery_at, Time.current ].compact.max
63
- return false if delivery_enqueued_until&.future?
64
-
65
- reserved_until = target + ENQUEUE_LEASE
66
- update!(delivery_enqueued_until: reserved_until)
67
- end
68
-
69
- if target.future?
70
- DeliverAlertJob.set(wait_until: target).perform_later(self)
71
- else
72
- DeliverAlertJob.perform_later(self)
73
- end
74
- true
75
- rescue StandardError
76
- Alert.where(id: id, status: "pending", delivery_enqueued_until: reserved_until)
77
- .update_all(delivery_enqueued_until: nil, updated_at: Time.current) if reserved_until
78
- raise
79
- end
80
-
81
- # Atomically turns one queued job into the only worker allowed to deliver.
82
- # Duplicate jobs and jobs for terminal alerts return nil without I/O.
83
- def claim_delivery!
84
- with_lock do
85
- return unless status == "pending"
86
- return if next_delivery_at.present? && next_delivery_at.future?
87
-
88
- token = SecureRandom.uuid
89
- update!(status: "delivering", delivery_attempts: delivery_attempts + 1,
90
- delivery_key: delivery_key.presence || SecureRandom.uuid,
91
- delivery_lease_id: token, delivery_lease_expires_at: DELIVERY_LEASE.from_now,
92
- delivery_enqueued_until: nil)
93
- token
94
- end
95
- end
96
-
97
- def perform_claimed_delivery!(token)
98
- if integration.enabled?
99
- integration.deliver(self)
100
- finish_claim!(token, "sent")
101
- else
102
- finish_claim!(token, "skipped")
103
- end
104
- end
105
-
106
- # Persists retry intent before another job is scheduled. A crash at the
107
- # following queue boundary is recovered by ReconcileAlertDeliveriesJob.
108
- def fail_claim!(token, exception)
109
- message = "#{exception.class}: #{exception.message}".first(1000)
110
- retry_at = nil
111
- with_lock do
112
- return unless owns_delivery_claim?(token)
113
-
114
- if delivery_attempts >= MAX_DELIVERY_ATTEMPTS
115
- update!(status: "failed", error: message, next_delivery_at: nil,
116
- delivery_lease_id: nil, delivery_lease_expires_at: nil, delivery_enqueued_until: nil)
117
- else
118
- retry_at = RETRY_DELAYS.fetch(delivery_attempts - 1).from_now
119
- update!(status: "pending", error: message, next_delivery_at: retry_at,
120
- delivery_lease_id: nil, delivery_lease_expires_at: nil, delivery_enqueued_until: nil)
121
- end
122
- integration.update_columns(last_error: message)
123
- end
124
- retry_at
125
- end
126
-
127
- # A lease expiry is indistinguishable from a worker dying after the remote
128
- # endpoint accepted the message but before `sent` committed. Retrying is
129
- # required for at-least-once delivery; webhook consumers can deduplicate by
130
- # delivery_key / Idempotency-Key.
131
- def recover_stale_delivery!
132
- retryable = false
133
- with_lock do
134
- return false unless status == "delivering" && delivery_lease_expires_at&.past?
135
-
136
- message = error.presence || "Delivery lease expired before completion"
137
- if delivery_attempts >= MAX_DELIVERY_ATTEMPTS
138
- update!(status: "failed", error: message, next_delivery_at: nil,
139
- delivery_lease_id: nil, delivery_lease_expires_at: nil, delivery_enqueued_until: nil)
140
- else
141
- update!(status: "pending", error: message, next_delivery_at: Time.current,
142
- delivery_lease_id: nil, delivery_lease_expires_at: nil, delivery_enqueued_until: nil)
143
- retryable = true
144
- end
145
- end
146
- retryable
147
- end
148
-
149
- # Explicit operator action: terminal failures get a fresh, bounded attempt
150
- # budget. Sent/skipped alerts are intentionally not replayed by this path.
151
- def retry_delivery!
152
- with_lock do
153
- return false unless status == "failed"
154
-
155
- update!(status: "pending", error: nil, delivery_attempts: 0,
156
- next_delivery_at: Time.current, delivery_enqueued_until: nil,
157
- delivery_lease_id: nil, delivery_lease_expires_at: nil)
158
- end
159
- enqueue_delivery!
160
- end
161
-
162
- # Synchronous delivery is retained for "Send test", whose HTTP response
163
- # must report the real destination outcome. Normal alerts use the leased job
164
- # path above.
165
- def deliver!
166
- return self if TERMINAL_STATUSES.include?(status)
167
- return update!(status: "skipped") unless integration.enabled?
168
-
169
- integration.deliver(self)
170
- mark_sent!
171
- self
172
- rescue StandardError => e
173
- message = "#{e.class}: #{e.message}".first(1000)
174
- update!(status: "failed", error: message)
175
- integration.update_columns(last_error: message)
176
- raise
177
- end
178
-
179
- private
180
-
181
- def enqueue_initial_work
182
- # An embedded install has no integration to deliver to and no delivery
183
- # jobs to enqueue on; the alert is recorded and that is the end of it.
184
- return if integration.nil?
185
-
186
- if status == "pending"
187
- enqueue_delivery!
188
- elsif status == "collapsed" && alert_rule
189
- SummarizeAlertBurstJob.set(wait_until: created_at + AlertRule::BURST_WINDOW).perform_later(alert_rule)
190
- end
191
- end
192
-
193
- def assign_delivery_metadata
194
- self.delivery_key ||= SecureRandom.uuid
195
- self.next_delivery_at ||= Time.current if status.blank? || status == "pending"
196
- end
197
-
198
- def inherit_integration
199
- self.integration ||= alert_rule&.integration
200
- end
201
-
202
- def owns_delivery_claim?(token)
203
- status == "delivering" && delivery_lease_id == token
204
- end
205
-
206
- def finish_claim!(token, outcome)
207
- with_lock do
208
- return false unless owns_delivery_claim?(token)
209
-
210
- attributes = { status: outcome, error: nil, next_delivery_at: nil,
211
- delivery_lease_id: nil, delivery_lease_expires_at: nil,
212
- delivery_enqueued_until: nil }
213
- attributes[:sent_at] = Time.current if outcome == "sent"
214
- update!(attributes)
215
- if outcome == "sent"
216
- integration.update_columns(last_delivered_at: Time.current, last_error: nil)
217
- issue&.activities&.create!(kind: "alert", data: { event: event, integration: integration.kind })
218
- end
219
- true
220
- end
221
- end
222
-
223
- def mark_sent!
224
- update!(status: "sent", sent_at: Time.current, error: nil)
225
- integration.update_columns(last_delivered_at: Time.current, last_error: nil)
226
- issue&.activities&.create!(kind: "alert", data: { event: event, integration: integration.kind })
227
- end
228
- end
229
- end
@@ -1,121 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class AlertRule < ApplicationRecord
5
- self.table_name = "railwatch_alert_rules"
6
- TITLE_PATTERN_MAX_LENGTH = 256
7
- TITLE_PATTERN_TIMEOUT = 0.01
8
- EVENTS = %w[new_issue regressed_issue resolved_issue ignored_issue assigned_issue threshold anomaly silent_host quota crash_free_drop].freeze
9
-
10
- # A deploy that breaks one page opens dozens of issues in a minute, and
11
- # each one used to be its own Slack message. These two events are the ones
12
- # that arrive in bursts.
13
- BURST_EVENTS = %w[new_issue regressed_issue].freeze
14
- BURST_WINDOW = 60.seconds
15
- # Deliver this many individually, then fold the rest of the window into one
16
- # summary message.
17
- BURST_LIMIT = 5
18
-
19
- def application = Application.current
20
- def integration = nil
21
- has_many :alerts, dependent: :destroy
22
-
23
- validates :event, inclusion: { in: EVENTS }
24
- validate :valid_title_pattern
25
-
26
- # De-duplicate: one alert per issue per event per 30 minutes.
27
- def fire!(event:, issue: nil, payload: {})
28
- # Serialize the duplicate/burst decision per rule. The alert is born in
29
- # its final outbox state, so its after-commit hook can never enqueue a row
30
- # that is about to become collapsed.
31
- with_lock do
32
- return if issue && alerts.where(issue: issue, event: event).where("created_at > ?", 30.minutes.ago).exists?
33
-
34
- status = bursting?(event, issue) ? "collapsed" : "pending"
35
- alerts.create!(event: event, issue: issue, payload: payload, status: status)
36
- end
37
- end
38
-
39
- # Optional narrowing of what this rule notifies about, stored in `filters`:
40
- # environment_ids, kinds, min_priority, title_pattern. An absent or empty
41
- # filter matches everything; an issue-less alert (quota, silent_host) is
42
- # only narrowed by environment, via payload[:environment_id].
43
- def matches?(issue:, payload: {})
44
- return false unless environment_matches?(issue, payload)
45
- return true unless issue
46
- kind_matches?(issue) && priority_matches?(issue) && title_matches?(issue)
47
- end
48
-
49
- private
50
-
51
- def valid_title_pattern
52
- pattern = filters["title_pattern"].to_s
53
- return if pattern.blank?
54
-
55
- if pattern.length > TITLE_PATTERN_MAX_LENGTH
56
- errors.add(:filters, "title pattern is too long (maximum is #{TITLE_PATTERN_MAX_LENGTH} characters)")
57
- return
58
- end
59
- return unless regular_expression_pattern?(pattern)
60
-
61
- compile_title_pattern(pattern)
62
- rescue RegexpError => e
63
- errors.add(:filters, "title pattern is not a valid regular expression: #{e.message}")
64
- end
65
-
66
- # True once this rule has already fired BURST_LIMIT issue alerts inside the
67
- # window. Only alerts that carry an issue are counted, so summary rows can
68
- # never inflate the next window's tally.
69
- def bursting?(event, issue)
70
- return false unless BURST_EVENTS.include?(event) && issue
71
- alerts.where(event: event, created_at: BURST_WINDOW.ago..).where.not(issue_id: nil).count >= BURST_LIMIT
72
- end
73
-
74
- def environment_matches?(issue, payload)
75
- ids = Array(filters["environment_ids"]).compact_blank.map(&:to_i)
76
- return true if ids.empty?
77
- ids.include?((issue&.environment_id || payload[:environment_id] || payload["environment_id"]).to_i)
78
- end
79
-
80
- def kind_matches?(issue)
81
- kinds = Array(filters["kinds"]).compact_blank
82
- kinds.empty? || kinds.include?(issue.kind)
83
- end
84
-
85
- def priority_matches?(issue)
86
- minimum = filters["min_priority"]
87
- return true if minimum.blank? || Issue::PRIORITIES.exclude?(minimum)
88
- Issue::PRIORITIES.index(issue.priority) >= Issue::PRIORITIES.index(minimum)
89
- end
90
-
91
- # A pattern wrapped in slashes is a regular expression, anything else is a
92
- # case-insensitive substring.
93
- def title_matches?(issue)
94
- pattern = filters["title_pattern"].to_s
95
- return true if pattern.blank?
96
- if regular_expression_pattern?(pattern)
97
- begin
98
- return issue.title.match?(compile_title_pattern(pattern))
99
- rescue Regexp::TimeoutError
100
- # A pattern that backtracks past its budget matches nothing rather
101
- # than stalling every alert behind it.
102
- Rails.logger.warn("alert rule title pattern timed out alert_rule_id=#{id}")
103
- return false
104
- rescue RegexpError
105
- # An unparsable regexp degrades to a substring match on its source, so
106
- # a rule saved before this was validated does not silently go dark.
107
- pattern = pattern[1..-2]
108
- end
109
- end
110
- issue.title.downcase.include?(pattern.downcase)
111
- end
112
-
113
- def regular_expression_pattern?(pattern)
114
- pattern.start_with?("/") && pattern.end_with?("/") && pattern.length > 2
115
- end
116
-
117
- def compile_title_pattern(pattern)
118
- Regexp.new(pattern[1..-2], Regexp::IGNORECASE, timeout: TITLE_PATTERN_TIMEOUT)
119
- end
120
- end
121
- end
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # "Alert when p95 of GET /checkout is more than 3 standard deviations above
5
- # its 7-day baseline for the same time of day." Evaluated by
6
- # DetectAnomaliesJob against hourly rollups.
7
- class AnomalyRule < ApplicationRecord
8
- self.table_name = "railwatch_anomaly_rules"
9
- MIN_BASELINE_DAYS = 3
10
- MAX_BASELINE_DAYS = 30
11
- MAX_DEVIATION = 10
12
- MAX_WINDOW_MINUTES = 1_440
13
- TARGET_KINDS = %w[requests jobs queries outgoing_requests scheduled_tasks llm_calls].freeze
14
- # spend included because "we are spending well above baseline for this
15
- # hour of the week" is the LLM alert that needs no threshold picked.
16
- METRICS = %w[p95 avg count error_rate spend].freeze
17
- SHARED_METRICS = %w[p95 avg count error_rate].freeze
18
- LLM_METRICS = %w[spend].freeze
19
-
20
- def self.metrics_for(target_kind)
21
- target_kind == "llm_calls" ? SHARED_METRICS + LLM_METRICS : SHARED_METRICS
22
- end
23
-
24
- def environment = Environment.current
25
-
26
- validates :target_kind, inclusion: { in: TARGET_KINDS }
27
- validates :metric, inclusion: { in: METRICS }
28
- validate :metric_applies_to_target_kind
29
- validates :target, presence: true, length: { maximum: 256 }
30
- validates :deviation, numericality: { greater_than: 0, less_than_or_equal_to: MAX_DEVIATION }
31
- validates :window_minutes, numericality: { only_integer: true, greater_than: 0,
32
- less_than_or_equal_to: MAX_WINDOW_MINUTES }
33
- validates :baseline_days, numericality: { only_integer: true, greater_than_or_equal_to: MIN_BASELINE_DAYS,
34
- less_than_or_equal_to: MAX_BASELINE_DAYS }
35
-
36
- scope :enabled, -> { where(enabled: true) }
37
-
38
- def description
39
- "#{target_kind} #{target == '*' ? 'all' : target}: #{metric} > #{deviation}σ above #{baseline_days}-day baseline"
40
- end
41
-
42
- private
43
-
44
- def metric_applies_to_target_kind
45
- return if metric.blank? || target_kind.blank?
46
- return if AnomalyRule.metrics_for(target_kind).include?(metric)
47
-
48
- errors.add(:metric, "is not available for #{target_kind}")
49
- end
50
- end
51
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # The one application an embedded install monitors: the host. Not a table.
5
- # Issue keys need a prefix and a counter; the rest of what the platform's
6
- # Application carries (account, repository links, alert rules) is either
7
- # fixed here or empty.
8
- class Application
9
- ID = 1
10
-
11
- attr_reader :id, :name, :slug
12
-
13
- def self.current = @current ||= new
14
- def self.find(id)
15
- raise ActiveRecord::RecordNotFound, "Application #{id}" unless id.to_i == ID
16
- current
17
- end
18
-
19
- def initialize
20
- @id = ID
21
- @name = Rails.application.class.module_parent_name
22
- @slug = @name.parameterize
23
- end
24
-
25
- def issue_prefix = Railwatch.config.issue_prefix || @name.upcase.gsub(/[^A-Z0-9]/, "")[0, 4].presence || "APP"
26
- def repository_url = Railwatch.config.repository_url
27
- def default_branch = "main"
28
- def environments = [ Environment.current ]
29
- def alert_rules = AlertRule.where(application_id: ID)
30
- def account = Railwatch::Embedded::Account
31
- def issues = Issue.where(application_id: ID)
32
-
33
- # Issue numbers are per application on the platform (APP-1, APP-2, ...),
34
- # allocated under a row lock on the application. There is no row here, so
35
- # the meta database's own write lock serialises it: Issue creation already
36
- # runs inside a transaction, and SQLite allows one writer.
37
- def next_issue_number!
38
- (Issue.where(application_id: ID).maximum(:number) || 0) + 1
39
- end
40
-
41
- include GlobalID::Identification
42
- def to_global_id(...) = GlobalID.create(self, app: "railwatch")
43
- end
44
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # Base class for what the engine owns about the monitored application besides
5
- # telemetry: issues and their comments and activity, deploys, saved views,
6
- # thresholds, anomaly rules, alert rules and alerts. These are authored and
7
- # permanent where telemetry is derived and pruned, so they live in their own
8
- # database (`railwatch` in the host's database.yml) and never in the host's
9
- # primary.
10
- class ApplicationRecord < ActiveRecord::Base
11
- self.abstract_class = true
12
- begin
13
- connects_to database: { writing: :railwatch, reading: :railwatch }
14
- rescue ActiveRecord::AdapterNotSpecified, LoadError
15
- # No `railwatch` entry in this environment's database.yml, or an entry
16
- # whose adapter gem is not in the bundle yet (LoadError). A cloud-transport
17
- # app has none and still eager-loads this class in production, and so
18
- # does the installer's own boot, before it has written the
19
- # entry -- so loading must not raise. Using it must, though: without
20
- # connects_to this class would inherit ActiveRecord::Base's PRIMARY
21
- # connection, and its tables are unprefixed, so a query would read and
22
- # a write would corrupt the host application's own tables. Every route
23
- # into the connection goes through connection_pool, so refusing here
24
- # fails closed for reads and writes alike.
25
- def self.connection_pool
26
- raise Railwatch::DatabaseNotConfigured,
27
- "the `railwatch` database (issues, comments, saved views and deploys) is not configured for the " \
28
- "#{Rails.env} environment; run `bin/rails generate railwatch:install` " \
29
- "or add it to config/database.yml (docs/embedded.md)"
30
- end
31
- end
32
- end
33
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class Comment < ApplicationRecord
5
- self.table_name = "railwatch_comments"
6
- SOURCES = %w[railwatch linear].freeze
7
-
8
- belongs_to :issue
9
- def user
10
- viewer_id && User.find_by(id: viewer_id)
11
- end
12
-
13
- def user=(u)
14
- self.viewer_id = u&.id&.to_s
15
- end
16
-
17
- validates :body, presence: true
18
- validates :source, inclusion: { in: SOURCES }
19
- validates :user, presence: true, if: -> { source == "railwatch" }
20
- validates :author_name, presence: true, if: -> { source == "linear" }
21
-
22
- after_create :record_activity
23
- after_create_commit :enqueue_linear_sync
24
-
25
- private
26
-
27
- def record_activity
28
- issue.activities.create!(kind: "comment", user: user,
29
- data: { comment_id: id, body: body, source: source, author_name: author_name })
30
- end
31
-
32
- def enqueue_linear_sync
33
- nil unless source == "railwatch"
34
- end
35
- end
36
- end
@@ -1,67 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class Deploy < ApplicationRecord
5
- self.table_name = "railwatch_deploys"
6
- def environment = Environment.current
7
-
8
- validates :deploy, presence: true, uniqueness: { scope: :environment_id }
9
- validates :deployed_at, presence: true
10
- # Rendered as the "Open" link on the deploy page. Same rule as
11
- # Application#repository_url, checked by parsing: one whole http(s) URL
12
- # with a host, so a hook (or whoever holds its token) cannot put a
13
- # javascript:/data: link, a hostless "https://?x", or an arbitrary
14
- # off-site destination behind a trusted button.
15
- validate :url_is_a_web_address
16
-
17
- scope :recent, -> { order(deployed_at: :desc) }
18
- scope :between, ->(from, to) { where(deployed_at: from..to) }
19
-
20
- before_validation :assign_previous_ref
21
- before_validation :trim_commits
22
-
23
- def short_ref
24
- ref.to_s.first(7).presence || deploy.first(12)
25
- end
26
-
27
- def commits_count
28
- commits.size
29
- end
30
-
31
- # The deploy immediately before this one in the same environment.
32
- def previous_deploy
33
- environment.deploys.where(deployed_at: ...deployed_at).recent.first
34
- end
35
-
36
- # The stretch of time this release was live: from its own deploy until the
37
- # next one, or until now for the current release. Release health is summed
38
- # over this, not over the page's window.
39
- def window
40
- [ deployed_at, environment.deploys.where("deployed_at > ?", deployed_at).order(:deployed_at).first&.deployed_at || Time.current ]
41
- end
42
-
43
- private
44
-
45
- def url_is_a_web_address
46
- return if url.blank?
47
- parsed = url.match?(/\A\S+\z/) ? URI.parse(url) : nil
48
- return if parsed.is_a?(URI::HTTP) && parsed.host.present?
49
- errors.add(:url, "must be a single http:// or https:// URL")
50
- rescue URI::InvalidURIError
51
- errors.add(:url, "must be a single http:// or https:// URL")
52
- end
53
-
54
- def assign_previous_ref
55
- return if previous_ref.present? || deployed_at.blank?
56
- self.previous_ref = previous_deploy&.ref
57
- end
58
-
59
- # The post-deploy hook sends its most recent commits (newest first) without
60
- # knowing where the last deploy stopped, so cut the list at that commit.
61
- def trim_commits
62
- return if previous_ref.blank?
63
- index = commits.index { |c| c["sha"].to_s.start_with?(previous_ref) }
64
- self.commits = commits.first(index) if index
65
- end
66
- end
67
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # The one environment an embedded install monitors: the host application in
5
- # its current Rails.env. Not a table. The hosted platform's Environment is an
6
- # account-scoped Active Record row with a token, quota and pause state; the
7
- # telemetry, ingest and dashboard code only ever asks it for an id, a slug,
8
- # a name, with_telemetry, and a few display attributes, so this stands in
9
- # with exactly that surface.
10
- class Environment
11
- ID = 1
12
-
13
- attr_reader :id, :name, :slug, :application_name
14
-
15
- def self.current = @current ||= new
16
-
17
- def self.find(id)
18
- raise ActiveRecord::RecordNotFound, "Environment #{id}" unless id.to_i == ID
19
- current
20
- end
21
-
22
- def initialize
23
- @id = ID
24
- @name = Rails.env.to_s
25
- @application_name = Rails.application.class.module_parent_name
26
- @slug = "#{@application_name.parameterize}-#{@name}"
27
- end
28
-
29
- def with_telemetry(&block) = TelemetryRecord.with_tenant(slug, &block)
30
- def telemetry_exists? = true
31
- def paused? = false
32
- def token_prefix = "embedded"
33
- def retention_days = 7
34
- # Deliberately read from the telemetry database rather than held in this
35
- # process. A deployed embedded install ingests in the writer process and
36
- # renders the dashboard in a web one, so a value set during ingest is
37
- # invisible to the page that needs it -- and a nil here is what the
38
- # dashboard treats as "no events yet", so it showed its install steps no
39
- # matter how much had been recorded. The newest batch row is the same
40
- # fact, in the file both processes already share, one indexed lookup away.
41
- # Ordered by received_at, not id: batches are written concurrently, so one
42
- # that captured an earlier receipt time can land after a later one, and
43
- # taking the newest row by insertion order would hand the dashboard a
44
- # timestamp that goes backwards. `recent` is the scope that states this.
45
- def last_seen_at
46
- with_telemetry { Telemetry::IngestBatch.recent.pick(:received_at) }
47
- end
48
- def application = Application.current
49
- def issues = Issue.where(environment_id: ID)
50
- def deploys = Deploy.where(environment_id: ID)
51
- def saved_views = SavedView.where(environment_id: ID)
52
- def thresholds = Threshold.where(environment_id: ID)
53
- def anomaly_rules = AnomalyRule.where(environment_id: ID)
54
- def events_this_month = 0
55
- # The platform lets an account list the servers it expects to report so the
56
- # dashboard can flag silent ones. One embedded install is its own server.
57
- def expected_servers = []
58
-
59
- # Ingest::Batch touches this after writing a batch. Nothing to record: the
60
- # batch row it just wrote is what last_seen_at reads.
61
- def update_columns(last_seen_at:) = nil
62
-
63
- # GlobalID for Active Job arguments (RollupJob.perform_later(environment, bucket)).
64
- include GlobalID::Identification
65
- def to_global_id(...) = GlobalID.create(self, app: "railwatch")
66
- def self.find_by_id(id) = find(id)
67
- end
68
- end