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,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- module Telemetry
5
- class Transaction < TelemetryRecord
6
- include Child
7
-
8
- def timeline_label
9
- outcome.to_s.first(120)
10
- end
11
- end
12
- end
13
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- module Telemetry
5
- class ViewRender < TelemetryRecord
6
- include Child
7
-
8
- def timeline_label
9
- identifier.to_s.first(120)
10
- end
11
- end
12
- end
13
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- module Telemetry
5
- class Visit < TelemetryRecord
6
- include Child
7
-
8
- # Core Web Vitals reported by the browser client, with Google's
9
- # [good, poor] thresholds: at or below good is "good", above poor is
10
- # "poor". lcp/inp/ttfb are milliseconds, cls is unitless.
11
- VITALS = { lcp: [ 2500, 4000 ], cls: [ 0.1, 0.25 ], inp: [ 200, 500 ], ttfb: [ 800, 1800 ] }.freeze
12
-
13
- def self.rating(metric, value)
14
- good, poor = VITALS.fetch(metric)
15
- return "good" if value <= good
16
- (value > poor) ? "poor" : "needs-improvement"
17
- end
18
-
19
- def timeline_label
20
- component.to_s.first(120)
21
- end
22
- end
23
- end
24
- end
@@ -1,107 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # Base class for everything the engine stores about the host application.
5
- # An embedded install monitors exactly one application, so this is a plain
6
- # second database (`railwatch_telemetry` in the host's database.yml),
7
- # separate from the host's own primary and from the engine's meta tables.
8
- # The hosted platform keeps one SQLite file per monitored environment
9
- # instead: it calls `Railwatch::TelemetryRecord.tenanted("telemetry")`
10
- # (activerecord-tenanted) from an initializer, which replaces the
11
- # connection below with a per-tenant one, and every telemetry query runs
12
- # inside Environment#with_telemetry so the code path is the same in both.
13
- class TelemetryRecord < ActiveRecord::Base
14
- self.abstract_class = true
15
- begin
16
- connects_to database: { writing: :railwatch_telemetry, reading: :railwatch_telemetry }
17
- rescue ActiveRecord::AdapterNotSpecified, LoadError
18
- # No `railwatch_telemetry` entry in this environment's database.yml, or an entry
19
- # whose adapter gem is not in the bundle yet (LoadError). A cloud-transport
20
- # app has none and still eager-loads this class in production, and so
21
- # does the installer's own boot, before it has written the
22
- # entry -- so loading must not raise. Using it must, though: without
23
- # connects_to this class would inherit ActiveRecord::Base's PRIMARY
24
- # connection, and its tables are unprefixed, so a query would read and
25
- # a write would corrupt the host application's own tables. Every route
26
- # into the connection goes through connection_pool, so refusing here
27
- # fails closed for reads and writes alike.
28
- def self.connection_pool
29
- raise Railwatch::DatabaseNotConfigured,
30
- "the `railwatch_telemetry` database (everything the app reports) is not configured for the " \
31
- "#{Rails.env} environment; run `bin/rails generate railwatch:install` " \
32
- "or add it to config/database.yml (docs/embedded.md)"
33
- end
34
- end
35
-
36
- # SQLite's auto_vacuum modes. Deleted pages only leave the file in
37
- # :incremental, and only when PRAGMA incremental_vacuum asks for them;
38
- # :none (the default, and what every database created before
39
- # EnableIncrementalVacuum is in) keeps them on the freelist forever.
40
- AUTO_VACUUM_MODES = { 0 => :none, 1 => :full, 2 => :incremental }.freeze
41
-
42
- # Everything below reads and writes THIS database, never the host's. The
43
- # pragmas are per-database and there is no Active Record wrapper for them,
44
- # so they go through this class's own connection on purpose: through
45
- # ActiveRecord::Base they would report on, and vacuum, the application's
46
- # primary database instead.
47
- def self.sqlite? = connection.adapter_name.match?(/sqlite/i)
48
-
49
- def self.auto_vacuum_mode = sqlite? ? AUTO_VACUUM_MODES.fetch(connection.select_value("PRAGMA auto_vacuum").to_i, :unknown) : nil
50
-
51
- def self.freelist_pages = sqlite? ? connection.select_value("PRAGMA freelist_count").to_i : 0
52
-
53
- def self.page_size = sqlite? ? connection.select_value("PRAGMA page_size").to_i : 0
54
-
55
- # Hands freelist pages back to the filesystem, a slice at a time. Each
56
- # PRAGMA is its own implicit transaction, so the write lock is taken and
57
- # released once per slice rather than held for the whole reclaim -- the
58
- # same reason PruneTelemetryJob deletes in bounded batches instead of one
59
- # statement. Returns the pages actually reclaimed, which is 0 on a
60
- # database that is not in incremental mode: there the pragma is accepted
61
- # and does nothing, and calling it would look like work that happened.
62
- def self.reclaim_freelist!(slice:, slices:)
63
- return 0 unless sqlite? && auto_vacuum_mode == :incremental
64
-
65
- before = freelist_pages
66
- slices.times do
67
- break if freelist_pages.zero?
68
-
69
- incremental_vacuum(slice.to_i)
70
- end
71
- before - freelist_pages
72
- end
73
-
74
- # Through the raw connection on purpose. incremental_vacuum frees one page
75
- # per sqlite3_step, and Active Record's execute steps a statement with no
76
- # result columns exactly once -- so through it this pragma frees a single
77
- # page whatever count it is handed, which is the kind of no-op that reads
78
- # as work done. The sqlite3 gem's own execute steps to completion. Same
79
- # raw_connection route Ingest::Writer already takes for its inserts.
80
- def self.incremental_vacuum(pages = nil)
81
- connection.raw_connection.execute("PRAGMA incremental_vacuum#{"(#{pages})" if pages}")
82
- end
83
-
84
- # Records arrive as the gem's wire hashes; this is the shared envelope.
85
- def self.envelope_columns(t)
86
- t.datetime :occurred_at, null: false, precision: 6
87
- t.string :deploy, limit: 128
88
- t.string :server, limit: 255
89
- t.string :group_hash, limit: 32
90
- t.string :trace_id, limit: 36
91
- t.string :execution_source, limit: 20
92
- t.string :execution_id, limit: 36
93
- t.string :execution_preview, limit: 255
94
- t.string :execution_stage, limit: 32
95
- t.string :user_ref, limit: 255
96
- t.string :app_tenant, limit: 255
97
- end
98
-
99
- # Platform parity: callers wrap telemetry work in with_tenant. There is one
100
- # tenant here, so it is just the block.
101
- def self.with_tenant(_slug)
102
- yield
103
- end
104
-
105
- def self.tenant_exist?(_slug) = true
106
- end
107
- end
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # A performance rule: "requests to * must keep p95 under 2000 ms over 5
5
- # minutes". Evaluated by DetectPerformanceIssuesJob against rollups.
6
- class Threshold < ApplicationRecord
7
- self.table_name = "railwatch_thresholds"
8
- MAX_LIMIT = 1_000_000_000
9
- MAX_WINDOW_MINUTES = 1_440
10
- TARGET_KINDS = %w[requests jobs commands queries scheduled_tasks outgoing_requests
11
- llm_calls llm_tools].freeze
12
- METRICS = %w[p95 max avg error_rate failure_rate missed spend tokens truncation_rate].freeze
13
-
14
- # Duration and rate metrics read the same on any timed record, so they are
15
- # allowed everywhere. Money and tokens only mean something where a record
16
- # carries them: a spend rule on tool calls could never fire, and a rule
17
- # that cannot fire is worse than no rule -- it reads as coverage.
18
- SHARED_METRICS = %w[p95 max avg error_rate failure_rate missed].freeze
19
- LLM_METRICS = %w[spend tokens truncation_rate].freeze
20
-
21
- # Money leads with its symbol; milliseconds and percent follow the number.
22
- # The old code assumed two units and branched on the metric name ending in
23
- # "rate", which cannot express a dollar sign in front.
24
- UNITS = { "spend" => "$", "tokens" => "", "truncation_rate" => "%",
25
- "error_rate" => "%", "failure_rate" => "%" }.freeze
26
-
27
- def environment = Environment.current
28
-
29
- validates :target_kind, inclusion: { in: TARGET_KINDS }
30
- validates :metric, inclusion: { in: METRICS }
31
- validates :limit, numericality: { greater_than: 0, less_than_or_equal_to: MAX_LIMIT }
32
- validates :window_minutes, numericality: { only_integer: true, greater_than: 0,
33
- less_than_or_equal_to: MAX_WINDOW_MINUTES }
34
- validates :target, presence: true, length: { maximum: 256 }
35
- validates :limit, numericality: { less_than_or_equal_to: 100 }, if: -> { metric&.end_with?("rate") }
36
- validates :target, uniqueness: { scope: %i[environment_id target_kind metric] }
37
- validate :metric_applies_to_target_kind
38
-
39
- def self.metrics_for(target_kind)
40
- target_kind == "llm_calls" ? SHARED_METRICS + LLM_METRICS : SHARED_METRICS
41
- end
42
-
43
- def unit = UNITS.fetch(metric, "ms")
44
-
45
- # "$4.50", "2000ms", "5%", "150000" -- the symbol's position is part of
46
- # the unit, not something a caller should have to know.
47
- def format_value(value)
48
- return "$#{value.is_a?(Float) ? format('%.2f', value) : value}" if unit == "$"
49
-
50
- "#{value}#{unit}"
51
- end
52
-
53
- def description
54
- "#{target_kind} #{target == '*' ? 'all' : target}: #{metric} over #{format_value(limit)} in #{window_minutes}m"
55
- end
56
-
57
- private
58
-
59
- def metric_applies_to_target_kind
60
- return if metric.blank? || target_kind.blank?
61
- return if Threshold.metrics_for(target_kind).include?(metric)
62
-
63
- errors.add(:metric, "is not available for #{target_kind}")
64
- end
65
- end
66
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # Who is looking at the dashboard. The platform has accounts and memberships;
5
- # an embedded install sits behind the host's own authentication, so this is
6
- # whatever the host says it is (Railwatch.config.dashboard_user), defaulting
7
- # to a single anonymous operator. Comments, saved views and issue activity
8
- # record this id.
9
- class User
10
- ID = 1
11
-
12
- attr_reader :id, :name, :email
13
-
14
- def self.current = Viewer.user
15
- def self.find(id) = find_by(id: id) || raise(ActiveRecord::RecordNotFound, "User #{id}")
16
-
17
- # A host's dashboard_user resolver hands back its own ids, and those ids
18
- # are what comments, saved views and issue activity store. Resolving only
19
- # ID meant a comment written by user 7 had no author, and assigning an
20
- # issue to them raised RecordNotFound. There is no user table to look
21
- # anyone up in, so: the viewer when it is them, the anonymous operator
22
- # for the default id, and a bare identity carrying the stored id
23
- # otherwise, which is everything that is actually known about them.
24
- def self.find_by(id:)
25
- return nil if id.nil? || id.to_s.empty?
26
- viewer = Viewer.user
27
- return viewer if viewer && viewer.id.to_s == id.to_s
28
- return default if id.to_i == ID
29
-
30
- new(id: id, name: "Operator", email: nil)
31
- end
32
-
33
- def self.default = @default ||= new(id: ID, name: "Operator", email: nil)
34
-
35
- def initialize(id:, name:, email: nil)
36
- @id = id
37
- @name = name
38
- @email = email
39
- end
40
-
41
- def as_json(*) = { id: id, name: name, email: email, provider: nil, verified: true, editor: "vscode", editor_root: nil }
42
- def accounts = [ Railwatch::Embedded::Account ]
43
- def ==(other) = other.is_a?(User) && other.id == id
44
- alias eql? ==
45
- def hash = id.hash
46
- end
47
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # Who is looking at the dashboard, request-scoped. The platform's code
5
- # reads Current.user; the gem's own Railwatch::Current is the running
6
- # execution, so this has a different name. Set from the host's
7
- # dashboard_user resolver on every dashboard request.
8
- class Viewer < ActiveSupport::CurrentAttributes
9
- attribute :user, :environment, :account
10
-
11
- def account = Railwatch::Embedded::Account
12
- end
13
- end
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- # The time range a telemetry page, API call, or MCP tool looks at: one of the
5
- # fixed presets ("1h" .. "30d") ending now, or a custom [from, to] pair. The
6
- # dashboard, the JSON API, and MCP all resolve their ?window= / "window"
7
- # argument through here so the presets and the default agree everywhere.
8
- class Window
9
- PRESETS = { "1h" => 1.hour, "6h" => 6.hours, "24h" => 24.hours, "7d" => 7.days, "30d" => 30.days }.freeze
10
- DEFAULT = "24h"
11
- MAX_CUSTOM_RANGE = 90.days
12
-
13
- attr_reader :key, :from, :to
14
-
15
- # A preset key, or anything else for the default.
16
- def self.preset(key)
17
- key = PRESETS.key?(key.to_s) ? key.to_s : DEFAULT
18
- to = Time.current
19
- new(key, to - PRESETS[key], to)
20
- end
21
-
22
- # A custom range from ?from=&to= when both parse, are in order, and span
23
- # no more than MAX_CUSTOM_RANGE; otherwise the preset (or default).
24
- def self.parse(window: nil, from: nil, to: nil)
25
- custom(from, to) || preset(window)
26
- end
27
-
28
- def self.custom(from, to)
29
- from = parse_time(from) or return
30
- to = to.blank? ? Time.current : parse_time(to) or return
31
- new("custom", from, to) if to > from && (to - from) <= MAX_CUSTOM_RANGE
32
- end
33
-
34
- def self.parse_time(value)
35
- return if value.blank?
36
- Time.zone.parse(value.to_s)
37
- rescue ArgumentError, TypeError
38
- nil
39
- end
40
- private_class_method :parse_time
41
-
42
- def initialize(key, from, to)
43
- @key = key
44
- @from = from
45
- @to = to
46
- end
47
-
48
- def custom? = key == "custom"
49
- def range = [ from, to ]
50
- def span = to - from
51
-
52
- # The same length immediately before this one, for "vs. previous period".
53
- def previous
54
- self.class.new(key, from - span, from)
55
- end
56
-
57
- def to_h
58
- { from: from.iso8601(6), to: to.iso8601(6) }
59
- end
60
-
61
- # Query params that reproduce this window on another page.
62
- def to_params
63
- custom? ? to_h : { window: key }
64
- end
65
- end
66
- end
@@ -1,25 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title inertia><%= content_for(:title) || "Railwatch" %></title>
5
- <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <meta name="application-name" content="Railwatch">
7
- <meta name="railwatch-mount" content="<%= request.script_name %>">
8
- <link rel="icon" href="/railwatch/icon.svg" type="image/svg+xml">
9
- <link rel="apple-touch-icon" href="/railwatch/icon.png">
10
- <%= csrf_meta_tags %>
11
- <%= csp_meta_tag %>
12
- <script nonce="<%= content_security_policy_nonce %>">
13
- document.documentElement.classList.toggle(
14
- "dark",
15
- localStorage.appearance === "dark" ||
16
- (!("appearance" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches),
17
- );
18
- </script>
19
- <%= railwatch_font_preloads %>
20
- <%= railwatch_asset_tags(nonce: content_security_policy_nonce) %>
21
- </head>
22
- <body>
23
- <%= yield %>
24
- </body>
25
- </html>
@@ -1,151 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # The engine's own record of the monitored application: issues and what
4
- # people did about them, saved views, thresholds, anomaly and alert rules,
5
- # deploy markers. Authored and permanent where telemetry is derived and
6
- # pruned, so it lives in the host's `railwatch` database, never its primary.
7
- #
8
- # Nothing here references the host's tables. `viewer_id` on comments,
9
- # activity and saved views is whatever the host's dashboard_user resolver
10
- # returned as `id`, stored as an integer and never joined; `assignee_id`
11
- # on issues is the same. application_id and environment_id are always 1 in
12
- # an embedded install and exist so the dashboard's URLs and the hosted
13
- # platform's code stay identical.
14
- class CreateRailwatchTables < ActiveRecord::Migration[8.1]
15
- def change
16
- create_table :railwatch_issues do |t|
17
- t.integer :application_id, null: false
18
- t.integer :environment_id, null: false
19
- t.integer :number, null: false
20
- t.string :group_hash, limit: 32, null: false
21
- t.string :kind, default: "exception", null: false
22
- t.string :title, null: false
23
- t.string :culprit
24
- t.string :source, limit: 128
25
- t.string :status, default: "open", null: false
26
- t.string :priority, default: "normal", null: false
27
- t.integer :assignee_id
28
- t.integer :merged_into_id
29
- t.bigint :occurrences, default: 0, null: false
30
- t.integer :affected_users, default: 0, null: false
31
- t.datetime :first_seen_at, null: false
32
- t.datetime :last_seen_at, null: false
33
- t.datetime :resolved_at
34
- t.string :resolved_in_deploy, limit: 128
35
- t.datetime :regressed_at
36
- t.json :sample, default: {}, null: false
37
- t.timestamps
38
- end
39
- add_index :railwatch_issues, [ :application_id, :number ], unique: true
40
- add_index :railwatch_issues, [ :environment_id, :group_hash ], unique: true
41
- add_index :railwatch_issues, [ :environment_id, :status, :last_seen_at ]
42
- add_index :railwatch_issues, :merged_into_id
43
-
44
- create_table :railwatch_comments do |t|
45
- t.integer :issue_id, null: false
46
- t.integer :viewer_id
47
- t.string :author_name
48
- t.text :body, null: false
49
- t.string :source, default: "railwatch", null: false
50
- t.string :external_id
51
- t.datetime :external_updated_at
52
- t.timestamps
53
- end
54
- add_index :railwatch_comments, :issue_id
55
-
56
- create_table :railwatch_issue_activities do |t|
57
- t.integer :issue_id, null: false
58
- t.integer :viewer_id
59
- t.string :kind, null: false
60
- t.json :data, default: {}, null: false
61
- t.datetime :created_at, null: false
62
- end
63
- add_index :railwatch_issue_activities, [ :issue_id, :created_at ]
64
-
65
- create_table :railwatch_deploys do |t|
66
- t.integer :environment_id, null: false
67
- t.string :deploy, limit: 128, null: false
68
- t.string :ref, limit: 128
69
- t.string :previous_ref, limit: 128
70
- t.string :name
71
- t.string :url
72
- t.string :server
73
- t.datetime :deployed_at, null: false
74
- t.json :commits, default: [], null: false
75
- t.json :detail, default: {}, null: false
76
- t.timestamps
77
- end
78
- add_index :railwatch_deploys, [ :environment_id, :deploy ], unique: true
79
- add_index :railwatch_deploys, [ :environment_id, :deployed_at ]
80
-
81
- create_table :railwatch_saved_views do |t|
82
- t.integer :environment_id, null: false
83
- t.integer :viewer_id, null: false
84
- t.string :name, null: false
85
- t.string :page, null: false
86
- t.string :query
87
- t.string :window, limit: 8
88
- t.json :params, default: {}, null: false
89
- t.boolean :pinned, default: true, null: false
90
- t.boolean :shared, default: true, null: false
91
- t.timestamps
92
- end
93
- add_index :railwatch_saved_views, [ :environment_id, :page ]
94
-
95
- create_table :railwatch_thresholds do |t|
96
- t.integer :environment_id, null: false
97
- t.string :target_kind, null: false
98
- t.string :target, default: "*", null: false
99
- t.string :metric, default: "p95", null: false
100
- t.float :limit, null: false
101
- t.integer :window_minutes, default: 5, null: false
102
- t.timestamps
103
- end
104
- add_index :railwatch_thresholds, [ :environment_id, :target_kind, :target, :metric ], unique: true, name: "index_railwatch_thresholds_unique"
105
-
106
- create_table :railwatch_anomaly_rules do |t|
107
- t.integer :environment_id, null: false
108
- t.string :target_kind, null: false
109
- t.string :target, default: "*", null: false
110
- t.string :metric, null: false
111
- t.float :deviation, default: 3.0, null: false
112
- t.integer :baseline_days, default: 7, null: false
113
- t.integer :window_minutes, default: 15, null: false
114
- t.boolean :enabled, default: true, null: false
115
- t.datetime :last_fired_at
116
- t.timestamps
117
- end
118
- add_index :railwatch_anomaly_rules, :environment_id
119
-
120
- create_table :railwatch_alert_rules do |t|
121
- t.integer :application_id, null: false
122
- t.string :event, null: false
123
- t.json :filters, default: {}, null: false
124
- t.timestamps
125
- end
126
- add_index :railwatch_alert_rules, :application_id
127
-
128
- create_table :railwatch_alerts do |t|
129
- t.integer :alert_rule_id
130
- t.integer :issue_id
131
- t.integer :summary_alert_id
132
- t.string :event, null: false
133
- t.string :status, default: "pending", null: false
134
- t.json :payload, default: {}, null: false
135
- t.text :error
136
- t.datetime :sent_at
137
- t.string :burst_key
138
- t.string :delivery_key
139
- t.integer :delivery_attempts, default: 0, null: false
140
- t.datetime :next_delivery_at
141
- t.datetime :delivery_enqueued_until
142
- t.string :delivery_lease_id
143
- t.datetime :delivery_lease_expires_at
144
- t.timestamps
145
- end
146
- add_index :railwatch_alerts, :burst_key, unique: true
147
- add_index :railwatch_alerts, :delivery_key, unique: true
148
- add_index :railwatch_alerts, [ :issue_id, :event, :created_at ]
149
- add_index :railwatch_alerts, [ :status, :created_at, :alert_rule_id ], name: "index_railwatch_alerts_for_burst_reconciliation"
150
- end
151
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # The embedded install's maintenance lease table: one row per task in
4
- # Railwatch::Maintenance::TASKS, recording when it last ran and which process
5
- # holds it now. Lives in the railwatch database with the other permanent,
6
- # operator-facing state rather than in the pruned telemetry file.
7
- class CreateRailwatchMaintenanceTasks < ActiveRecord::Migration[8.1]
8
- def change
9
- create_table :railwatch_maintenance_tasks do |t|
10
- t.string :name, null: false
11
- t.datetime :last_run_at
12
- t.string :lease_owner
13
- t.datetime :lease_expires_at
14
- t.timestamps
15
- end
16
- add_index :railwatch_maintenance_tasks, :name, unique: true
17
- end
18
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # What makes replaying a batch's follow-ups safe. Exception grouping bumps an
4
- # issue's occurrence count in this database, but the "done" mark lives on the
5
- # batch's ledger row in the OTHER database, so a crash between the two (or
6
- # the inline drain racing the maintenance drain) would count the same
7
- # exceptions twice. A receipt per (batch, group) is inserted in the same
8
- # transaction as the issue update; the unique index turns a second attempt
9
- # into a no-op.
10
- class CreateRailwatchFollowupReceipts < ActiveRecord::Migration[8.1]
11
- def change
12
- create_table :railwatch_followup_receipts do |t|
13
- t.string :batch_id, limit: 36, null: false
14
- t.string :group_hash, limit: 32, null: false
15
- t.datetime :created_at, null: false
16
- end
17
- add_index :railwatch_followup_receipts, [ :batch_id, :group_hash ], unique: true
18
- add_index :railwatch_followup_receipts, :created_at
19
- end
20
- end
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # The ids on comments, issue activity, saved views and issue assignment are
4
- # whatever the host's `dashboard_user` resolver returned. They were integer
5
- # columns, which quietly assumed every application numbers its users. An app
6
- # with UUID primary keys could not store a comment at all, and one with ids
7
- # past the signed range could not either.
8
- #
9
- # Nothing joins on these columns and nothing indexes them; they are an opaque
10
- # handle the host gave us and hands back. A string says that, and costs
11
- # nothing in a database that is a few thousand rows of authored content.
12
- #
13
- # Existing values convert cleanly: SQLite rewrites 7 as "7", and the models
14
- # compare with to_s on both sides, so a row written before this migration
15
- # still matches the same viewer after it.
16
- class WidenHostUserIds < ActiveRecord::Migration[8.1]
17
- COLUMNS = {
18
- railwatch_issues: :assignee_id,
19
- railwatch_comments: :viewer_id,
20
- railwatch_issue_activities: :viewer_id,
21
- railwatch_saved_views: :viewer_id
22
- }.freeze
23
-
24
- # SQLite rebuilds a table to change a column type, and a rebuild inside a
25
- # transaction ignores `PRAGMA foreign_keys = OFF`. This database declares no
26
- # foreign keys, so there is nothing to cascade, but the rebuild is kept out
27
- # of the migrator's transaction anyway: it is the cheap half of a habit
28
- # whose expensive half is deleted rows.
29
- disable_ddl_transaction!
30
-
31
- def up
32
- COLUMNS.each do |table, column|
33
- null = column == :viewer_id && table == :railwatch_saved_views ? false : true
34
- change_column table, column, :string, limit: 255, null: null
35
- end
36
- end
37
-
38
- # Reversible only while every stored id still looks like a number. Once a
39
- # host with UUIDs (or emails, or anything else) has written one, there is
40
- # no integer to go back to: the column would either refuse the value or
41
- # quietly coerce it to something that is no longer that person. Say so and
42
- # stop, rather than losing the identity on the way down. These tables are
43
- # authored content, a few thousand rows at most, so reading them is cheap.
44
- INTEGERISH = /\A-?\d+\z/
45
- MAX_REPORTED = 3
46
-
47
- def down
48
- blocking = COLUMNS.filter_map do |table, column|
49
- next unless connection.table_exists?(table)
50
-
51
- values = connection.select_values(
52
- "SELECT DISTINCT #{connection.quote_column_name(column)} FROM #{connection.quote_table_name(table)} " \
53
- "WHERE #{connection.quote_column_name(column)} IS NOT NULL"
54
- )
55
- offenders = values.reject { |value| INTEGERISH.match?(value.to_s) }
56
- "#{table}.#{column} (#{offenders.first(MAX_REPORTED).join(', ')}#{"..." if offenders.size > MAX_REPORTED})" if offenders.any?
57
- end
58
-
59
- if blocking.any?
60
- raise ActiveRecord::IrreversibleMigration,
61
- "cannot narrow host user ids back to integers: #{blocking.join('; ')}. " \
62
- "Those ids came from this application's own dashboard_user resolver and have no integer form; " \
63
- "rolling back would discard them."
64
- end
65
-
66
- COLUMNS.each do |table, column|
67
- null = column == :viewer_id && table == :railwatch_saved_views ? false : true
68
- change_column table, column, :integer, null: null
69
- end
70
- end
71
- end