railwatch 0.9.4 → 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 (452) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +37 -33
  3. data/CHANGELOG.md +50 -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 +35 -66
  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/configuration.rb +21 -151
  21. data/lib/railwatch/engine.rb +9 -118
  22. data/lib/railwatch/patches.rb +0 -11
  23. data/lib/railwatch/reporter.rb +3 -19
  24. data/lib/railwatch/subscribers/process_info.rb +1 -2
  25. data/lib/railwatch/version.rb +1 -1
  26. data/lib/railwatch.rb +4 -62
  27. data/lib/tasks/railwatch_tasks.rake +3 -325
  28. data/llms.txt +23 -25
  29. metadata +3 -453
  30. data/app/channels/railwatch/environment_channel.rb +0 -62
  31. data/app/controllers/concerns/railwatch/telemetry_identity.rb +0 -25
  32. data/app/controllers/railwatch/alerts_controller.rb +0 -48
  33. data/app/controllers/railwatch/anomaly_rules_controller.rb +0 -31
  34. data/app/controllers/railwatch/attachments_controller.rb +0 -22
  35. data/app/controllers/railwatch/broadcasts_controller.rb +0 -15
  36. data/app/controllers/railwatch/cache_events_controller.rb +0 -30
  37. data/app/controllers/railwatch/commands_controller.rb +0 -16
  38. data/app/controllers/railwatch/comments_controller.rb +0 -11
  39. data/app/controllers/railwatch/dashboard_controller.rb +0 -70
  40. data/app/controllers/railwatch/deploys_controller.rb +0 -34
  41. data/app/controllers/railwatch/deprecations_controller.rb +0 -54
  42. data/app/controllers/railwatch/environment_scoped.rb +0 -81
  43. data/app/controllers/railwatch/exceptions_controller.rb +0 -52
  44. data/app/controllers/railwatch/executions_controller.rb +0 -13
  45. data/app/controllers/railwatch/issues_controller.rb +0 -258
  46. data/app/controllers/railwatch/jobs_controller.rb +0 -63
  47. data/app/controllers/railwatch/llm_calls_controller.rb +0 -124
  48. data/app/controllers/railwatch/logs_controller.rb +0 -40
  49. data/app/controllers/railwatch/mails_controller.rb +0 -19
  50. data/app/controllers/railwatch/notifications_controller.rb +0 -11
  51. data/app/controllers/railwatch/outgoing_requests_controller.rb +0 -19
  52. data/app/controllers/railwatch/overview_controller.rb +0 -38
  53. data/app/controllers/railwatch/people_controller.rb +0 -25
  54. data/app/controllers/railwatch/processes_controller.rb +0 -33
  55. data/app/controllers/railwatch/profiles_controller.rb +0 -65
  56. data/app/controllers/railwatch/queries_controller.rb +0 -59
  57. data/app/controllers/railwatch/releases_controller.rb +0 -75
  58. data/app/controllers/railwatch/requests_controller.rb +0 -37
  59. data/app/controllers/railwatch/saved_views_controller.rb +0 -53
  60. data/app/controllers/railwatch/scheduled_tasks_controller.rb +0 -45
  61. data/app/controllers/railwatch/spans_controller.rb +0 -47
  62. data/app/controllers/railwatch/storage_ops_controller.rb +0 -15
  63. data/app/controllers/railwatch/tenants_controller.rb +0 -44
  64. data/app/controllers/railwatch/thresholds_controller.rb +0 -47
  65. data/app/controllers/railwatch/traces_controller.rb +0 -72
  66. data/app/controllers/railwatch/transactions_controller.rb +0 -21
  67. data/app/controllers/railwatch/view_renders_controller.rb +0 -28
  68. data/app/controllers/railwatch/visits_controller.rb +0 -53
  69. data/app/frontend/components/app-content.tsx +0 -26
  70. data/app/frontend/components/app-logo-icon.tsx +0 -23
  71. data/app/frontend/components/app-logo.tsx +0 -14
  72. data/app/frontend/components/app-shell.tsx +0 -38
  73. data/app/frontend/components/app-sidebar-header.tsx +0 -54
  74. data/app/frontend/components/app-sidebar.tsx +0 -94
  75. data/app/frontend/components/app-wordmark.tsx +0 -29
  76. data/app/frontend/components/breadcrumbs.tsx +0 -46
  77. data/app/frontend/components/icon.tsx +0 -16
  78. data/app/frontend/components/input-error.tsx +0 -18
  79. data/app/frontend/components/nav-footer.tsx +0 -46
  80. data/app/frontend/components/nav-main.tsx +0 -41
  81. data/app/frontend/components/nav-user.tsx +0 -50
  82. data/app/frontend/components/railwatch/chart-hover.tsx +0 -34
  83. data/app/frontend/components/railwatch/chart-panel.tsx +0 -170
  84. data/app/frontend/components/railwatch/code.tsx +0 -28
  85. data/app/frontend/components/railwatch/command-palette.tsx +0 -132
  86. data/app/frontend/components/railwatch/copy-block.tsx +0 -52
  87. data/app/frontend/components/railwatch/copy-id.tsx +0 -49
  88. data/app/frontend/components/railwatch/cursor-load-more.tsx +0 -39
  89. data/app/frontend/components/railwatch/data-table.tsx +0 -195
  90. data/app/frontend/components/railwatch/digit-roll.tsx +0 -312
  91. data/app/frontend/components/railwatch/empty-state.tsx +0 -159
  92. data/app/frontend/components/railwatch/env-nav.tsx +0 -130
  93. data/app/frontend/components/railwatch/env-switcher.tsx +0 -145
  94. data/app/frontend/components/railwatch/filter-bar.tsx +0 -153
  95. data/app/frontend/components/railwatch/flamegraph.tsx +0 -324
  96. data/app/frontend/components/railwatch/frames.tsx +0 -295
  97. data/app/frontend/components/railwatch/json-viewer.tsx +0 -159
  98. data/app/frontend/components/railwatch/live-dot.tsx +0 -78
  99. data/app/frontend/components/railwatch/live-toggle.tsx +0 -76
  100. data/app/frontend/components/railwatch/metric-chart.tsx +0 -112
  101. data/app/frontend/components/railwatch/mobile-page-switcher.tsx +0 -89
  102. data/app/frontend/components/railwatch/onboarding.tsx +0 -91
  103. data/app/frontend/components/railwatch/origin-identity.tsx +0 -84
  104. data/app/frontend/components/railwatch/page-header.tsx +0 -42
  105. data/app/frontend/components/railwatch/percentile-picker.tsx +0 -18
  106. data/app/frontend/components/railwatch/relative-time.tsx +0 -18
  107. data/app/frontend/components/railwatch/saved-views.tsx +0 -232
  108. data/app/frontend/components/railwatch/segmented.tsx +0 -64
  109. data/app/frontend/components/railwatch/series-chart.tsx +0 -400
  110. data/app/frontend/components/railwatch/shortcuts-dialog.tsx +0 -89
  111. data/app/frontend/components/railwatch/signal-toast.tsx +0 -121
  112. data/app/frontend/components/railwatch/sort-header.tsx +0 -37
  113. data/app/frontend/components/railwatch/source-link.tsx +0 -57
  114. data/app/frontend/components/railwatch/sparkline-cell.tsx +0 -14
  115. data/app/frontend/components/railwatch/sql-block.tsx +0 -116
  116. data/app/frontend/components/railwatch/stat.tsx +0 -185
  117. data/app/frontend/components/railwatch/status-badge.tsx +0 -79
  118. data/app/frontend/components/railwatch/step-picker.tsx +0 -57
  119. data/app/frontend/components/railwatch/timeline.tsx +0 -405
  120. data/app/frontend/components/railwatch/utilisation-bar.tsx +0 -44
  121. data/app/frontend/components/railwatch/window-picker.tsx +0 -147
  122. data/app/frontend/components/ui/avatar.tsx +0 -51
  123. data/app/frontend/components/ui/badge.tsx +0 -46
  124. data/app/frontend/components/ui/breadcrumb.tsx +0 -109
  125. data/app/frontend/components/ui/button.tsx +0 -60
  126. data/app/frontend/components/ui/card.tsx +0 -92
  127. data/app/frontend/components/ui/chart.tsx +0 -374
  128. data/app/frontend/components/ui/checkbox.tsx +0 -32
  129. data/app/frontend/components/ui/collapsible.tsx +0 -31
  130. data/app/frontend/components/ui/command.tsx +0 -182
  131. data/app/frontend/components/ui/dialog.tsx +0 -158
  132. data/app/frontend/components/ui/dropdown-menu.tsx +0 -255
  133. data/app/frontend/components/ui/hover-card.tsx +0 -42
  134. data/app/frontend/components/ui/input.tsx +0 -21
  135. data/app/frontend/components/ui/label.tsx +0 -24
  136. data/app/frontend/components/ui/popover.tsx +0 -87
  137. data/app/frontend/components/ui/select.tsx +0 -185
  138. data/app/frontend/components/ui/separator.tsx +0 -28
  139. data/app/frontend/components/ui/sheet.tsx +0 -137
  140. data/app/frontend/components/ui/sidebar.tsx +0 -726
  141. data/app/frontend/components/ui/skeleton.tsx +0 -13
  142. data/app/frontend/components/ui/sonner.tsx +0 -41
  143. data/app/frontend/components/ui/table.tsx +0 -114
  144. data/app/frontend/components/ui/tabs.tsx +0 -91
  145. data/app/frontend/components/ui/textarea.tsx +0 -18
  146. data/app/frontend/components/ui/tooltip.tsx +0 -61
  147. data/app/frontend/components/user-info.tsx +0 -32
  148. data/app/frontend/components/user-menu-content.tsx +0 -94
  149. data/app/frontend/entrypoints/application.css +0 -244
  150. data/app/frontend/entrypoints/inertia.tsx +0 -92
  151. data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
  152. data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
  153. data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
  154. data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
  155. data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
  156. data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
  157. data/app/frontend/hooks/use-appearance.tsx +0 -67
  158. data/app/frontend/hooks/use-clipboard.ts +0 -32
  159. data/app/frontend/hooks/use-flash.tsx +0 -23
  160. data/app/frontend/hooks/use-hotkeys.ts +0 -63
  161. data/app/frontend/hooks/use-initials.tsx +0 -15
  162. data/app/frontend/hooks/use-live.ts +0 -205
  163. data/app/frontend/hooks/use-mobile-navigation.ts +0 -8
  164. data/app/frontend/hooks/use-mobile.ts +0 -21
  165. data/app/frontend/hooks/use-percentile.ts +0 -24
  166. data/app/frontend/hooks/use-reduced-motion.ts +0 -25
  167. data/app/frontend/hooks/use-row-nav.ts +0 -106
  168. data/app/frontend/hooks/use-step.ts +0 -27
  169. data/app/frontend/hooks/use-ticker.ts +0 -12
  170. data/app/frontend/hooks/use-window.ts +0 -58
  171. data/app/frontend/layouts/app/app-sidebar-layout.tsx +0 -107
  172. data/app/frontend/layouts/app-layout.tsx +0 -21
  173. data/app/frontend/layouts/env-layout.tsx +0 -53
  174. data/app/frontend/layouts/persistent-layout.tsx +0 -18
  175. data/app/frontend/lib/cable.ts +0 -10
  176. data/app/frontend/lib/env-pages.ts +0 -236
  177. data/app/frontend/lib/execution-path.ts +0 -55
  178. data/app/frontend/lib/explain.ts +0 -50
  179. data/app/frontend/lib/filter.ts +0 -25
  180. data/app/frontend/lib/flamegraph.ts +0 -155
  181. data/app/frontend/lib/format.ts +0 -71
  182. data/app/frontend/lib/keyboard.ts +0 -18
  183. data/app/frontend/lib/mount.ts +0 -23
  184. data/app/frontend/lib/source-link.ts +0 -217
  185. data/app/frontend/lib/utils.ts +0 -6
  186. data/app/frontend/pages/alerts/index.tsx +0 -171
  187. data/app/frontend/pages/broadcasts/index.tsx +0 -171
  188. data/app/frontend/pages/cache_events/index.tsx +0 -127
  189. data/app/frontend/pages/commands/index.tsx +0 -143
  190. data/app/frontend/pages/deploys/index.tsx +0 -94
  191. data/app/frontend/pages/deploys/show.tsx +0 -309
  192. data/app/frontend/pages/deprecations/index.tsx +0 -176
  193. data/app/frontend/pages/exceptions/index.tsx +0 -271
  194. data/app/frontend/pages/executions/show.tsx +0 -821
  195. data/app/frontend/pages/issues/index.tsx +0 -399
  196. data/app/frontend/pages/issues/show.tsx +0 -1367
  197. data/app/frontend/pages/jobs/index.tsx +0 -381
  198. data/app/frontend/pages/jobs/klass.tsx +0 -215
  199. data/app/frontend/pages/llm_calls/index.tsx +0 -664
  200. data/app/frontend/pages/logs/index.tsx +0 -303
  201. data/app/frontend/pages/mails/index.tsx +0 -218
  202. data/app/frontend/pages/notifications/index.tsx +0 -161
  203. data/app/frontend/pages/outgoing_requests/index.tsx +0 -309
  204. data/app/frontend/pages/overview/show.tsx +0 -333
  205. data/app/frontend/pages/people/index.tsx +0 -102
  206. data/app/frontend/pages/people/show.tsx +0 -149
  207. data/app/frontend/pages/processes/index.tsx +0 -335
  208. data/app/frontend/pages/profiles/index.tsx +0 -110
  209. data/app/frontend/pages/profiles/show.tsx +0 -105
  210. data/app/frontend/pages/queries/index.tsx +0 -492
  211. data/app/frontend/pages/queries/show.tsx +0 -276
  212. data/app/frontend/pages/releases/index.tsx +0 -190
  213. data/app/frontend/pages/releases/release-health.tsx +0 -68
  214. data/app/frontend/pages/releases/release-path.ts +0 -25
  215. data/app/frontend/pages/releases/show.tsx +0 -262
  216. data/app/frontend/pages/requests/index.tsx +0 -235
  217. data/app/frontend/pages/requests/route.tsx +0 -159
  218. data/app/frontend/pages/scheduled_tasks/index.tsx +0 -220
  219. data/app/frontend/pages/spans/index.tsx +0 -229
  220. data/app/frontend/pages/spans/show.tsx +0 -220
  221. data/app/frontend/pages/storage_ops/index.tsx +0 -150
  222. data/app/frontend/pages/tenants/index.tsx +0 -236
  223. data/app/frontend/pages/tenants/show.tsx +0 -477
  224. data/app/frontend/pages/tenants/tenant-path.ts +0 -26
  225. data/app/frontend/pages/thresholds/index.tsx +0 -414
  226. data/app/frontend/pages/traces/show.tsx +0 -208
  227. data/app/frontend/pages/transactions/index.tsx +0 -220
  228. data/app/frontend/pages/view_renders/index.tsx +0 -241
  229. data/app/frontend/pages/visits/index.tsx +0 -321
  230. data/app/frontend/public/icon.png +0 -0
  231. data/app/frontend/public/icon.svg +0 -5
  232. data/app/frontend/routes/index.d.ts +0 -2136
  233. data/app/frontend/routes/index.js +0 -2112
  234. data/app/frontend/types/globals.d.ts +0 -9
  235. data/app/frontend/types/index.ts +0 -288
  236. data/app/frontend/types/vite-env.d.ts +0 -9
  237. data/app/helpers/railwatch/assets_helper.rb +0 -52
  238. data/app/jobs/railwatch/anomaly_scan_job.rb +0 -11
  239. data/app/jobs/railwatch/application_job.rb +0 -7
  240. data/app/jobs/railwatch/auto_resolve_issues_job.rb +0 -19
  241. data/app/jobs/railwatch/check_scheduled_tasks_job.rb +0 -113
  242. data/app/jobs/railwatch/detect_anomalies_job.rb +0 -171
  243. data/app/jobs/railwatch/detect_performance_issues_job.rb +0 -95
  244. data/app/jobs/railwatch/group_exceptions_job.rb +0 -91
  245. data/app/jobs/railwatch/optimize_telemetry_job.rb +0 -23
  246. data/app/jobs/railwatch/performance_scan_job.rb +0 -11
  247. data/app/jobs/railwatch/prune_telemetry_job.rb +0 -129
  248. data/app/jobs/railwatch/release_health_rollup_job.rb +0 -64
  249. data/app/jobs/railwatch/rollup_catchup_job.rb +0 -21
  250. data/app/jobs/railwatch/rollup_job.rb +0 -132
  251. data/app/jobs/railwatch/scheduled_task_scan_job.rb +0 -11
  252. data/app/models/concerns/railwatch/detection_snapshotting.rb +0 -20
  253. data/app/models/railwatch/alert.rb +0 -229
  254. data/app/models/railwatch/alert_rule.rb +0 -121
  255. data/app/models/railwatch/anomaly_rule.rb +0 -51
  256. data/app/models/railwatch/application.rb +0 -44
  257. data/app/models/railwatch/application_record.rb +0 -33
  258. data/app/models/railwatch/comment.rb +0 -36
  259. data/app/models/railwatch/deploy.rb +0 -67
  260. data/app/models/railwatch/environment.rb +0 -68
  261. data/app/models/railwatch/execution_presenter.rb +0 -185
  262. data/app/models/railwatch/filter_query.rb +0 -184
  263. data/app/models/railwatch/followup_receipt.rb +0 -27
  264. data/app/models/railwatch/ingest/batch.rb +0 -355
  265. data/app/models/railwatch/ingest/mapper.rb +0 -585
  266. data/app/models/railwatch/ingest/payload.rb +0 -96
  267. data/app/models/railwatch/ingest/rollup_absorber.rb +0 -170
  268. data/app/models/railwatch/ingest/writer.rb +0 -137
  269. data/app/models/railwatch/issue.rb +0 -277
  270. data/app/models/railwatch/issue_activity.rb +0 -23
  271. data/app/models/railwatch/issue_detection_presenter.rb +0 -309
  272. data/app/models/railwatch/issue_detection_snapshot.rb +0 -77
  273. data/app/models/railwatch/maintenance_task.rb +0 -53
  274. data/app/models/railwatch/saved_view.rb +0 -63
  275. data/app/models/railwatch/telemetry/aggregations.rb +0 -251
  276. data/app/models/railwatch/telemetry/attachment.rb +0 -31
  277. data/app/models/railwatch/telemetry/bounded_gzip.rb +0 -101
  278. data/app/models/railwatch/telemetry/broadcast.rb +0 -13
  279. data/app/models/railwatch/telemetry/cache_event.rb +0 -16
  280. data/app/models/railwatch/telemetry/child.rb +0 -53
  281. data/app/models/railwatch/telemetry/cursor_page.rb +0 -99
  282. data/app/models/railwatch/telemetry/deprecation.rb +0 -13
  283. data/app/models/railwatch/telemetry/enqueued_job.rb +0 -13
  284. data/app/models/railwatch/telemetry/exception.rb +0 -28
  285. data/app/models/railwatch/telemetry/execution.rb +0 -152
  286. data/app/models/railwatch/telemetry/export_delivery.rb +0 -32
  287. data/app/models/railwatch/telemetry/export_destination.rb +0 -66
  288. data/app/models/railwatch/telemetry/health_sample.rb +0 -72
  289. data/app/models/railwatch/telemetry/ingest_batch.rb +0 -31
  290. data/app/models/railwatch/telemetry/llm_call.rb +0 -37
  291. data/app/models/railwatch/telemetry/log.rb +0 -91
  292. data/app/models/railwatch/telemetry/mail.rb +0 -13
  293. data/app/models/railwatch/telemetry/n_plus_one.rb +0 -92
  294. data/app/models/railwatch/telemetry/notification.rb +0 -13
  295. data/app/models/railwatch/telemetry/outgoing_request.rb +0 -13
  296. data/app/models/railwatch/telemetry/person.rb +0 -50
  297. data/app/models/railwatch/telemetry/process.rb +0 -10
  298. data/app/models/railwatch/telemetry/profile.rb +0 -37
  299. data/app/models/railwatch/telemetry/query.rb +0 -57
  300. data/app/models/railwatch/telemetry/query_shape.rb +0 -45
  301. data/app/models/railwatch/telemetry/release_health.rb +0 -69
  302. data/app/models/railwatch/telemetry/rollup.rb +0 -157
  303. data/app/models/railwatch/telemetry/session.rb +0 -24
  304. data/app/models/railwatch/telemetry/span.rb +0 -19
  305. data/app/models/railwatch/telemetry/storage_op.rb +0 -13
  306. data/app/models/railwatch/telemetry/tenant.rb +0 -240
  307. data/app/models/railwatch/telemetry/transaction.rb +0 -13
  308. data/app/models/railwatch/telemetry/view_render.rb +0 -13
  309. data/app/models/railwatch/telemetry/visit.rb +0 -24
  310. data/app/models/railwatch/telemetry_record.rb +0 -107
  311. data/app/models/railwatch/threshold.rb +0 -66
  312. data/app/models/railwatch/user.rb +0 -47
  313. data/app/models/railwatch/viewer.rb +0 -13
  314. data/app/models/railwatch/window.rb +0 -66
  315. data/app/views/layouts/railwatch/dashboard.html.erb +0 -25
  316. data/db/railwatch_migrate/20260916000000_create_railwatch_tables.rb +0 -151
  317. data/db/railwatch_migrate/20260917000000_create_railwatch_maintenance_tasks.rb +0 -18
  318. data/db/railwatch_migrate/20260917120000_create_railwatch_followup_receipts.rb +0 -20
  319. data/db/railwatch_migrate/20260918120000_widen_host_user_ids.rb +0 -71
  320. data/db/railwatch_telemetry_migrate/20260903000000_enable_incremental_vacuum.rb +0 -62
  321. data/db/railwatch_telemetry_migrate/20260903000001_create_telemetry.rb +0 -481
  322. data/db/railwatch_telemetry_migrate/20260903000002_rename_tenant_to_app_tenant.rb +0 -14
  323. data/db/railwatch_telemetry_migrate/20260903000003_add_statement_count_to_transactions.rb +0 -9
  324. data/db/railwatch_telemetry_migrate/20260903000004_add_role_and_channel.rb +0 -10
  325. data/db/railwatch_telemetry_migrate/20260903000005_add_locals_to_exceptions.rb +0 -9
  326. data/db/railwatch_telemetry_migrate/20260903000006_add_spans_health_vitals_and_fts.rb +0 -82
  327. data/db/railwatch_telemetry_migrate/20260903000007_rename_span_attributes_to_payload.rb +0 -10
  328. data/db/railwatch_telemetry_migrate/20260903000008_add_profiles_and_attachments.rb +0 -60
  329. data/db/railwatch_telemetry_migrate/20260903000009_add_truncated_to_attachments.rb +0 -9
  330. data/db/railwatch_telemetry_migrate/20260903000010_create_sessions_and_release_health.rb +0 -51
  331. data/db/railwatch_telemetry_migrate/20260903000011_add_fingerprint_to_exceptions.rb +0 -11
  332. data/db/railwatch_telemetry_migrate/20260904000012_add_failed_to_broadcasts.rb +0 -10
  333. data/db/railwatch_telemetry_migrate/20260904010000_add_filter_cursor_indexes.rb +0 -37
  334. data/db/railwatch_telemetry_migrate/20260904020000_add_n_plus_ones_execution_id_index.rb +0 -11
  335. data/db/railwatch_telemetry_migrate/20260904120000_create_query_shapes.rb +0 -15
  336. data/db/railwatch_telemetry_migrate/20260906120000_add_backpressure_factor_to_ingest_batches.rb +0 -9
  337. data/db/railwatch_telemetry_migrate/20260907000000_rename_lantern_version_on_processes.rb +0 -10
  338. data/db/railwatch_telemetry_migrate/20260913000000_rename_nightrail_version_on_processes.rb +0 -9
  339. data/db/railwatch_telemetry_migrate/20260914000000_drop_orphan_durable_ingest_tables.rb +0 -18
  340. data/db/railwatch_telemetry_migrate/20260914010000_drop_orphan_durable_ingest_columns.rb +0 -25
  341. data/db/railwatch_telemetry_migrate/20260915000000_create_llm_calls.rb +0 -55
  342. data/db/railwatch_telemetry_migrate/20260915120000_add_detail_to_llm_calls.rb +0 -21
  343. data/db/railwatch_telemetry_migrate/20260915200000_add_explained_index_to_queries.rb +0 -16
  344. data/db/railwatch_telemetry_migrate/20260915210000_add_slowest_index_to_queries.rb +0 -18
  345. data/db/railwatch_telemetry_migrate/20260917010000_add_batch_ledger_to_ingest_batches.rb +0 -16
  346. data/db/railwatch_telemetry_migrate/20260919000100_create_export_queue.rb +0 -146
  347. data/db/railwatch_telemetry_migrate/20260922000000_add_covering_indexes_for_dashboard_aggregates.rb +0 -34
  348. data/db/railwatch_telemetry_migrate/20260923000000_add_people_index.rb +0 -15
  349. data/db/railwatch_telemetry_migrate/20260925000000_add_tenant_summary_index.rb +0 -17
  350. data/db/railwatch_telemetry_migrate/20261008000000_add_in_transaction_to_outgoing_requests.rb +0 -10
  351. data/docs/embedded.md +0 -503
  352. data/lib/puma/plugin/railwatch.rb +0 -215
  353. data/lib/railwatch/dashboard_assets.rb +0 -45
  354. data/lib/railwatch/embedded.rb +0 -50
  355. data/lib/railwatch/export/client.rb +0 -124
  356. data/lib/railwatch/export/lease.rb +0 -56
  357. data/lib/railwatch/export/outbox.rb +0 -361
  358. data/lib/railwatch/export/policy.rb +0 -79
  359. data/lib/railwatch/export/sender.rb +0 -130
  360. data/lib/railwatch/ingest_request_body_limit.rb +0 -10
  361. data/lib/railwatch/maintenance.rb +0 -194
  362. data/lib/railwatch/patches/migration_busy_timeout.rb +0 -73
  363. data/lib/railwatch/transport/local.rb +0 -78
  364. data/lib/railwatch/transport/socket.rb +0 -183
  365. data/lib/railwatch/writer.rb +0 -370
  366. data/public/railwatch/assets/CommitMono-Bold-D6h61ieg.woff2 +0 -0
  367. data/public/railwatch/assets/CommitMono-Regular-zr8w7Obm.woff2 +0 -0
  368. data/public/railwatch/assets/Roboto-Black-auA4GeOK.woff2 +0 -0
  369. data/public/railwatch/assets/Roboto-Bold-CJLnO8j1.woff2 +0 -0
  370. data/public/railwatch/assets/Roboto-Medium-Cm2bwKpj.woff2 +0 -0
  371. data/public/railwatch/assets/Roboto-Regular-Chaq1-PV.woff2 +0 -0
  372. data/public/railwatch/assets/app-layout-by7G3sa1.js +0 -41
  373. data/public/railwatch/assets/application-Cc5v_Vk9.css +0 -1
  374. data/public/railwatch/assets/arrow-up-DCZ7emux.js +0 -1
  375. data/public/railwatch/assets/braces-CykT5_5u.js +0 -1
  376. data/public/railwatch/assets/card-C-PPSA0g.js +0 -1
  377. data/public/railwatch/assets/chart-Cov79YC5.js +0 -39
  378. data/public/railwatch/assets/chart-hover-DdK4E-2h.js +0 -1
  379. data/public/railwatch/assets/chart-panel-BPaYRXJB.js +0 -1
  380. data/public/railwatch/assets/code-BYjC4YZ_.js +0 -1
  381. data/public/railwatch/assets/copy-id-LPfoj9cC.js +0 -1
  382. data/public/railwatch/assets/cursor-load-more-DSdhgpGl.js +0 -1
  383. data/public/railwatch/assets/data-table-CQ_YRWQs.js +0 -1
  384. data/public/railwatch/assets/env-layout-CdMWbUkm.js +0 -1
  385. data/public/railwatch/assets/execution-path-7MYt-Qbn.js +0 -1
  386. data/public/railwatch/assets/filter-bar-CrzpQ_vD.js +0 -1
  387. data/public/railwatch/assets/flamegraph-BIFEdTAg.js +0 -2
  388. data/public/railwatch/assets/format-B9SDkrWj.js +0 -1
  389. data/public/railwatch/assets/frames-C5t-x4f0.js +0 -1
  390. data/public/railwatch/assets/index--NQ7us-L.js +0 -1
  391. data/public/railwatch/assets/index-2kgYe_99.js +0 -1
  392. data/public/railwatch/assets/index-4YEPHxtO.js +0 -1
  393. data/public/railwatch/assets/index-AOCC1GDJ.js +0 -1
  394. data/public/railwatch/assets/index-BEo-u0eO.js +0 -1
  395. data/public/railwatch/assets/index-BJ1V3w2N.js +0 -1
  396. data/public/railwatch/assets/index-BPoHsBKb.js +0 -1
  397. data/public/railwatch/assets/index-BZzyGUM3.js +0 -1
  398. data/public/railwatch/assets/index-BgVq7yKL.js +0 -1
  399. data/public/railwatch/assets/index-BqFGEZvW.js +0 -1
  400. data/public/railwatch/assets/index-ByuHJ1CH.js +0 -1
  401. data/public/railwatch/assets/index-C2j50AL2.js +0 -1
  402. data/public/railwatch/assets/index-CLEdTkvW.js +0 -1
  403. data/public/railwatch/assets/index-CWgq8qBu.js +0 -1
  404. data/public/railwatch/assets/index-CXQ7_oVj.js +0 -2
  405. data/public/railwatch/assets/index-CmyYrjS2.js +0 -1
  406. data/public/railwatch/assets/index-Cn9_HjDo.js +0 -1
  407. data/public/railwatch/assets/index-CsMAm9UK.js +0 -1
  408. data/public/railwatch/assets/index-D0Kk_oGY.js +0 -1
  409. data/public/railwatch/assets/index-DC9ZptJh.js +0 -1
  410. data/public/railwatch/assets/index-DNzg7jmV.js +0 -1
  411. data/public/railwatch/assets/index-DQ8uJO5f.js +0 -1
  412. data/public/railwatch/assets/index-Deq14BxB.js +0 -1
  413. data/public/railwatch/assets/index-IaV3Blzp.js +0 -1
  414. data/public/railwatch/assets/index-TYNWcJQs.js +0 -1
  415. data/public/railwatch/assets/index-WonH2fcZ.js +0 -1
  416. data/public/railwatch/assets/index-WxWUaH_d.js +0 -1
  417. data/public/railwatch/assets/index-mUNrl4ky.js +0 -1
  418. data/public/railwatch/assets/inertia-DfNRARuA.js +0 -97
  419. data/public/railwatch/assets/json-viewer-DMsF5glp.js +0 -1
  420. data/public/railwatch/assets/klass-CmHYk5rz.js +0 -1
  421. data/public/railwatch/assets/origin-identity-1R3bqa3R.js +0 -1
  422. data/public/railwatch/assets/percentile-picker-73z5PAps.js +0 -1
  423. data/public/railwatch/assets/relative-time-Dsc_cCdX.js +0 -1
  424. data/public/railwatch/assets/release-health-BV5OaOWd.js +0 -1
  425. data/public/railwatch/assets/route-CWh58C7K.js +0 -1
  426. data/public/railwatch/assets/segmented-DB4zh-Am.js +0 -1
  427. data/public/railwatch/assets/select-DBKjFlOX.js +0 -1
  428. data/public/railwatch/assets/series-chart-BaiDG795.js +0 -1
  429. data/public/railwatch/assets/show-B4ICCFGh.js +0 -1
  430. data/public/railwatch/assets/show-B5lOU9Gv.js +0 -1
  431. data/public/railwatch/assets/show-BYPB74t4.js +0 -2
  432. data/public/railwatch/assets/show-C7A0m2M8.js +0 -1
  433. data/public/railwatch/assets/show-DT5X1zqE.js +0 -1
  434. data/public/railwatch/assets/show-D_NBdXsn.js +0 -2
  435. data/public/railwatch/assets/show-DgPCnRg8.js +0 -1
  436. data/public/railwatch/assets/show-Yl69HF0x.js +0 -1
  437. data/public/railwatch/assets/show-_QxuUQCm.js +0 -3
  438. data/public/railwatch/assets/show-g__jnN36.js +0 -1
  439. data/public/railwatch/assets/show-l4ztfjl9.js +0 -1
  440. data/public/railwatch/assets/sort-header-BcG8ulEH.js +0 -1
  441. data/public/railwatch/assets/source-link-B2183i2-.js +0 -1
  442. data/public/railwatch/assets/sparkline-cell-B47ipscB.js +0 -1
  443. data/public/railwatch/assets/stat-DT2hhsqS.js +0 -1
  444. data/public/railwatch/assets/status-badge-CmbRKlY7.js +0 -1
  445. data/public/railwatch/assets/tenant-path-C4rNGauE.js +0 -1
  446. data/public/railwatch/assets/use-clipboard-oTNeP4ax.js +0 -1
  447. data/public/railwatch/assets/use-live-CLZZj245.js +0 -1
  448. data/public/railwatch/icon.png +0 -0
  449. data/public/railwatch/icon.svg +0 -5
  450. data/public/railwatch/manifest.json +0 -1245
  451. data/public/railwatch/rails-vite.json +0 -1
  452. /data/{app/frontend/lib → lib/generators/railwatch/install/templates}/railwatch.ts +0 -0
@@ -1,9 +0,0 @@
1
- import type { FlashData, SharedProps } from "@/types"
2
-
3
- declare module "@inertiajs/core" {
4
- export interface InertiaConfig {
5
- sharedPageProps: SharedProps
6
- flashDataType: FlashData
7
- errorValueType: string[]
8
- }
9
- }
@@ -1,288 +0,0 @@
1
- import type { LucideIcon } from "lucide-react"
2
-
3
- export interface Auth {
4
- user: User
5
- session: Pick<Session, "id" | "recently_authenticated">
6
- }
7
-
8
- export interface BreadcrumbItem {
9
- title: string
10
- href: string
11
- }
12
-
13
- export interface NavItem {
14
- title: string
15
- href: string
16
- icon?: LucideIcon | null
17
- isActive?: boolean
18
- }
19
-
20
- export interface FlashData {
21
- alert?: string
22
- warning?: string
23
- notice?: string
24
- }
25
-
26
- export interface AccountSummary {
27
- id: number
28
- name: string
29
- slug: string
30
- plan: string
31
- }
32
-
33
- export interface EnvironmentSummary {
34
- id: number
35
- name: string
36
- slug: string
37
- last_seen_at: string | null
38
- paused: boolean
39
- }
40
-
41
- export interface ApplicationSummary {
42
- id: number
43
- name: string
44
- slug: string
45
- issue_prefix: string
46
- environments: EnvironmentSummary[]
47
- }
48
-
49
- export interface EnvironmentContext {
50
- id: number
51
- name: string
52
- slug: string
53
- application_id: number
54
- application_name: string
55
- issue_prefix: string
56
- last_seen_at: string | null
57
- paused: boolean
58
- token_prefix: string
59
- repository_url: string | null
60
- default_branch: string
61
- }
62
-
63
- export type Window = "1h" | "6h" | "24h" | "7d" | "30d" | "custom"
64
-
65
- // Chart bucket width; the server offers the subset that fits the window.
66
- export type Step = "1m" | "5m" | "15m" | "1h" | "6h" | "1d"
67
-
68
- export interface WindowRange {
69
- from: string
70
- to: string
71
- }
72
-
73
- export type TelemetryTimeParams =
74
- | { window: Exclude<Window, "custom">; from?: never; to?: never }
75
- | { window?: never; from: string; to: string }
76
-
77
- export interface CursorMeta {
78
- limit: number
79
- next_cursor: string | null
80
- has_more: boolean
81
- }
82
-
83
- export interface SharedProps {
84
- auth: Auth
85
- account: AccountSummary | null
86
- accounts: { id: number; name: string }[]
87
- applications: ApplicationSummary[]
88
- environment?: EnvironmentContext
89
- telemetry_freshness?: {
90
- received_at: string | null
91
- aggregated_at?: string | null
92
- processing_lag_seconds: number | null
93
- }
94
- window?: Window
95
- range?: WindowRange
96
- step?: Step
97
- steps?: Step[]
98
- flash: FlashData
99
- google_oauth: boolean
100
- embedded?: boolean
101
- saved_views?: SavedView[]
102
- [key: string]: unknown
103
- }
104
-
105
- export interface User {
106
- id: number
107
- name: string
108
- email: string
109
- provider: string | null
110
- avatar?: string
111
- verified: boolean
112
- editor: string
113
- editor_root: string | null
114
- created_at: string
115
- updated_at: string
116
- [key: string]: unknown
117
- }
118
-
119
- export interface Session {
120
- id: string
121
- user_agent: string
122
- ip_address: string
123
- created_at: string
124
- recently_authenticated: boolean
125
- }
126
-
127
- // Telemetry shapes shared by pages
128
-
129
- export type Percentile = "p50" | "p95" | "p99"
130
-
131
- // One chart bucket. Durations are null where nothing ran, so a latency
132
- // line breaks over a quiet bucket instead of dropping to zero.
133
- export interface SeriesPoint {
134
- t: string
135
- count: number
136
- errors: number
137
- client_errors: number
138
- avg: number | null
139
- p50: number | null
140
- p95: number | null
141
- p99: number | null
142
- }
143
-
144
- export interface GroupRow {
145
- group_hash: string
146
- name: string
147
- count: number
148
- errors: number
149
- client_errors: number
150
- avg: number
151
- p50: number
152
- p95: number
153
- p99: number
154
- max: number
155
- sparkline: number[]
156
- }
157
-
158
- export interface Summary {
159
- count: number
160
- errors: number
161
- client_errors: number
162
- avg: number
163
- p50: number
164
- p95: number
165
- p99: number
166
- max: number
167
- }
168
-
169
- export interface SummaryWithDelta {
170
- current: Summary
171
- previous: Summary
172
- }
173
-
174
- // Release health: crash-free rates are null, not 0, when there were no
175
- // sessions (or no users) to divide by.
176
- export interface ReleaseHealthSummary {
177
- sessions: number
178
- users: number
179
- crash_free_sessions: number | null
180
- crash_free_users: number | null
181
- errored: number | null
182
- avg_duration_ms: number | null
183
- }
184
-
185
- export interface SessionStatusPoint {
186
- t: string
187
- sessions: number
188
- ok: number
189
- errored: number
190
- crashed: number
191
- }
192
-
193
- export interface DeployMarker {
194
- deploy: string
195
- ref: string
196
- at: string
197
- }
198
-
199
- export interface IssueRow {
200
- id: number
201
- key: string
202
- title: string
203
- kind: "exception" | "performance"
204
- status: "open" | "resolved" | "ignored" | "merged"
205
- priority: "low" | "normal" | "high" | "urgent"
206
- culprit?: string | null
207
- occurrences: number
208
- affected_users: number
209
- first_seen_at?: string
210
- last_seen_at: string
211
- assignee?: { id: number; name: string } | null
212
- fingerprint_source?: string | null
213
- source?: string | null
214
- application?: { id: number; name: string }
215
- environment?: { id: number; name: string }
216
- }
217
-
218
- export interface ExecutionRow {
219
- execution_id: string
220
- name: string
221
- kind?: string
222
- status?: number | null
223
- outcome?: string | null
224
- duration: number
225
- occurred_at: string
226
- user_ref?: string | null
227
- tenant?: string | null
228
- exception_preview?: string | null
229
- inertia_component?: string | null
230
- queries?: number
231
- deploy?: string | null
232
- }
233
-
234
- export interface Frame {
235
- file: string
236
- line: number
237
- function: string
238
- in_app: boolean
239
- code?: Record<string, string>
240
- }
241
-
242
- export interface ExceptionDetail {
243
- id: number
244
- class_name: string
245
- message: string
246
- handled: boolean
247
- severity?: string
248
- source?: string
249
- file?: string | null
250
- line?: number | null
251
- frames: Frame[]
252
- cause?: { class: string; message: string } | null
253
- locals?: Record<string, string> | null
254
- context?: string | null
255
- occurred_at?: string
256
- execution_id?: string | null
257
- execution_source?: string | null
258
- execution_preview?: string | null
259
- deploy?: string | null
260
- group_hash?: string
261
- }
262
-
263
- export interface TimelineEntry {
264
- type: string
265
- id: number
266
- offset: number
267
- duration: number | null
268
- label: string
269
- stage: string | null
270
- sql?: string | null
271
- source?: string | null
272
- detail?: Record<string, string | number | boolean | null>
273
- }
274
-
275
- // A named filter on one environment page, shared through every env page's
276
- // props. `url` is built server-side from the page's route helper.
277
- export interface SavedView {
278
- id: number
279
- name: string
280
- page: string
281
- query: string | null
282
- window: string | null
283
- params: Record<string, string>
284
- pinned: boolean
285
- shared: boolean
286
- mine: boolean
287
- url: string
288
- }
@@ -1,9 +0,0 @@
1
- /// <reference types="vite/client" />
2
-
3
- interface ImportMetaEnv {
4
- readonly VITE_APP_NAME: string
5
- }
6
-
7
- interface ImportMeta {
8
- readonly env: ImportMetaEnv
9
- }
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # Emits the dashboard's script and stylesheet tags from the manifest Vite
5
- # wrote at gem build time. The gem ships the built bundle under
6
- # public/railwatch, so a host app needs no Node toolchain and no asset
7
- # pipeline integration: the engine serves those files itself and this
8
- # helper does the little that rails_vite's vite_tags would have done.
9
- module AssetsHelper
10
- MANIFEST = Railwatch::Engine.root.join("public/railwatch/manifest.json")
11
- ASSET_PREFIX = "/railwatch/assets"
12
-
13
- def railwatch_asset_tags(nonce: nil)
14
- manifest = railwatch_manifest
15
- tags = []
16
- preloaded = Set.new
17
- %w[app/frontend/entrypoints/application.css app/frontend/entrypoints/inertia.tsx].each do |entry|
18
- info = manifest.fetch(entry)
19
- Array(info["imports"]).each do |key|
20
- file = manifest.fetch(key)["file"]
21
- next unless preloaded.add?(file)
22
- tags << tag.link(rel: "modulepreload", href: "#{ASSET_PREFIX}/#{file}", nonce: nonce)
23
- end
24
- tags << if info["file"].end_with?(".css")
25
- tag.link(rel: "stylesheet", href: "#{ASSET_PREFIX}/#{info['file']}", nonce: nonce)
26
- else
27
- tag.script(src: "#{ASSET_PREFIX}/#{info['file']}", type: "module", nonce: nonce)
28
- end
29
- Array(info["css"]).each { |css| tags << tag.link(rel: "stylesheet", href: "#{ASSET_PREFIX}/#{css}", nonce: nonce) }
30
- end
31
- safe_join(tags, "\n")
32
- end
33
-
34
- def railwatch_font_preloads
35
- safe_join(railwatch_manifest.select { |k, _| k.end_with?(".woff2") && k.include?("Roboto") }.map { |_, v|
36
- tag.link(rel: "preload", href: "#{ASSET_PREFIX}/#{v['file']}", as: "font", type: "font/woff2", crossorigin: "anonymous")
37
- }, "\n")
38
- end
39
-
40
- # The bundle digest doubles as the Inertia asset version: a gem upgrade
41
- # changes it and every open tab does a full reload on its next visit.
42
- def self.digest
43
- @digest ||= Digest::SHA256.file(MANIFEST).hexdigest[0, 16]
44
- end
45
-
46
- private
47
-
48
- def railwatch_manifest
49
- @railwatch_manifest ||= JSON.parse(File.read(MANIFEST))
50
- end
51
- end
52
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class AnomalyScanJob < ApplicationJob
5
- queue_as :default
6
-
7
- def perform
8
- DetectAnomaliesJob.perform_later(Environment.current) if AnomalyRule.where(enabled: true).exists?
9
- end
10
- end
11
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # The engine's jobs run on whatever Active Job adapter the host configured.
5
- class ApplicationJob < ActiveJob::Base
6
- end
7
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # Auto-resolves open issues that have gone quiet for a configured number of
5
- # days (nil = off). Resolved with no deploy attached, so any recurrence is
6
- # treated as a regression rather than a duplicate. Runs daily (recurring.yml).
7
- class AutoResolveIssuesJob < ApplicationJob
8
- queue_as :default
9
-
10
- def perform
11
- days = Railwatch::Embedded::Account.auto_resolve_after_days
12
- return unless days
13
-
14
- Issue.open.where("last_seen_at < ?", days.days.ago).find_each do |issue|
15
- issue.resolve!(deploy: nil)
16
- end
17
- end
18
- end
19
- end
@@ -1,113 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # Flags scheduled tasks that have not run when expected. Uses the schedule
5
- # string the gem sends (Solid Queue's cron / natural language) parsed with
6
- # fugit, which Solid Queue already depends on.
7
- class CheckScheduledTasksJob < ApplicationJob
8
- include DetectionSnapshotting
9
-
10
- queue_as :default
11
-
12
- GRACE = 5.minutes
13
- # A deploy restarts the worker, and Solid Queue skips (does not replay) the
14
- # recurring ticks that fell inside the restart window. A tick missed within
15
- # this long after a deploy is the deploy, not the scheduler.
16
- DEPLOY_GRACE = 15.minutes
17
-
18
- def perform(environment)
19
- require "fugit"
20
- tasks = environment.with_telemetry do
21
- last_runs = Telemetry::Execution.scheduled.where("occurred_at > ?", 30.days.ago).group(:task_key).maximum(:occurred_at)
22
- schedules = Telemetry::Execution.latest_schedules(last_runs.keys)
23
- last_runs.map { |key, last| [ key, last, schedules[key] ] }
24
- end
25
- last_deploy_at = environment.deploys.maximum(:deployed_at)
26
- configured = environment.with_telemetry { Telemetry::HealthSample.recurring_task_keys }
27
- open_missed = environment.issues.open.where("group_hash LIKE 'missed:%'").index_by(&:group_hash)
28
- tasks.each do |key, last_run, schedule|
29
- next if schedule.blank?
30
- # A task taken out of config/recurring.yml stops running on purpose.
31
- # Its history would otherwise read as a miss every window for 30 days
32
- # (reconcile_ingest_batches was live for four minutes and flagged 62
33
- # times). The gem ships the schedule Solid Queue is actually running
34
- # on every health sample; a key absent from the newest manifest is
35
- # removed, not missed. No manifest yet means an older gem: judge from
36
- # history alone, as before.
37
- if configured && !configured.include?(key)
38
- resolve_missed!(open_missed["missed:#{key}"], "Auto-resolved: #{key} is no longer a configured recurring task.")
39
- next
40
- end
41
- cron = Fugit.parse(schedule) or next
42
- expected = cron.next_time(last_run).to_t
43
- if expected > Time.current - GRACE
44
- # The task is back on schedule; a missed-run issue for it heals on its own.
45
- resolve_missed!(open_missed["missed:#{key}"], "Auto-resolved: #{key} ran again on schedule.")
46
- next
47
- end
48
- next if last_deploy_at && expected.between?(last_deploy_at - DEPLOY_GRACE, last_deploy_at + DEPLOY_GRACE)
49
- diagnosis = diagnose(key, expected)
50
- issue, outcome = Issue.record_occurrence!(
51
- environment: environment, group_hash: "missed:#{key}", kind: "performance",
52
- title: "Scheduled task #{key} missed its run (expected #{expected.utc.iso8601})#{diagnosis[:title_suffix]}",
53
- culprit: key, occurred_at: Time.current, deploy: nil, user_ref: nil,
54
- sample: { task_key: key, schedule: schedule, last_run: last_run.iso8601, expected: expected.iso8601 }.merge(diagnosis[:sample]))
55
- next unless outcome == :new || outcome == :regressed
56
-
57
- # Only on a new or regressed issue: the detectors re-run every few minutes
58
- # and capturing a snapshot on every breached window would query telemetry
59
- # for an issue nobody is newly looking at.
60
- latest = environment.with_telemetry { Telemetry::Execution.scheduled.where(task_key: key).recent.first }
61
- issue.update!(sample: issue.sample.merge("detection" => IssueDetectionSnapshot.capture(
62
- environment: environment, kind: "performance", telemetry_type: "scheduled_task",
63
- telemetry_group_hash: latest.group_hash, target: key, metric: "missed", from: expected, to: Time.current,
64
- representative_from: last_run,
65
- rule: { type: "schedule", task_key: key, schedule: schedule,
66
- last_run: last_run.iso8601, expected: expected.iso8601 }
67
- )))
68
- payload = { issue_key: issue.key, title: issue.title, environment: environment.name }
69
- environment.application.alert_rules.where(event: "threshold").find_each do |rule|
70
- next unless rule.matches?(issue: issue, payload: payload)
71
- rule.fire!(event: "threshold", issue: issue, payload: payload)
72
- end
73
- end
74
- end
75
-
76
- private
77
-
78
- # Where the run went, from Solid Queue's own ledger when it is in this
79
- # process. A RecurringExecution row is written by the scheduler the moment
80
- # it enqueues a run, so its presence without a scheduled_task execution
81
- # means "enqueued, never performed" (no worker, or a wedged one); its
82
- # absence means the scheduler itself never fired. The hosted platform, and
83
- # any adapter other than Solid Queue, only ever sees the absence of the
84
- # run and reports it as before.
85
- def diagnose(key, expected)
86
- return NO_DIAGNOSIS unless defined?(::SolidQueue::RecurringExecution)
87
-
88
- enqueued_at = Railwatch.ignore { ::SolidQueue::RecurringExecution.where(task_key: key).where("run_at >= ?", expected - 1.minute).maximum(:run_at) }
89
- workers = Railwatch.ignore { ::SolidQueue::Process.where(kind: "Worker").count }
90
- if enqueued_at
91
- { title_suffix: workers.zero? ? ": enqueued, but no Solid Queue worker is running" : ": enqueued, not yet performed",
92
- sample: { enqueued_at: enqueued_at.iso8601, workers: workers, cause: workers.zero? ? "no_worker" : "backlog" } }
93
- else
94
- { title_suffix: ": the scheduler never enqueued it",
95
- sample: { enqueued_at: nil, workers: workers, cause: "scheduler" } }
96
- end
97
- rescue StandardError
98
- NO_DIAGNOSIS
99
- end
100
-
101
- NO_DIAGNOSIS = { title_suffix: "", sample: {} }.freeze
102
-
103
- # Sentry Crons marked a monitor OK again on the next successful check-in;
104
- # the same here: once the task has run inside its window again, the open
105
- # missed-run issue resolves with an activity row, so it does not sit open
106
- # until someone notices it is stale.
107
- def resolve_missed!(issue, body)
108
- return unless issue
109
- issue.resolve!
110
- issue.activities.create!(kind: "comment", data: { body: body })
111
- end
112
- end
113
- end
@@ -1,171 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # Compares each enabled anomaly rule's current window against the same clock
5
- # window on each of the previous `baseline_days` days and opens an anomaly
6
- # issue when the current value sits above the baseline's mean plus
7
- # `deviation` standard deviations. Runs on a schedule via AnomalyScanJob.
8
- class DetectAnomaliesJob < ApplicationJob
9
- include DetectionSnapshotting
10
-
11
- queue_as :default
12
-
13
- TYPE_FOR = DetectPerformanceIssuesJob::TYPE_FOR
14
- # Fewer baseline days than this and the standard deviation is noise.
15
- MIN_BASELINE_DAYS = AnomalyRule::MIN_BASELINE_DAYS
16
- # Volume metrics on a handful of events swing wildly; ignore them.
17
- MIN_EVENTS = 20
18
- COOL_DOWN = 60.minutes
19
- MAX_GROUPS_PER_RULE = 50
20
- MAX_CURRENT_ROWS_PER_RULE = MAX_GROUPS_PER_RULE * 25
21
- MAX_BASELINE_ROWS_PER_RULE = 50_000
22
-
23
- def perform(environment)
24
- now = Time.current
25
- environment.anomaly_rules.enabled.find_each do |rule|
26
- detect(environment, rule, now).each { |detection| open_issue(environment, rule, now, detection) }
27
- end
28
- end
29
-
30
- private
31
-
32
- def detect(environment, rule, now)
33
- from = now - rule.window_minutes.minutes
34
- type = TYPE_FOR.fetch(rule.target_kind)
35
- environment.with_telemetry do
36
- groups = current_groups(rule, type, from, now)
37
- baseline = baseline_rows(rule, type, groups.keys, from, now)
38
- return [] unless baseline
39
-
40
- groups.filter_map do |group_hash, (name, summary)|
41
- anomaly_for(rule, group_hash, name, summary, baseline.fetch(group_hash, []), from, now)
42
- end
43
- end
44
- end
45
-
46
- # Rollups are hourly, so a window shorter than an hour reads the whole
47
- # enclosing hour bucket (Rollup.between rounds `from` down); the baseline
48
- # windows are shifted by whole days and land on the same hour, so the
49
- # comparison stays like-for-like.
50
- def current_groups(rule, type, from, to)
51
- scope = Telemetry::Rollup.for_type(type).between(from, to)
52
- scope = scope.where(name: rule.target) unless rule.target == "*"
53
- group_hashes = scope.reorder(nil).group(:group_hash).order(Arel.sql("SUM(count) DESC"), :group_hash)
54
- .limit(MAX_GROUPS_PER_RULE + 1).pluck(:group_hash)
55
- if group_hashes.length > MAX_GROUPS_PER_RULE
56
- Rails.logger.warn("anomaly detector evaluated only the #{MAX_GROUPS_PER_RULE} busiest groups rule_id=#{rule.id}")
57
- group_hashes.pop
58
- end
59
- rows = scope.where(group_hash: group_hashes).order(:bucket, :group_hash).limit(MAX_CURRENT_ROWS_PER_RULE + 1).to_a
60
- if rows.length > MAX_CURRENT_ROWS_PER_RULE
61
- Rails.logger.error("anomaly detector skipped rule_id=#{rule.id}: more than #{MAX_CURRENT_ROWS_PER_RULE} current rollups")
62
- return {}
63
- end
64
-
65
- rows.group_by(&:group_hash)
66
- .transform_values { |group_rows| [ group_rows.first.name, Telemetry::Rollup.summarize(group_rows) ] }
67
- end
68
-
69
- def anomaly_for(rule, group_hash, name, summary, baseline_rows, from, to)
70
- return if cooling_down?(rule, group_hash)
71
- current = value_for(rule.metric, summary, rule.window_minutes)
72
- return if current.nil?
73
- return if %w[count error_rate].include?(rule.metric) && summary[:count] < MIN_EVENTS
74
-
75
- baseline = baseline_values(rule, baseline_rows, from, to)
76
- return if baseline.size < MIN_BASELINE_DAYS
77
- mean = baseline.sum / baseline.size
78
- stddev = Math.sqrt(baseline.sum { |v| (v - mean)**2 } / baseline.size)
79
- return unless current > mean + (rule.deviation * stddev) && current > mean * 1.25
80
-
81
- # A perfectly flat baseline has no σ to measure against, so report the
82
- # rule's own threshold rather than an infinite one.
83
- sigmas = stddev.positive? ? (current - mean) / stddev : rule.deviation
84
- { group_hash: group_hash, name: name, current: current.round(2), mean: mean.round(2),
85
- stddev: stddev.round(2), sigmas: sigmas.round(2) }
86
- end
87
-
88
- # The same clock window on each of the previous `baseline_days` days. Days
89
- # with no rollups at all are left out rather than counted as zero.
90
- def baseline_values(rule, rows, from, to)
91
- (1..rule.baseline_days).filter_map do |days|
92
- day_rows = rows.select { |row| row.bucket.between?((from - days.days).beginning_of_hour, to - days.days) }
93
- next if day_rows.empty?
94
- value_for(rule.metric, Telemetry::Rollup.summarize(day_rows), rule.window_minutes)
95
- end
96
- end
97
-
98
- # One query for every group's baseline, reading only the hour buckets the
99
- # baseline windows actually cover rather than the whole `baseline_days` span.
100
- def baseline_rows(rule, type, group_hashes, from, to)
101
- return {} if group_hashes.empty?
102
-
103
- rows = Telemetry::Rollup.for_type(type)
104
- .where(group_hash: group_hashes, bucket: baseline_buckets(rule, from, to))
105
- .order(:bucket, :group_hash).limit(MAX_BASELINE_ROWS_PER_RULE + 1).to_a
106
- if rows.length > MAX_BASELINE_ROWS_PER_RULE
107
- Rails.logger.error("anomaly detector skipped rule_id=#{rule.id}: more than #{MAX_BASELINE_ROWS_PER_RULE} baseline rollups")
108
- return nil
109
- end
110
- rows.group_by(&:group_hash)
111
- end
112
-
113
- # The hour buckets covered by the same clock window on each of the previous
114
- # `baseline_days` days.
115
- def baseline_buckets(rule, from, to)
116
- (1..rule.baseline_days).flat_map do |days|
117
- bucket = (from - days.days).beginning_of_hour
118
- last = to - days.days
119
- buckets = []
120
- while bucket <= last
121
- buckets << bucket
122
- bucket += 1.hour
123
- end
124
- buckets
125
- end
126
- end
127
-
128
- def value_for(metric, summary, window_minutes)
129
- case metric
130
- when "p95" then summary[:p95] / 1000.0
131
- when "avg" then summary[:avg] / 1000.0
132
- when "count" then summary[:count] / window_minutes.to_f
133
- when "error_rate" then summary[:count].zero? ? nil : (summary[:errors] * 100.0 / summary[:count])
134
- end
135
- end
136
-
137
- # Reads the primary database: one open anomaly issue per (rule, group) is
138
- # enough for an hour, however often the scan runs.
139
- def cooling_down?(rule, group_hash)
140
- Issue.where(environment_id: rule.environment_id, group_hash: issue_group_hash(rule, group_hash))
141
- .where(last_seen_at: COOL_DOWN.ago..).exists?
142
- end
143
-
144
- def issue_group_hash(rule, group_hash)
145
- "anomaly:#{rule.id}:#{group_hash}"
146
- end
147
-
148
- def open_issue(environment, rule, now, detection)
149
- issue, outcome = Issue.record_occurrence!(
150
- environment: environment, group_hash: issue_group_hash(rule, detection[:group_hash]), kind: "anomaly",
151
- title: "#{rule.metric} of #{detection[:name]} is #{detection[:sigmas].round(1)}σ above its #{rule.baseline_days}-day baseline (#{detection[:current]} vs #{detection[:mean]})",
152
- culprit: detection[:name], occurred_at: now, deploy: nil, user_ref: nil,
153
- sample: { rule_id: rule.id, metric: rule.metric, current: detection[:current], mean: detection[:mean],
154
- stddev: detection[:stddev], sigmas: detection[:sigmas], window_minutes: rule.window_minutes })
155
- carry_detection(issue, outcome) do
156
- IssueDetectionSnapshot.capture(
157
- environment: environment, kind: "anomaly", telemetry_type: TYPE_FOR.fetch(rule.target_kind),
158
- telemetry_group_hash: detection[:group_hash], target: detection[:name], metric: rule.metric,
159
- from: now - rule.window_minutes.minutes, to: now, value: detection[:current],
160
- baseline: detection.slice(:mean, :stddev, :sigmas).merge(days: rule.baseline_days, deviation: rule.deviation),
161
- rule: { type: "anomaly", id: rule.id, target_kind: rule.target_kind, target: rule.target }
162
- )
163
- end
164
- if outcome == :new || outcome == :regressed
165
- issue.fire_alerts!("anomaly", rule_id: rule.id, metric: rule.metric, current: detection[:current],
166
- mean: detection[:mean], sigmas: detection[:sigmas])
167
- end
168
- rule.update_columns(last_fired_at: now)
169
- end
170
- end
171
- end