railwatch 0.9.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (454) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +37 -33
  3. data/CHANGELOG.md +66 -0
  4. data/README.md +51 -60
  5. data/config/routes.rb +0 -59
  6. data/docs/ai-and-mcp.md +9 -8
  7. data/docs/configuration.md +57 -71
  8. data/docs/faq.md +17 -25
  9. data/docs/getting-started.md +105 -117
  10. data/docs/replacing-nightwatch.md +8 -7
  11. data/docs/replacing-sentry.md +12 -16
  12. data/docs/security.md +14 -20
  13. data/docs/self-hosting.md +24 -22
  14. data/docs/testing.md +6 -14
  15. data/docs/troubleshooting.md +12 -46
  16. data/lib/generators/railwatch/install/install_generator.rb +12 -240
  17. data/lib/generators/railwatch/install/templates/initializer.rb.tt +10 -40
  18. data/lib/generators/railwatch/install/templates/post-deploy +0 -7
  19. data/lib/railwatch/authentication.rb +6 -5
  20. data/lib/railwatch/buffer.rb +9 -0
  21. data/lib/railwatch/configuration.rb +21 -151
  22. data/lib/railwatch/engine.rb +9 -118
  23. data/lib/railwatch/patches.rb +0 -11
  24. data/lib/railwatch/reporter.rb +92 -21
  25. data/lib/railwatch/sampler.rb +10 -2
  26. data/lib/railwatch/subscribers/process_info.rb +1 -2
  27. data/lib/railwatch/version.rb +1 -1
  28. data/lib/railwatch.rb +8 -64
  29. data/lib/tasks/railwatch_tasks.rake +3 -325
  30. data/llms.txt +23 -25
  31. metadata +3 -453
  32. data/app/channels/railwatch/environment_channel.rb +0 -62
  33. data/app/controllers/concerns/railwatch/telemetry_identity.rb +0 -25
  34. data/app/controllers/railwatch/alerts_controller.rb +0 -48
  35. data/app/controllers/railwatch/anomaly_rules_controller.rb +0 -31
  36. data/app/controllers/railwatch/attachments_controller.rb +0 -22
  37. data/app/controllers/railwatch/broadcasts_controller.rb +0 -15
  38. data/app/controllers/railwatch/cache_events_controller.rb +0 -30
  39. data/app/controllers/railwatch/commands_controller.rb +0 -16
  40. data/app/controllers/railwatch/comments_controller.rb +0 -11
  41. data/app/controllers/railwatch/dashboard_controller.rb +0 -70
  42. data/app/controllers/railwatch/deploys_controller.rb +0 -34
  43. data/app/controllers/railwatch/deprecations_controller.rb +0 -54
  44. data/app/controllers/railwatch/environment_scoped.rb +0 -81
  45. data/app/controllers/railwatch/exceptions_controller.rb +0 -52
  46. data/app/controllers/railwatch/executions_controller.rb +0 -13
  47. data/app/controllers/railwatch/issues_controller.rb +0 -258
  48. data/app/controllers/railwatch/jobs_controller.rb +0 -63
  49. data/app/controllers/railwatch/llm_calls_controller.rb +0 -124
  50. data/app/controllers/railwatch/logs_controller.rb +0 -40
  51. data/app/controllers/railwatch/mails_controller.rb +0 -19
  52. data/app/controllers/railwatch/notifications_controller.rb +0 -11
  53. data/app/controllers/railwatch/outgoing_requests_controller.rb +0 -19
  54. data/app/controllers/railwatch/overview_controller.rb +0 -38
  55. data/app/controllers/railwatch/people_controller.rb +0 -25
  56. data/app/controllers/railwatch/processes_controller.rb +0 -33
  57. data/app/controllers/railwatch/profiles_controller.rb +0 -65
  58. data/app/controllers/railwatch/queries_controller.rb +0 -59
  59. data/app/controllers/railwatch/releases_controller.rb +0 -75
  60. data/app/controllers/railwatch/requests_controller.rb +0 -37
  61. data/app/controllers/railwatch/saved_views_controller.rb +0 -53
  62. data/app/controllers/railwatch/scheduled_tasks_controller.rb +0 -45
  63. data/app/controllers/railwatch/spans_controller.rb +0 -47
  64. data/app/controllers/railwatch/storage_ops_controller.rb +0 -15
  65. data/app/controllers/railwatch/tenants_controller.rb +0 -44
  66. data/app/controllers/railwatch/thresholds_controller.rb +0 -47
  67. data/app/controllers/railwatch/traces_controller.rb +0 -72
  68. data/app/controllers/railwatch/transactions_controller.rb +0 -21
  69. data/app/controllers/railwatch/view_renders_controller.rb +0 -28
  70. data/app/controllers/railwatch/visits_controller.rb +0 -53
  71. data/app/frontend/components/app-content.tsx +0 -26
  72. data/app/frontend/components/app-logo-icon.tsx +0 -23
  73. data/app/frontend/components/app-logo.tsx +0 -14
  74. data/app/frontend/components/app-shell.tsx +0 -38
  75. data/app/frontend/components/app-sidebar-header.tsx +0 -54
  76. data/app/frontend/components/app-sidebar.tsx +0 -94
  77. data/app/frontend/components/app-wordmark.tsx +0 -29
  78. data/app/frontend/components/breadcrumbs.tsx +0 -46
  79. data/app/frontend/components/icon.tsx +0 -16
  80. data/app/frontend/components/input-error.tsx +0 -18
  81. data/app/frontend/components/nav-footer.tsx +0 -46
  82. data/app/frontend/components/nav-main.tsx +0 -41
  83. data/app/frontend/components/nav-user.tsx +0 -50
  84. data/app/frontend/components/railwatch/chart-hover.tsx +0 -34
  85. data/app/frontend/components/railwatch/chart-panel.tsx +0 -170
  86. data/app/frontend/components/railwatch/code.tsx +0 -28
  87. data/app/frontend/components/railwatch/command-palette.tsx +0 -132
  88. data/app/frontend/components/railwatch/copy-block.tsx +0 -52
  89. data/app/frontend/components/railwatch/copy-id.tsx +0 -49
  90. data/app/frontend/components/railwatch/cursor-load-more.tsx +0 -39
  91. data/app/frontend/components/railwatch/data-table.tsx +0 -195
  92. data/app/frontend/components/railwatch/digit-roll.tsx +0 -312
  93. data/app/frontend/components/railwatch/empty-state.tsx +0 -159
  94. data/app/frontend/components/railwatch/env-nav.tsx +0 -130
  95. data/app/frontend/components/railwatch/env-switcher.tsx +0 -145
  96. data/app/frontend/components/railwatch/filter-bar.tsx +0 -153
  97. data/app/frontend/components/railwatch/flamegraph.tsx +0 -324
  98. data/app/frontend/components/railwatch/frames.tsx +0 -295
  99. data/app/frontend/components/railwatch/json-viewer.tsx +0 -159
  100. data/app/frontend/components/railwatch/live-dot.tsx +0 -78
  101. data/app/frontend/components/railwatch/live-toggle.tsx +0 -76
  102. data/app/frontend/components/railwatch/metric-chart.tsx +0 -112
  103. data/app/frontend/components/railwatch/mobile-page-switcher.tsx +0 -89
  104. data/app/frontend/components/railwatch/onboarding.tsx +0 -91
  105. data/app/frontend/components/railwatch/origin-identity.tsx +0 -84
  106. data/app/frontend/components/railwatch/page-header.tsx +0 -42
  107. data/app/frontend/components/railwatch/percentile-picker.tsx +0 -18
  108. data/app/frontend/components/railwatch/relative-time.tsx +0 -18
  109. data/app/frontend/components/railwatch/saved-views.tsx +0 -232
  110. data/app/frontend/components/railwatch/segmented.tsx +0 -64
  111. data/app/frontend/components/railwatch/series-chart.tsx +0 -400
  112. data/app/frontend/components/railwatch/shortcuts-dialog.tsx +0 -89
  113. data/app/frontend/components/railwatch/signal-toast.tsx +0 -121
  114. data/app/frontend/components/railwatch/sort-header.tsx +0 -37
  115. data/app/frontend/components/railwatch/source-link.tsx +0 -57
  116. data/app/frontend/components/railwatch/sparkline-cell.tsx +0 -14
  117. data/app/frontend/components/railwatch/sql-block.tsx +0 -116
  118. data/app/frontend/components/railwatch/stat.tsx +0 -185
  119. data/app/frontend/components/railwatch/status-badge.tsx +0 -79
  120. data/app/frontend/components/railwatch/step-picker.tsx +0 -57
  121. data/app/frontend/components/railwatch/timeline.tsx +0 -405
  122. data/app/frontend/components/railwatch/utilisation-bar.tsx +0 -44
  123. data/app/frontend/components/railwatch/window-picker.tsx +0 -147
  124. data/app/frontend/components/ui/avatar.tsx +0 -51
  125. data/app/frontend/components/ui/badge.tsx +0 -46
  126. data/app/frontend/components/ui/breadcrumb.tsx +0 -109
  127. data/app/frontend/components/ui/button.tsx +0 -60
  128. data/app/frontend/components/ui/card.tsx +0 -92
  129. data/app/frontend/components/ui/chart.tsx +0 -374
  130. data/app/frontend/components/ui/checkbox.tsx +0 -32
  131. data/app/frontend/components/ui/collapsible.tsx +0 -31
  132. data/app/frontend/components/ui/command.tsx +0 -182
  133. data/app/frontend/components/ui/dialog.tsx +0 -158
  134. data/app/frontend/components/ui/dropdown-menu.tsx +0 -255
  135. data/app/frontend/components/ui/hover-card.tsx +0 -42
  136. data/app/frontend/components/ui/input.tsx +0 -21
  137. data/app/frontend/components/ui/label.tsx +0 -24
  138. data/app/frontend/components/ui/popover.tsx +0 -87
  139. data/app/frontend/components/ui/select.tsx +0 -185
  140. data/app/frontend/components/ui/separator.tsx +0 -28
  141. data/app/frontend/components/ui/sheet.tsx +0 -137
  142. data/app/frontend/components/ui/sidebar.tsx +0 -726
  143. data/app/frontend/components/ui/skeleton.tsx +0 -13
  144. data/app/frontend/components/ui/sonner.tsx +0 -41
  145. data/app/frontend/components/ui/table.tsx +0 -114
  146. data/app/frontend/components/ui/tabs.tsx +0 -91
  147. data/app/frontend/components/ui/textarea.tsx +0 -18
  148. data/app/frontend/components/ui/tooltip.tsx +0 -61
  149. data/app/frontend/components/user-info.tsx +0 -32
  150. data/app/frontend/components/user-menu-content.tsx +0 -94
  151. data/app/frontend/entrypoints/application.css +0 -244
  152. data/app/frontend/entrypoints/inertia.tsx +0 -92
  153. data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
  154. data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
  155. data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
  156. data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
  157. data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
  158. data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
  159. data/app/frontend/hooks/use-appearance.tsx +0 -67
  160. data/app/frontend/hooks/use-clipboard.ts +0 -32
  161. data/app/frontend/hooks/use-flash.tsx +0 -23
  162. data/app/frontend/hooks/use-hotkeys.ts +0 -63
  163. data/app/frontend/hooks/use-initials.tsx +0 -15
  164. data/app/frontend/hooks/use-live.ts +0 -205
  165. data/app/frontend/hooks/use-mobile-navigation.ts +0 -8
  166. data/app/frontend/hooks/use-mobile.ts +0 -21
  167. data/app/frontend/hooks/use-percentile.ts +0 -24
  168. data/app/frontend/hooks/use-reduced-motion.ts +0 -25
  169. data/app/frontend/hooks/use-row-nav.ts +0 -106
  170. data/app/frontend/hooks/use-step.ts +0 -27
  171. data/app/frontend/hooks/use-ticker.ts +0 -12
  172. data/app/frontend/hooks/use-window.ts +0 -58
  173. data/app/frontend/layouts/app/app-sidebar-layout.tsx +0 -107
  174. data/app/frontend/layouts/app-layout.tsx +0 -21
  175. data/app/frontend/layouts/env-layout.tsx +0 -53
  176. data/app/frontend/layouts/persistent-layout.tsx +0 -18
  177. data/app/frontend/lib/cable.ts +0 -10
  178. data/app/frontend/lib/env-pages.ts +0 -236
  179. data/app/frontend/lib/execution-path.ts +0 -55
  180. data/app/frontend/lib/explain.ts +0 -50
  181. data/app/frontend/lib/filter.ts +0 -25
  182. data/app/frontend/lib/flamegraph.ts +0 -155
  183. data/app/frontend/lib/format.ts +0 -71
  184. data/app/frontend/lib/keyboard.ts +0 -18
  185. data/app/frontend/lib/mount.ts +0 -23
  186. data/app/frontend/lib/source-link.ts +0 -217
  187. data/app/frontend/lib/utils.ts +0 -6
  188. data/app/frontend/pages/alerts/index.tsx +0 -171
  189. data/app/frontend/pages/broadcasts/index.tsx +0 -171
  190. data/app/frontend/pages/cache_events/index.tsx +0 -127
  191. data/app/frontend/pages/commands/index.tsx +0 -143
  192. data/app/frontend/pages/deploys/index.tsx +0 -94
  193. data/app/frontend/pages/deploys/show.tsx +0 -309
  194. data/app/frontend/pages/deprecations/index.tsx +0 -176
  195. data/app/frontend/pages/exceptions/index.tsx +0 -271
  196. data/app/frontend/pages/executions/show.tsx +0 -821
  197. data/app/frontend/pages/issues/index.tsx +0 -399
  198. data/app/frontend/pages/issues/show.tsx +0 -1367
  199. data/app/frontend/pages/jobs/index.tsx +0 -381
  200. data/app/frontend/pages/jobs/klass.tsx +0 -215
  201. data/app/frontend/pages/llm_calls/index.tsx +0 -664
  202. data/app/frontend/pages/logs/index.tsx +0 -303
  203. data/app/frontend/pages/mails/index.tsx +0 -218
  204. data/app/frontend/pages/notifications/index.tsx +0 -161
  205. data/app/frontend/pages/outgoing_requests/index.tsx +0 -309
  206. data/app/frontend/pages/overview/show.tsx +0 -333
  207. data/app/frontend/pages/people/index.tsx +0 -102
  208. data/app/frontend/pages/people/show.tsx +0 -149
  209. data/app/frontend/pages/processes/index.tsx +0 -335
  210. data/app/frontend/pages/profiles/index.tsx +0 -110
  211. data/app/frontend/pages/profiles/show.tsx +0 -105
  212. data/app/frontend/pages/queries/index.tsx +0 -492
  213. data/app/frontend/pages/queries/show.tsx +0 -276
  214. data/app/frontend/pages/releases/index.tsx +0 -190
  215. data/app/frontend/pages/releases/release-health.tsx +0 -68
  216. data/app/frontend/pages/releases/release-path.ts +0 -25
  217. data/app/frontend/pages/releases/show.tsx +0 -262
  218. data/app/frontend/pages/requests/index.tsx +0 -235
  219. data/app/frontend/pages/requests/route.tsx +0 -159
  220. data/app/frontend/pages/scheduled_tasks/index.tsx +0 -220
  221. data/app/frontend/pages/spans/index.tsx +0 -229
  222. data/app/frontend/pages/spans/show.tsx +0 -220
  223. data/app/frontend/pages/storage_ops/index.tsx +0 -150
  224. data/app/frontend/pages/tenants/index.tsx +0 -236
  225. data/app/frontend/pages/tenants/show.tsx +0 -477
  226. data/app/frontend/pages/tenants/tenant-path.ts +0 -26
  227. data/app/frontend/pages/thresholds/index.tsx +0 -414
  228. data/app/frontend/pages/traces/show.tsx +0 -208
  229. data/app/frontend/pages/transactions/index.tsx +0 -220
  230. data/app/frontend/pages/view_renders/index.tsx +0 -241
  231. data/app/frontend/pages/visits/index.tsx +0 -321
  232. data/app/frontend/public/icon.png +0 -0
  233. data/app/frontend/public/icon.svg +0 -5
  234. data/app/frontend/routes/index.d.ts +0 -2136
  235. data/app/frontend/routes/index.js +0 -2112
  236. data/app/frontend/types/globals.d.ts +0 -9
  237. data/app/frontend/types/index.ts +0 -288
  238. data/app/frontend/types/vite-env.d.ts +0 -9
  239. data/app/helpers/railwatch/assets_helper.rb +0 -52
  240. data/app/jobs/railwatch/anomaly_scan_job.rb +0 -11
  241. data/app/jobs/railwatch/application_job.rb +0 -7
  242. data/app/jobs/railwatch/auto_resolve_issues_job.rb +0 -19
  243. data/app/jobs/railwatch/check_scheduled_tasks_job.rb +0 -113
  244. data/app/jobs/railwatch/detect_anomalies_job.rb +0 -171
  245. data/app/jobs/railwatch/detect_performance_issues_job.rb +0 -95
  246. data/app/jobs/railwatch/group_exceptions_job.rb +0 -91
  247. data/app/jobs/railwatch/optimize_telemetry_job.rb +0 -23
  248. data/app/jobs/railwatch/performance_scan_job.rb +0 -11
  249. data/app/jobs/railwatch/prune_telemetry_job.rb +0 -129
  250. data/app/jobs/railwatch/release_health_rollup_job.rb +0 -64
  251. data/app/jobs/railwatch/rollup_catchup_job.rb +0 -21
  252. data/app/jobs/railwatch/rollup_job.rb +0 -132
  253. data/app/jobs/railwatch/scheduled_task_scan_job.rb +0 -11
  254. data/app/models/concerns/railwatch/detection_snapshotting.rb +0 -20
  255. data/app/models/railwatch/alert.rb +0 -229
  256. data/app/models/railwatch/alert_rule.rb +0 -121
  257. data/app/models/railwatch/anomaly_rule.rb +0 -51
  258. data/app/models/railwatch/application.rb +0 -44
  259. data/app/models/railwatch/application_record.rb +0 -33
  260. data/app/models/railwatch/comment.rb +0 -36
  261. data/app/models/railwatch/deploy.rb +0 -67
  262. data/app/models/railwatch/environment.rb +0 -68
  263. data/app/models/railwatch/execution_presenter.rb +0 -185
  264. data/app/models/railwatch/filter_query.rb +0 -184
  265. data/app/models/railwatch/followup_receipt.rb +0 -27
  266. data/app/models/railwatch/ingest/batch.rb +0 -355
  267. data/app/models/railwatch/ingest/mapper.rb +0 -585
  268. data/app/models/railwatch/ingest/payload.rb +0 -96
  269. data/app/models/railwatch/ingest/rollup_absorber.rb +0 -170
  270. data/app/models/railwatch/ingest/writer.rb +0 -137
  271. data/app/models/railwatch/issue.rb +0 -277
  272. data/app/models/railwatch/issue_activity.rb +0 -23
  273. data/app/models/railwatch/issue_detection_presenter.rb +0 -309
  274. data/app/models/railwatch/issue_detection_snapshot.rb +0 -77
  275. data/app/models/railwatch/maintenance_task.rb +0 -53
  276. data/app/models/railwatch/saved_view.rb +0 -63
  277. data/app/models/railwatch/telemetry/aggregations.rb +0 -251
  278. data/app/models/railwatch/telemetry/attachment.rb +0 -31
  279. data/app/models/railwatch/telemetry/bounded_gzip.rb +0 -101
  280. data/app/models/railwatch/telemetry/broadcast.rb +0 -13
  281. data/app/models/railwatch/telemetry/cache_event.rb +0 -16
  282. data/app/models/railwatch/telemetry/child.rb +0 -53
  283. data/app/models/railwatch/telemetry/cursor_page.rb +0 -99
  284. data/app/models/railwatch/telemetry/deprecation.rb +0 -13
  285. data/app/models/railwatch/telemetry/enqueued_job.rb +0 -13
  286. data/app/models/railwatch/telemetry/exception.rb +0 -28
  287. data/app/models/railwatch/telemetry/execution.rb +0 -152
  288. data/app/models/railwatch/telemetry/export_delivery.rb +0 -32
  289. data/app/models/railwatch/telemetry/export_destination.rb +0 -66
  290. data/app/models/railwatch/telemetry/health_sample.rb +0 -72
  291. data/app/models/railwatch/telemetry/ingest_batch.rb +0 -31
  292. data/app/models/railwatch/telemetry/llm_call.rb +0 -37
  293. data/app/models/railwatch/telemetry/log.rb +0 -91
  294. data/app/models/railwatch/telemetry/mail.rb +0 -13
  295. data/app/models/railwatch/telemetry/n_plus_one.rb +0 -92
  296. data/app/models/railwatch/telemetry/notification.rb +0 -13
  297. data/app/models/railwatch/telemetry/outgoing_request.rb +0 -13
  298. data/app/models/railwatch/telemetry/person.rb +0 -50
  299. data/app/models/railwatch/telemetry/process.rb +0 -10
  300. data/app/models/railwatch/telemetry/profile.rb +0 -37
  301. data/app/models/railwatch/telemetry/query.rb +0 -57
  302. data/app/models/railwatch/telemetry/query_shape.rb +0 -45
  303. data/app/models/railwatch/telemetry/release_health.rb +0 -69
  304. data/app/models/railwatch/telemetry/rollup.rb +0 -157
  305. data/app/models/railwatch/telemetry/session.rb +0 -24
  306. data/app/models/railwatch/telemetry/span.rb +0 -19
  307. data/app/models/railwatch/telemetry/storage_op.rb +0 -13
  308. data/app/models/railwatch/telemetry/tenant.rb +0 -240
  309. data/app/models/railwatch/telemetry/transaction.rb +0 -13
  310. data/app/models/railwatch/telemetry/view_render.rb +0 -13
  311. data/app/models/railwatch/telemetry/visit.rb +0 -24
  312. data/app/models/railwatch/telemetry_record.rb +0 -107
  313. data/app/models/railwatch/threshold.rb +0 -66
  314. data/app/models/railwatch/user.rb +0 -47
  315. data/app/models/railwatch/viewer.rb +0 -13
  316. data/app/models/railwatch/window.rb +0 -66
  317. data/app/views/layouts/railwatch/dashboard.html.erb +0 -25
  318. data/db/railwatch_migrate/20260916000000_create_railwatch_tables.rb +0 -151
  319. data/db/railwatch_migrate/20260917000000_create_railwatch_maintenance_tasks.rb +0 -18
  320. data/db/railwatch_migrate/20260917120000_create_railwatch_followup_receipts.rb +0 -20
  321. data/db/railwatch_migrate/20260918120000_widen_host_user_ids.rb +0 -71
  322. data/db/railwatch_telemetry_migrate/20260903000000_enable_incremental_vacuum.rb +0 -62
  323. data/db/railwatch_telemetry_migrate/20260903000001_create_telemetry.rb +0 -481
  324. data/db/railwatch_telemetry_migrate/20260903000002_rename_tenant_to_app_tenant.rb +0 -14
  325. data/db/railwatch_telemetry_migrate/20260903000003_add_statement_count_to_transactions.rb +0 -9
  326. data/db/railwatch_telemetry_migrate/20260903000004_add_role_and_channel.rb +0 -10
  327. data/db/railwatch_telemetry_migrate/20260903000005_add_locals_to_exceptions.rb +0 -9
  328. data/db/railwatch_telemetry_migrate/20260903000006_add_spans_health_vitals_and_fts.rb +0 -82
  329. data/db/railwatch_telemetry_migrate/20260903000007_rename_span_attributes_to_payload.rb +0 -10
  330. data/db/railwatch_telemetry_migrate/20260903000008_add_profiles_and_attachments.rb +0 -60
  331. data/db/railwatch_telemetry_migrate/20260903000009_add_truncated_to_attachments.rb +0 -9
  332. data/db/railwatch_telemetry_migrate/20260903000010_create_sessions_and_release_health.rb +0 -51
  333. data/db/railwatch_telemetry_migrate/20260903000011_add_fingerprint_to_exceptions.rb +0 -11
  334. data/db/railwatch_telemetry_migrate/20260904000012_add_failed_to_broadcasts.rb +0 -10
  335. data/db/railwatch_telemetry_migrate/20260904010000_add_filter_cursor_indexes.rb +0 -37
  336. data/db/railwatch_telemetry_migrate/20260904020000_add_n_plus_ones_execution_id_index.rb +0 -11
  337. data/db/railwatch_telemetry_migrate/20260904120000_create_query_shapes.rb +0 -15
  338. data/db/railwatch_telemetry_migrate/20260906120000_add_backpressure_factor_to_ingest_batches.rb +0 -9
  339. data/db/railwatch_telemetry_migrate/20260907000000_rename_lantern_version_on_processes.rb +0 -10
  340. data/db/railwatch_telemetry_migrate/20260913000000_rename_nightrail_version_on_processes.rb +0 -9
  341. data/db/railwatch_telemetry_migrate/20260914000000_drop_orphan_durable_ingest_tables.rb +0 -18
  342. data/db/railwatch_telemetry_migrate/20260914010000_drop_orphan_durable_ingest_columns.rb +0 -25
  343. data/db/railwatch_telemetry_migrate/20260915000000_create_llm_calls.rb +0 -55
  344. data/db/railwatch_telemetry_migrate/20260915120000_add_detail_to_llm_calls.rb +0 -21
  345. data/db/railwatch_telemetry_migrate/20260915200000_add_explained_index_to_queries.rb +0 -16
  346. data/db/railwatch_telemetry_migrate/20260915210000_add_slowest_index_to_queries.rb +0 -18
  347. data/db/railwatch_telemetry_migrate/20260917010000_add_batch_ledger_to_ingest_batches.rb +0 -16
  348. data/db/railwatch_telemetry_migrate/20260919000100_create_export_queue.rb +0 -146
  349. data/db/railwatch_telemetry_migrate/20260922000000_add_covering_indexes_for_dashboard_aggregates.rb +0 -34
  350. data/db/railwatch_telemetry_migrate/20260923000000_add_people_index.rb +0 -15
  351. data/db/railwatch_telemetry_migrate/20260925000000_add_tenant_summary_index.rb +0 -17
  352. data/db/railwatch_telemetry_migrate/20261008000000_add_in_transaction_to_outgoing_requests.rb +0 -10
  353. data/docs/embedded.md +0 -503
  354. data/lib/puma/plugin/railwatch.rb +0 -215
  355. data/lib/railwatch/dashboard_assets.rb +0 -45
  356. data/lib/railwatch/embedded.rb +0 -50
  357. data/lib/railwatch/export/client.rb +0 -124
  358. data/lib/railwatch/export/lease.rb +0 -56
  359. data/lib/railwatch/export/outbox.rb +0 -361
  360. data/lib/railwatch/export/policy.rb +0 -79
  361. data/lib/railwatch/export/sender.rb +0 -130
  362. data/lib/railwatch/ingest_request_body_limit.rb +0 -10
  363. data/lib/railwatch/maintenance.rb +0 -194
  364. data/lib/railwatch/patches/migration_busy_timeout.rb +0 -73
  365. data/lib/railwatch/transport/local.rb +0 -78
  366. data/lib/railwatch/transport/socket.rb +0 -183
  367. data/lib/railwatch/writer.rb +0 -370
  368. data/public/railwatch/assets/CommitMono-Bold-D6h61ieg.woff2 +0 -0
  369. data/public/railwatch/assets/CommitMono-Regular-zr8w7Obm.woff2 +0 -0
  370. data/public/railwatch/assets/Roboto-Black-auA4GeOK.woff2 +0 -0
  371. data/public/railwatch/assets/Roboto-Bold-CJLnO8j1.woff2 +0 -0
  372. data/public/railwatch/assets/Roboto-Medium-Cm2bwKpj.woff2 +0 -0
  373. data/public/railwatch/assets/Roboto-Regular-Chaq1-PV.woff2 +0 -0
  374. data/public/railwatch/assets/app-layout-by7G3sa1.js +0 -41
  375. data/public/railwatch/assets/application-Cc5v_Vk9.css +0 -1
  376. data/public/railwatch/assets/arrow-up-DCZ7emux.js +0 -1
  377. data/public/railwatch/assets/braces-CykT5_5u.js +0 -1
  378. data/public/railwatch/assets/card-C-PPSA0g.js +0 -1
  379. data/public/railwatch/assets/chart-Cov79YC5.js +0 -39
  380. data/public/railwatch/assets/chart-hover-DdK4E-2h.js +0 -1
  381. data/public/railwatch/assets/chart-panel-BPaYRXJB.js +0 -1
  382. data/public/railwatch/assets/code-BYjC4YZ_.js +0 -1
  383. data/public/railwatch/assets/copy-id-LPfoj9cC.js +0 -1
  384. data/public/railwatch/assets/cursor-load-more-DSdhgpGl.js +0 -1
  385. data/public/railwatch/assets/data-table-CQ_YRWQs.js +0 -1
  386. data/public/railwatch/assets/env-layout-CdMWbUkm.js +0 -1
  387. data/public/railwatch/assets/execution-path-7MYt-Qbn.js +0 -1
  388. data/public/railwatch/assets/filter-bar-CrzpQ_vD.js +0 -1
  389. data/public/railwatch/assets/flamegraph-BIFEdTAg.js +0 -2
  390. data/public/railwatch/assets/format-B9SDkrWj.js +0 -1
  391. data/public/railwatch/assets/frames-C5t-x4f0.js +0 -1
  392. data/public/railwatch/assets/index--NQ7us-L.js +0 -1
  393. data/public/railwatch/assets/index-2kgYe_99.js +0 -1
  394. data/public/railwatch/assets/index-4YEPHxtO.js +0 -1
  395. data/public/railwatch/assets/index-AOCC1GDJ.js +0 -1
  396. data/public/railwatch/assets/index-BEo-u0eO.js +0 -1
  397. data/public/railwatch/assets/index-BJ1V3w2N.js +0 -1
  398. data/public/railwatch/assets/index-BPoHsBKb.js +0 -1
  399. data/public/railwatch/assets/index-BZzyGUM3.js +0 -1
  400. data/public/railwatch/assets/index-BgVq7yKL.js +0 -1
  401. data/public/railwatch/assets/index-BqFGEZvW.js +0 -1
  402. data/public/railwatch/assets/index-ByuHJ1CH.js +0 -1
  403. data/public/railwatch/assets/index-C2j50AL2.js +0 -1
  404. data/public/railwatch/assets/index-CLEdTkvW.js +0 -1
  405. data/public/railwatch/assets/index-CWgq8qBu.js +0 -1
  406. data/public/railwatch/assets/index-CXQ7_oVj.js +0 -2
  407. data/public/railwatch/assets/index-CmyYrjS2.js +0 -1
  408. data/public/railwatch/assets/index-Cn9_HjDo.js +0 -1
  409. data/public/railwatch/assets/index-CsMAm9UK.js +0 -1
  410. data/public/railwatch/assets/index-D0Kk_oGY.js +0 -1
  411. data/public/railwatch/assets/index-DC9ZptJh.js +0 -1
  412. data/public/railwatch/assets/index-DNzg7jmV.js +0 -1
  413. data/public/railwatch/assets/index-DQ8uJO5f.js +0 -1
  414. data/public/railwatch/assets/index-Deq14BxB.js +0 -1
  415. data/public/railwatch/assets/index-IaV3Blzp.js +0 -1
  416. data/public/railwatch/assets/index-TYNWcJQs.js +0 -1
  417. data/public/railwatch/assets/index-WonH2fcZ.js +0 -1
  418. data/public/railwatch/assets/index-WxWUaH_d.js +0 -1
  419. data/public/railwatch/assets/index-mUNrl4ky.js +0 -1
  420. data/public/railwatch/assets/inertia-DfNRARuA.js +0 -97
  421. data/public/railwatch/assets/json-viewer-DMsF5glp.js +0 -1
  422. data/public/railwatch/assets/klass-CmHYk5rz.js +0 -1
  423. data/public/railwatch/assets/origin-identity-1R3bqa3R.js +0 -1
  424. data/public/railwatch/assets/percentile-picker-73z5PAps.js +0 -1
  425. data/public/railwatch/assets/relative-time-Dsc_cCdX.js +0 -1
  426. data/public/railwatch/assets/release-health-BV5OaOWd.js +0 -1
  427. data/public/railwatch/assets/route-CWh58C7K.js +0 -1
  428. data/public/railwatch/assets/segmented-DB4zh-Am.js +0 -1
  429. data/public/railwatch/assets/select-DBKjFlOX.js +0 -1
  430. data/public/railwatch/assets/series-chart-BaiDG795.js +0 -1
  431. data/public/railwatch/assets/show-B4ICCFGh.js +0 -1
  432. data/public/railwatch/assets/show-B5lOU9Gv.js +0 -1
  433. data/public/railwatch/assets/show-BYPB74t4.js +0 -2
  434. data/public/railwatch/assets/show-C7A0m2M8.js +0 -1
  435. data/public/railwatch/assets/show-DT5X1zqE.js +0 -1
  436. data/public/railwatch/assets/show-D_NBdXsn.js +0 -2
  437. data/public/railwatch/assets/show-DgPCnRg8.js +0 -1
  438. data/public/railwatch/assets/show-Yl69HF0x.js +0 -1
  439. data/public/railwatch/assets/show-_QxuUQCm.js +0 -3
  440. data/public/railwatch/assets/show-g__jnN36.js +0 -1
  441. data/public/railwatch/assets/show-l4ztfjl9.js +0 -1
  442. data/public/railwatch/assets/sort-header-BcG8ulEH.js +0 -1
  443. data/public/railwatch/assets/source-link-B2183i2-.js +0 -1
  444. data/public/railwatch/assets/sparkline-cell-B47ipscB.js +0 -1
  445. data/public/railwatch/assets/stat-DT2hhsqS.js +0 -1
  446. data/public/railwatch/assets/status-badge-CmbRKlY7.js +0 -1
  447. data/public/railwatch/assets/tenant-path-C4rNGauE.js +0 -1
  448. data/public/railwatch/assets/use-clipboard-oTNeP4ax.js +0 -1
  449. data/public/railwatch/assets/use-live-CLZZj245.js +0 -1
  450. data/public/railwatch/icon.png +0 -0
  451. data/public/railwatch/icon.svg +0 -5
  452. data/public/railwatch/manifest.json +0 -1245
  453. data/public/railwatch/rails-vite.json +0 -1
  454. /data/{app/frontend/lib → lib/generators/railwatch/install/templates}/railwatch.ts +0 -0
@@ -1,258 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class IssuesController < DashboardController
5
- BULK_LIMIT = 200
6
-
7
- before_action :set_issue, only: %i[show update merge_candidates]
8
- rescue_from Issue::InvalidMerge, with: :invalid_merge
9
-
10
- def index
11
- scope = Issue.all
12
- scope = scope.where(status: params[:status]) if Issue::STATUSES.include?(params[:status].to_s)
13
- scope = scope.where(status: "open") unless params.key?(:status)
14
- scope = scope.where(application_id: params[:application_id]) if params[:application_id].present?
15
- scope = scope.where(environment_id: params[:environment_id]) if params[:environment_id].present?
16
- scope = scope.where(kind: params[:kind]) if Issue::KINDS.include?(params[:kind].to_s)
17
- scope = scope.where(assignee_id: Viewer.user.id) if params[:mine] == "1"
18
- # Same "key:value free text" grammar as the other list pages: today the
19
- # only key is source, which is how you pull out the browser's own errors.
20
- parsed = FilterQuery.parse(params[:q])
21
- scope = scope.where(source: parsed[:fields]["source"]) if parsed[:fields]["source"].present?
22
- scope = scope.where("title LIKE ?", "%#{Issue.sanitize_sql_like(parsed[:text])}%") if parsed[:text].present?
23
- render inertia: { issues: scope.recent.limit(200).map { |i| row(i) },
24
- filters: params.permit(:status, :application_id, :environment_id, :kind, :mine, :q).to_h,
25
- counts: Issue.all.group(:status).count,
26
- kindCounts: Issue.all.open.group(:kind).count,
27
- members: User.default.then { |u| [ { id: u.id, name: u.name } ] } }
28
- end
29
-
30
- def show
31
- env = @issue.environment
32
- return render_detection_issue(env) unless @issue.kind == "exception"
33
-
34
- from = 30.days.ago
35
- occurrences = env.with_telemetry do
36
- Telemetry::Exception.where(group_hash: @issue.group_hash).between(from, Time.current).recent.limit(50)
37
- .map { |e| { id: e.id, message: e.message.first(300), handled: e.handled, occurred_at: e.occurred_at, execution_id: e.execution_id, execution_source: e.execution_source, execution_preview: e.execution_preview, user_ref: e.user_ref, tenant: e.app_tenant, deploy: e.deploy, server: e.server } }
38
- end
39
- latest = env.with_telemetry { Telemetry::Exception.where(group_hash: @issue.group_hash).recent.first }
40
- browser_breadcrumbs = browser_breadcrumbs_for(latest)
41
- breadcrumbs = latest&.execution_id ? env.with_telemetry { breadcrumbs_for(latest) } : []
42
- daily_counts = env.with_telemetry { Telemetry::Exception.where(group_hash: @issue.group_hash).between(from, Time.current).group(Arel.sql("date(occurred_at)")).count }.transform_keys(&:to_s)
43
- daily = (from.to_date..Date.current).map { |d| { day: d.to_s, count: daily_counts[d.to_s].to_i } }
44
- by_deploy = env.with_telemetry { Telemetry::Exception.where(group_hash: @issue.group_hash).between(from, Time.current).group(:deploy).count }
45
- by_tenant = env.with_telemetry { Telemetry::Exception.where(group_hash: @issue.group_hash).between(from, Time.current).where.not(app_tenant: nil).group(:app_tenant).count }
46
- attachments = env.with_telemetry { attachments_for_issue }
47
- # What a split by message would produce, and what the fingerprint that
48
- # grouped these occurrences was -- both drive the Grouping panel.
49
- messages = env.with_telemetry { Telemetry::Exception.where(group_hash: @issue.group_hash).recent.limit(Issue::SPLIT_SAMPLE).pluck(:message) }.tally
50
- render inertia: {
51
- issue: row(@issue).merge(first_seen_at: @issue.first_seen_at, resolved_at: @issue.resolved_at, resolved_in_deploy: @issue.resolved_in_deploy, regressed_at: @issue.regressed_at, sample: @issue.sample),
52
- detection: nil,
53
- latest: latest && { id: latest.id, class_name: latest.class_name, message: latest.message, handled: latest.handled, severity: latest.severity, source: latest.source, file: latest.file, line: latest.line, frames: latest.frames, cause: latest.cause, locals: latest.locals, context: context_without_breadcrumbs(latest), ruby_version: latest.ruby_version, rails_version: latest.rails_version, execution_id: latest.execution_id, execution_source: latest.execution_source, execution_preview: latest.execution_preview, deploy: latest.deploy },
54
- breadcrumbs: breadcrumbs, browser_breadcrumbs: browser_breadcrumbs,
55
- fingerprint: (latest&.fingerprint).presence || Array(@issue.sample["fingerprint"]),
56
- fingerprint_source: latest&.fingerprint_source || @issue.sample["fingerprint_source"],
57
- distinct_messages: messages.size,
58
- top_messages: messages.sort_by { |_, n| -n }.first(5).map { |message, n| { message: message.to_s.first(200), count: n } },
59
- occurrences: occurrences, daily: daily, by_deploy: by_deploy, by_tenant: by_tenant, attachments: attachments,
60
- comments: @issue.comments.order(:created_at).map { |c| { id: c.id, body: c.body, user: c.user&.name || c.author_name || "Linear", source: c.source, created_at: c.created_at } },
61
- activities: @issue.activities.order(:created_at).map { |a| activity_row(a) },
62
- members: User.default.then { |u| [ { id: u.id, name: u.name } ] },
63
- deploys: env.deploys.recent.limit(20).map { |d| { deploy: d.deploy, ref: d.short_ref, at: d.deployed_at } },
64
- related_issues: related_issues,
65
- merged_into: @issue.merged_into && { id: @issue.merged_into.id, key: @issue.merged_into.key, title: @issue.merged_into.title },
66
- merged_issues: @issue.merged_issues.map { |i| { id: i.id, key: i.key, title: i.title } },
67
- alerts: @issue.alerts.order(created_at: :desc).map { |a| alert_row(a) }
68
- }
69
- end
70
-
71
- def merge_candidates
72
- query = params[:q].to_s.strip.first(200)
73
- scope = @issue.environment.issues.open.where.not(id: @issue.id)
74
- if query.present?
75
- pattern = "%#{Issue.sanitize_sql_like(query)}%"
76
- key_match = query.upcase.match(/\A#{Regexp.escape(@issue.application.issue_prefix)}-(\d+)\z/)
77
- scope = if key_match
78
- scope.where("title LIKE :pattern OR number = :number", pattern: pattern, number: key_match[1].to_i)
79
- else
80
- scope.where("title LIKE ?", pattern)
81
- end
82
- end
83
-
84
- render json: { issues: scope.order(last_seen_at: :desc, id: :desc).limit(20)
85
- .map { |issue| { id: issue.id, key: issue.key, title: issue.title } } }
86
- end
87
-
88
- def update
89
- notice = "Issue updated"
90
- case params[:action_name]
91
- when "resolve" then @issue.resolve!(deploy: params[:deploy].presence || @issue.environment.deploys.recent.first&.deploy)
92
- when "ignore" then @issue.ignore!
93
- when "reopen" then @issue.reopen!
94
- when "assign" then @issue.update!(assignee: assignee_from(params[:assignee_id]))
95
- when "assign_me" then @issue.update!(assignee_id: Viewer.user.id)
96
- when "priority" then @issue.update!(priority: params[:priority])
97
- when "merge" then @issue.merge_into!(@issue.environment.issues.open.find(params[:target_id]))
98
- when "unmerge" then @issue.unmerge!
99
- when "split" then notice = split_notice(@issue.split!(by: params[:by].to_s))
100
- else @issue.update!(params.permit(:title))
101
- end
102
- redirect_to issue_path(@issue), notice: notice
103
- end
104
-
105
- def bulk
106
- ids = Array(params[:ids]).first(BULK_LIMIT)
107
- scope = Issue.all.where(id: ids)
108
- if params[:action_name] == "merge"
109
- bulk_merge(scope)
110
- else
111
- scope.find_each do |issue|
112
- case params[:action_name]
113
- when "resolve" then issue.resolve!(deploy: issue.environment.deploys.recent.first&.deploy)
114
- when "ignore" then issue.ignore!
115
- when "priority" then issue.update!(priority: params[:value])
116
- end
117
- end
118
- end
119
- redirect_back fallback_location: issues_path, notice: "#{ids.size} issue(s) updated"
120
- end
121
-
122
- private
123
-
124
- # A stale selection can contain issues that have already been merged since
125
- # the list rendered. Ignore those sources, choose an open target, and wrap
126
- # the whole operation so a concurrent state change cannot partially fold a
127
- # bulk selection before the model rejects it.
128
- def bulk_merge(scope)
129
- issues = scope.order(:first_seen_at, :id).to_a
130
- target = issues.find(&:open?)
131
- return unless target
132
-
133
- Issue.transaction do
134
- issues.each do |issue|
135
- next if issue.id == target.id || issue.environment_id != target.environment_id || issue.merged?
136
-
137
- issue.merge_into!(target)
138
- end
139
- end
140
- end
141
-
142
- def invalid_merge(error)
143
- redirect_back fallback_location: (@issue ? issue_path(@issue) : issues_path), alert: error.message
144
- end
145
-
146
- def render_detection_issue(env)
147
- render inertia: {
148
- issue: row(@issue).merge(first_seen_at: @issue.first_seen_at, resolved_at: @issue.resolved_at,
149
- resolved_in_deploy: @issue.resolved_in_deploy, regressed_at: @issue.regressed_at, sample: @issue.sample),
150
- detection: IssueDetectionPresenter.new(@issue).as_json,
151
- latest: nil, breadcrumbs: [], browser_breadcrumbs: [], fingerprint: [], fingerprint_source: nil,
152
- distinct_messages: 0, top_messages: [], occurrences: [], daily: [], by_deploy: {}, by_tenant: {}, attachments: [],
153
- comments: @issue.comments.order(:created_at).map { |c| { id: c.id, body: c.body, user: c.user.name, created_at: c.created_at } },
154
- activities: @issue.activities.order(:created_at).map { |a| activity_row(a) },
155
- members: User.default.then { |u| [ { id: u.id, name: u.name } ] },
156
- deploys: env.deploys.recent.limit(20).map { |d| { deploy: d.deploy, ref: d.short_ref, at: d.deployed_at } },
157
- related_issues: related_issues,
158
- merged_into: @issue.merged_into && { id: @issue.merged_into.id, key: @issue.merged_into.key, title: @issue.merged_into.title },
159
- merged_issues: @issue.merged_issues.map { |i| { id: i.id, key: i.key, title: i.title } },
160
- alerts: @issue.alerts.order(created_at: :desc).map { |a| alert_row(a) }
161
- }
162
- end
163
-
164
- # Sentry-style breadcrumbs: everything the execution did before it raised
165
- # (queries, cache, outgoing calls, logs, jobs), newest last, capped at 30.
166
- def breadcrumbs_for(exception)
167
- exe = Telemetry::Execution.find_by(execution_id: exception.execution_id) or return []
168
- ExecutionPresenter.new(exe, @issue.environment).timeline_entries
169
- .select { |e| e[:offset] <= ((exception.occurred_at - exe.occurred_at) * 1000.0) + 1 }
170
- .reject { |e| e[:type] == "exception" && e[:id] == exception.id }
171
- .last(30)
172
- end
173
-
174
- # A browser exception's execution is the beacon POST that carried it, whose
175
- # timeline says nothing about the crash. What it has instead is the trail
176
- # the browser client recorded -- console errors, clicks, navigations --
177
- # which the gem stores in the exception's context.
178
- def browser_breadcrumbs_for(exception)
179
- trail = browser_context(exception)["breadcrumbs"]
180
- return [] unless trail.is_a?(Array)
181
- trail.filter_map { |crumb| crumb.slice("at", "kind", "text") if crumb.is_a?(Hash) }
182
- end
183
-
184
- # The same context with the trail taken back out, since the page renders it
185
- # as a breadcrumb list rather than as raw JSON in the context panel.
186
- def context_without_breadcrumbs(exception)
187
- context = parsed_context(exception)
188
- return exception.context unless context["browser"].is_a?(Hash) && context["browser"].key?("breadcrumbs")
189
- context.merge("browser" => context["browser"].except("breadcrumbs")).to_json
190
- end
191
-
192
- def browser_context(exception)
193
- context = parsed_context(exception)
194
- context["browser"].is_a?(Hash) ? context["browser"] : {}
195
- end
196
-
197
- # Exception context is a JSON string the gem built; anything else in that
198
- # column is an older or garbled record, and reads as empty rather than 500.
199
- def parsed_context(exception)
200
- parsed = JSON.parse(exception&.context.to_s)
201
- parsed.is_a?(Hash) ? parsed : {}
202
- rescue JSON::ParserError
203
- {}
204
- end
205
-
206
- # Files the app attached while raising this exception, from any execution.
207
- # `data` is left out of the select: it is the whole gzipped file and the
208
- # page only links to the download.
209
- def attachments_for_issue
210
- Telemetry::Attachment.where(exception_group_hash: @issue.group_hash).recent.limit(20)
211
- .select(:id, :name, :content_type, :bytes, :truncated, :occurred_at, :execution_id, :execution_preview)
212
- .map { |a| { id: a.id, name: a.name, content_type: a.content_type, bytes: a.bytes, truncated: a.truncated,
213
- viewable: a.viewable?, occurred_at: a.occurred_at, execution_id: a.execution_id, execution_preview: a.execution_preview } }
214
- end
215
-
216
- # split! no-ops when there is nothing to split apart (the button is hidden
217
- # then, but a stale page can still ask), so say which one happened.
218
- def split_notice(created)
219
- created.empty? ? "Nothing to split \u2014 every recent occurrence is already in its own issue" : "Split into #{created.size} issue(s)"
220
- end
221
-
222
- def set_issue
223
- @issue = Issue.all.find(params[:id])
224
- end
225
-
226
- # Only a member of the current account can be assigned; nil clears.
227
- def assignee_from(id)
228
- id.present? ? User.find(id) : nil
229
- end
230
-
231
- # Same exception class seen in other environments of the same application:
232
- # issue titles are stored as "ClassName: message", so we match on the
233
- # "ClassName:" prefix.
234
- def related_issues
235
- class_name = @issue.title.to_s.split(":", 2).first
236
- return [] if class_name.blank?
237
- Issue.all.where.not(id: @issue.id)
238
- .where("title LIKE ?", "#{Issue.sanitize_sql_like(class_name)}:%").limit(20)
239
- .map { |i| { id: i.id, key: i.key, title: i.title, status: i.status, environment: { id: i.environment.id, name: i.environment.name } } }
240
- end
241
-
242
- def row(i)
243
- { id: i.id, key: i.key, title: i.title, kind: i.kind, status: i.status, priority: i.priority, culprit: i.culprit, occurrences: i.occurrences,
244
- affected_users: i.affected_users, first_seen_at: i.first_seen_at, last_seen_at: i.last_seen_at, assignee: i.assignee && { id: i.assignee.id, name: i.assignee.name },
245
- fingerprint_source: i.sample["fingerprint_source"], source: i.source,
246
- application: { id: i.application.id, name: i.application.name }, environment: { id: i.environment.id, name: i.environment.name } }
247
- end
248
-
249
- def activity_row(a)
250
- { id: a.id, kind: a.kind, data: a.data, user: a.user && { id: a.user.id, name: a.user.name }, created_at: a.created_at }
251
- end
252
-
253
- def alert_row(a)
254
- { id: a.id, event: a.event, status: a.status, sent_at: a.sent_at, error: a.error,
255
- integration: { kind: a.integration.kind, name: a.integration.name } }
256
- end
257
- end
258
- end
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class JobsController < DashboardController
5
- include TelemetryIdentity
6
-
7
- def index
8
- from, to = window_range
9
- page_data = nil
10
- page = lambda do
11
- page_data ||= telemetry do
12
- scope = FilterQuery.apply(Telemetry::Execution.jobs, resource: :jobs, query: params[:q], from: from, to: to)
13
- rows, meta = Telemetry::CursorPage.call(scope, cursor: params[:cursor], limit: params[:limit],
14
- context: telemetry_cursor_context(:jobs))
15
- people = origin_people(rows)
16
- [ rows.map { |r| job_row(r, people) }, meta ]
17
- end
18
- end
19
- render inertia: {
20
- classes: -> { grouped("job_attempt", limit: 200) }, series: -> { series("job_attempt") },
21
- queues: -> { queue_stats(from, to) }, recent: InertiaRails.merge { page.call.first },
22
- pagination: -> { page.call.last }, deploys: -> { deploys_in_window }, q: params[:q].to_s
23
- }
24
- end
25
-
26
- def klass
27
- group_hash = params[:group_hash]
28
- from, to = window_range
29
- name, attempts = telemetry do
30
- base = Telemetry::Execution.jobs.where(group_hash: group_hash)
31
- rows = FilterQuery.apply(base, resource: :jobs,
32
- query: params[:q], from: from, to: to).recent.limit(200).to_a
33
- people = origin_people(rows)
34
- [ base.between(from, to).pick(:name), rows.map { |row| job_row(row, people) } ]
35
- end
36
- render inertia: "jobs/klass", props: { name: name, group_hash: group_hash,
37
- summary: summary_with_delta("job_attempt", group_hash: group_hash),
38
- series: series("job_attempt", group_hash: group_hash), attempts: attempts,
39
- deploys: deploys_in_window, q: params[:q].to_s }
40
- end
41
-
42
- def show
43
- exe = telemetry { Telemetry::Execution.find_by!(execution_id: params[:id]) }
44
- render inertia: "executions/show", props: ExecutionPresenter.new(exe, environment).props
45
- end
46
-
47
- private
48
-
49
- def queue_stats(from, to)
50
- telemetry do
51
- Telemetry::Execution.jobs.between(from, to).group(:queue)
52
- .pluck(:queue, Arel.sql("COUNT(*)"), Arel.sql("AVG(queue_latency)"), Arel.sql("SUM(CASE WHEN outcome = 'failed' THEN 1 ELSE 0 END)"))
53
- .map { |q, c, lat, f| { queue: q, count: c, avg_latency: (lat.to_f / 1000.0).round(1), failed: f } }
54
- end
55
- end
56
-
57
- def job_row(r, people)
58
- { execution_id: r.execution_id, name: r.name, outcome: r.outcome, queue: r.queue, attempt: r.attempt, duration: r.duration_ms.round(2),
59
- queue_latency: r.queue_latency && (r.queue_latency / 1000.0).round(1), occurred_at: r.occurred_at, exception_preview: r.exception_preview,
60
- job_id: r.job_id, deploy: r.deploy }.merge(origin_identity(r, people))
61
- end
62
- end
63
- end
@@ -1,124 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class LlmCallsController < DashboardController
5
- NANOS_PER_DOLLAR = 1_000_000_000.0
6
-
7
- def index
8
- from, to = window_range
9
- fields = FilterQuery.parse(params[:q]).fetch(:fields)
10
- models = telemetry { model_rows(from, to) }
11
- tools = telemetry { tool_rows(from, to) }
12
- rows = telemetry { recent_rows(fields) }
13
- render inertia: { models: models, tools: tools, recent: rows,
14
- totals: totals(models, tools),
15
- # Two record types over one table, the way Execution
16
- # feeds request/job_attempt/... Waiting on a model is
17
- # seconds of somebody else's compute; a tool call is
18
- # your own code in milliseconds. One p95 over both
19
- # describes neither.
20
- series: series("llm_call"), tool_series: series("llm_tool"),
21
- q: params[:q].to_s }
22
- end
23
-
24
- private
25
-
26
- # One row per provider/model/operation group, from rollups: tokens and
27
- # spend come out of Rollup#extra, which sums them on merge, so a 30-day
28
- # window costs the same few rows as an hour.
29
- def model_rows(from, to)
30
- Telemetry::Rollup.for_type("llm_call").between(from, to).to_a
31
- .group_by(&:group_hash).map { |group_hash, group_rows| model_row(group_hash, group_rows, from, to) }
32
- .sort_by { |r| [ -(r[:cost] || 0), -r[:count] ] }.first(200)
33
- end
34
-
35
- # Tools have no tokens and no price, so they rank by how much time they
36
- # actually cost the request: slowest first.
37
- def tool_rows(from, to)
38
- Telemetry::Rollup.for_type("llm_tool").between(from, to).to_a
39
- .group_by(&:group_hash).map do |group_hash, group_rows|
40
- summary = Telemetry::Rollup.summarize(group_rows)
41
- { group_hash: group_hash, name: group_rows.max_by(&:bucket).name,
42
- count: group_rows.sum(&:count), errors: group_rows.sum(&:error_count),
43
- avg: (summary[:avg] / 1000.0).round(2), p95: (summary[:p95] / 1000.0).round(2),
44
- max: (summary[:max] / 1000.0).round(2),
45
- sparkline: Telemetry::Aggregations.sparkline(group_rows, from, to) }
46
- end.sort_by { |r| -r[:p95] }.first(200)
47
- end
48
-
49
- def model_row(group_hash, group_rows, from, to)
50
- count = group_rows.sum(&:count)
51
- summary = Telemetry::Rollup.summarize(group_rows)
52
- nanos = sum_extra(group_rows, "cost_nanos")
53
- priced = sum_extra(group_rows, "priced")
54
- unpriced = sum_extra(group_rows, "unpriced")
55
- {
56
- group_hash: group_hash, name: group_rows.max_by(&:bucket).name, count: count,
57
- errors: group_rows.sum(&:error_count),
58
- input_tokens: sum_extra(group_rows, "input_tokens"), output_tokens: sum_extra(group_rows, "output_tokens"),
59
- cache_read_tokens: sum_extra(group_rows, "cache_read_tokens"),
60
- cache_write_tokens: sum_extra(group_rows, "cache_write_tokens"),
61
- # nil, not 0, when nothing in the group was priced: a table that shows
62
- # $0.00 for an unpriced model is lying about the bill. `unpriced`
63
- # carries how many calls the figure leaves out when it is partial.
64
- cost: priced.zero? ? nil : nanos / NANOS_PER_DOLLAR, unpriced: unpriced,
65
- # Neither of these is an error, so neither shows in the error rate.
66
- truncated: sum_extra(group_rows, "truncated"),
67
- with_attachments: sum_extra(group_rows, "with_attachments"),
68
- avg: (summary[:avg] / 1000.0).round(2), p95: (summary[:p95] / 1000.0).round(2),
69
- sparkline: Telemetry::Aggregations.sparkline(group_rows, from, to)
70
- }
71
- end
72
-
73
- def sum_extra(rows, key)
74
- rows.sum { |r| r.extra[key].to_i }
75
- end
76
-
77
- # `count` is model calls only. Tool calls are counted separately rather
78
- # than folded in: they cost no tokens and no money, so adding them to the
79
- # headline number inflates it with work that never reached a provider.
80
- def totals(models, tools)
81
- priced = models.reject { |m| m[:cost].nil? }
82
- {
83
- count: models.sum { |m| m[:count] }, errors: models.sum { |m| m[:errors] },
84
- tool_count: tools.sum { |t| t[:count] }, tool_errors: tools.sum { |t| t[:errors] },
85
- input_tokens: models.sum { |m| m[:input_tokens] }, output_tokens: models.sum { |m| m[:output_tokens] },
86
- cost: priced.empty? ? nil : priced.sum { |m| m[:cost] }, unpriced: models.sum { |m| m[:unpriced] }
87
- }
88
- end
89
-
90
- def recent_rows(fields)
91
- scope = Telemetry::LlmCall.between(*window_range)
92
- scope = scope.where("model LIKE ?", "%#{Telemetry::LlmCall.sanitize_sql_like(fields['model'])}%") if fields["model"].present?
93
- scope = scope.where(provider: fields["provider"]) if fields["provider"].present?
94
- scope = scope.where(operation: fields["operation"]) if fields["operation"].present?
95
- scope = scope.where(workflow_id: fields["workflow"]) if fields["workflow"].present?
96
- scope = scope.where(finish_reason: fields["finish"]) if fields["finish"].present?
97
- # "attachments:image" is the question a document-heavy app actually asks.
98
- if fields["attachments"].present?
99
- scope = if fields["attachments"] == "any"
100
- scope.where.not(attachments: nil)
101
- else
102
- scope.where("attachment_types LIKE ?", "%#{Telemetry::LlmCall.sanitize_sql_like(fields['attachments'])}%")
103
- end
104
- end
105
- scope.recent.limit(200).map { |r| row_props(r) }
106
- end
107
-
108
- def row_props(call)
109
- { id: call.id, operation: call.operation, provider: call.provider, model: call.model,
110
- response_model: call.response_model, tool_name: call.tool_name, duration: call.duration_ms.round(1),
111
- status: call.status, error: call.error, streaming: call.streaming, message_count: call.message_count,
112
- tool_count: call.tool_count, input_tokens: call.input_tokens, output_tokens: call.output_tokens,
113
- cache_read_tokens: call.cache_read_tokens, cache_write_tokens: call.cache_write_tokens,
114
- thinking_tokens: call.thinking_tokens, cost: call.cost, cost_reported: call.cost_reported,
115
- workflow_id: call.workflow_id, workflow_name: call.workflow_name,
116
- workflow_step_name: call.workflow_step_name,
117
- finish_reason: call.finish_reason, provider_request_id: call.provider_request_id,
118
- tools: call.tools, attachments: call.attachments, attachment_types: call.attachment_types,
119
- attachment_names: call.attachment_names, tool_call_id: call.tool_call_id, params: call.params,
120
- prompt: call.prompt, completion: call.completion, occurred_at: call.occurred_at,
121
- execution_id: call.execution_id, execution_preview: call.execution_preview }
122
- end
123
- end
124
- end
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class LogsController < DashboardController
5
- def index
6
- from, to = window_range
7
- parsed = FilterQuery.parse(params[:q])
8
- page_data = nil
9
- page = lambda do
10
- page_data ||= telemetry do
11
- filtered = FilterQuery.apply(Telemetry::Log.all, resource: :logs, query: params[:q], from: from, to: to)
12
- rows, meta = Telemetry::CursorPage.call(filtered, cursor: params[:cursor], limit: params[:limit],
13
- context: telemetry_cursor_context(:logs))
14
- [ rows.map { |l| log_row(l) }, meta, Telemetry::Log.fts_snippets(rows.map(&:id), parsed[:text]) ]
15
- end
16
- end
17
- render inertia: {
18
- logs: InertiaRails.merge { page.call[0] }, pagination: -> { page.call[1] },
19
- counts: -> { level_counts(from, to) }, highlights: InertiaRails.deep_merge { page.call[2] }, q: params[:q].to_s
20
- }
21
- end
22
-
23
- private
24
-
25
- # The level facet counts the same rows the search does, except for the
26
- # selected level itself, so switching level never disagrees with the table.
27
- def level_counts(from, to)
28
- telemetry do
29
- FilterQuery.apply(Telemetry::Log.all, resource: :logs, query: params[:q], from: from, to: to,
30
- except: %w[level status]).group(:level).count
31
- end
32
- end
33
-
34
- def log_row(log)
35
- { id: log.id, level: log.level, message: log.message.first(2000), tags: log.tags, occurred_at: log.occurred_at,
36
- execution_id: log.execution_id, execution_source: log.execution_source, execution_preview: log.execution_preview,
37
- source: log.source, tenant: log.app_tenant, user_ref: log.user_ref, context: log.context }
38
- end
39
- end
40
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class MailsController < DashboardController
5
- def index
6
- fields = FilterQuery.parse(params[:q]).fetch(:fields)
7
- rows = telemetry do
8
- scope = Telemetry::Mail.between(*window_range)
9
- scope = scope.where(mailer: fields["mailer"]) if fields["mailer"].present?
10
- # Mail rows have no "kind" column; delivery_method (smtp/test/deliver_later
11
- # etc.) is the closest fit for a kind: filter token.
12
- scope = scope.where(delivery_method: fields["kind"]) if fields["kind"].present?
13
- scope.recent.limit(200).to_a
14
- end
15
- render inertia: { mailers: grouped("mail", limit: 100), series: series("mail"), q: params[:q].to_s,
16
- recent: rows.map { |m| { id: m.id, mailer: m.mailer, subject: m.subject, to: m.to, cc: m.cc, bcc: m.bcc, attachments: m.attachments, delivery_method: m.delivery_method, duration: m.duration_ms.round(1), failed: m.failed, occurred_at: m.occurred_at, execution_id: m.execution_id, execution_preview: m.execution_preview } } }
17
- end
18
- end
19
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class NotificationsController < DashboardController
5
- def index
6
- rows = telemetry { Telemetry::Notification.between(*window_range).recent.limit(200).to_a }
7
- render inertia: { groups: grouped("notification", limit: 100),
8
- recent: rows.map { |n| { id: n.id, notifier: n.notifier, delivery_method: n.delivery_method, duration: n.duration_ms.round(1), failed: n.failed, occurred_at: n.occurred_at, execution_id: n.execution_id, execution_preview: n.execution_preview } } }
9
- end
10
- end
11
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class OutgoingRequestsController < DashboardController
5
- def index
6
- fields = FilterQuery.parse(params[:q]).fetch(:fields)
7
- rows = telemetry do
8
- scope = Telemetry::OutgoingRequest.between(*window_range)
9
- scope = scope.where("host LIKE ?", "%#{Telemetry::OutgoingRequest.sanitize_sql_like(fields['host'])}%") if fields["host"].present?
10
- if (range = FilterQuery.status_range(fields["status"]))
11
- scope = scope.where(status_code: range)
12
- end
13
- scope.recent.limit(200).to_a
14
- end
15
- render inertia: { hosts: grouped("outgoing_request", limit: 100), series: series("outgoing_request"), q: params[:q].to_s,
16
- recent: rows.map { |r| { id: r.id, host: r.host, method: r.method, url: r.url, status_code: r.status_code, duration: r.duration_ms.round(1), request_size: r.request_size, response_size: r.response_size, error: r.error, source: r.source, occurred_at: r.occurred_at, execution_id: r.execution_id, execution_preview: r.execution_preview, response_body: r.response_body } } }
17
- end
18
- end
19
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class OverviewController < DashboardController
5
- def show
6
- requests = grouped("request", limit: 8, order: :p95)
7
- jobs = grouped("job_attempt", limit: 8)
8
- render inertia: {
9
- totals: { requests: summary_with_delta("request"), jobs: summary_with_delta("job_attempt") },
10
- request_series: series("request"),
11
- job_series: series("job_attempt"),
12
- slow_routes: requests,
13
- top_jobs: jobs,
14
- issues: environment.issues.open.recent.limit(8).map { |i| issue_row(i) },
15
- deploys: deploys_in_window,
16
- release_health: release_health,
17
- processes: telemetry { Telemetry::Process.recent.limit(20).group_by(&:server).transform_values { |ps| ps.first.slice(:role, :deploy, :ruby_version, :rails_version, :railwatch_version, :booted_at) } }
18
- }
19
- end
20
-
21
- private
22
-
23
- # Crash-free sessions for the release that is live now, over the page's
24
- # window. nil (and the stat is not rendered) until the environment reports
25
- # sessions at all.
26
- def release_health
27
- deploy = environment.deploys.recent.first or return nil
28
- health = telemetry { Telemetry::ReleaseHealth.for_deploy(deploy.deploy, *window_range) }
29
- return nil if health[:crash_free_sessions].nil?
30
- health.slice(:sessions, :crash_free_sessions).merge(deploy: deploy.deploy, ref: deploy.short_ref)
31
- end
32
-
33
- def issue_row(i)
34
- { id: i.id, key: i.key, title: i.title, kind: i.kind, status: i.status, priority: i.priority, occurrences: i.occurrences,
35
- affected_users: i.affected_users, last_seen_at: i.last_seen_at, culprit: i.culprit }
36
- end
37
- end
38
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class PeopleController < DashboardController
5
- def index
6
- from, to = window_range
7
- active = telemetry do
8
- Telemetry::Execution.requests.between(from, to).where.not(user_ref: nil).group(:user_ref)
9
- .pluck(:user_ref, Arel.sql("COUNT(*)"), Arel.sql("SUM(CASE WHEN status >= 500 THEN 1 ELSE 0 END)"), Arel.sql("MAX(occurred_at)"))
10
- .map { |ref, c, e, last| { ref: ref, requests: c, errors: e, last_seen_at: last } }.sort_by { |r| -r[:requests] }.first(200)
11
- end
12
- people = telemetry { Telemetry::Person.where(ref: active.map { |a| a[:ref] }).index_by(&:ref) }
13
- render inertia: { people: active.map { |a| a.merge(name: people[a[:ref]]&.display_name, email: people[a[:ref]]&.email, tenant: people[a[:ref]]&.app_tenant) } }
14
- end
15
-
16
- def show
17
- ref = params[:id]
18
- person = telemetry { Telemetry::Person.find_by(ref: ref) }
19
- executions = telemetry { Telemetry::Execution.where(user_ref: ref).between(*window_range).recent.limit(100).map { |r| { execution_id: r.execution_id, kind: r.kind, name: r.name, status: r.status, outcome: r.outcome, duration: r.duration_ms.round(1), occurred_at: r.occurred_at, exception_preview: r.exception_preview } } }
20
- exception_groups = telemetry { Telemetry::Exception.where(user_ref: ref).between(*window_range).group(:group_hash).count }
21
- issues = environment.issues.where(group_hash: exception_groups.keys).map { |i| { id: i.id, key: i.key, title: i.title, status: i.status, count: exception_groups[i.group_hash] } }
22
- render inertia: { person: person && { person_ref: person.ref, name: person.display_name, email: person.email, tenant: person.app_tenant, first_seen_at: person.first_seen_at, last_seen_at: person.last_seen_at }, person_ref: ref, executions: executions, issues: issues }
23
- end
24
- end
25
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class ProcessesController < DashboardController
5
- def index
6
- live_since = Telemetry::HealthSample::LIVE_WINDOW.ago
7
- samples, series, queues, live_servers, processes = telemetry do
8
- live = Telemetry::HealthSample.live(live_since).to_a
9
- [ live, Telemetry::HealthSample.series(*window_range, bucket: Telemetry::Aggregations::STEPS.fetch(step_key)), Telemetry::HealthSample.queue_depths(live),
10
- live.map(&:server) | Telemetry::Execution.servers_since(live_since),
11
- Telemetry::Process.recent.limit(200).to_a ]
12
- end
13
- render inertia: { samples: samples.map { |s| sample_row(s) }, series: series, queues: queues,
14
- silent_servers: environment.expected_servers - live_servers.compact,
15
- processes: processes.map { |p| process_row(p) } }
16
- end
17
-
18
- private
19
-
20
- def sample_row(s)
21
- { id: s.id, server: s.server, role: s.role, pid: s.pid, deploy: s.deploy, sampled_at: s.sampled_at,
22
- threads_busy: s.threads_busy, threads_max: s.threads_max, backlog: s.backlog,
23
- pool_busy: s.pool_busy, pool_size: s.pool_size, pool_waiting: s.pool_waiting,
24
- queue_depth: s.queue_depth, queue_latency: s.queue_latency && (s.queue_latency / 1000.0).round(1), memory: s.memory }
25
- end
26
-
27
- def process_row(p)
28
- { id: p.id, booted_at: p.booted_at, pid: p.pid, role: p.role, server: p.server, deploy: p.deploy,
29
- ruby_version: p.ruby_version, rails_version: p.rails_version, railwatch_version: p.railwatch_version,
30
- boot_seconds: p.boot_seconds, detail: p.detail }
31
- end
32
- end
33
- end