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,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class ProfilesController < DashboardController
5
- # Profiles are never rolled up (they are opt-in and sampled, so a rollup
6
- # row would be noise), which means every number on these pages comes from
7
- # the raw table. This caps how many rows the grouping pass reads; the
8
- # StatStrip totals still come from SQL aggregates over the whole window.
9
- SCAN_LIMIT = 5_000
10
-
11
- def index
12
- from, to = window_range
13
- rows, count, avg_samples, profiled, executions = telemetry do
14
- scope = Telemetry::Profile.between(from, to)
15
- [ scope.recent.limit(SCAN_LIMIT).select(:id, :group_hash, :execution_preview, :duration, :samples, :occurred_at).to_a,
16
- scope.count, scope.average(:samples), scope.distinct.count(:execution_id), executions_in(from, to) ]
17
- end
18
- render inertia: { profiles: groups(rows),
19
- summary: { profiles: count, executions: executions, profiled: profiled, avg_samples: avg_samples.to_f.round(0) } }
20
- end
21
-
22
- def show
23
- profile = telemetry { Telemetry::Profile.find(params[:id]) }
24
- collapsed, truncated = profile.collapsed_capped
25
- render inertia: { profile: profile_row(profile), collapsed: collapsed, truncated: truncated }
26
- rescue Telemetry::BoundedGzip::Error
27
- head :unprocessable_content
28
- end
29
-
30
- private
31
-
32
- # How many executions the window holds, as the profiled share's
33
- # denominator. Every kind is rolled up, so whole hours come from a few
34
- # hundred rollup rows rather than counting every execution (1.2 s over a
35
- # week of the platform's own). A rollup holds its whole hour, so the
36
- # partial hours at either end of the window are counted from the rows.
37
- def executions_in(from, to)
38
- first = from.beginning_of_hour == from ? from : from.beginning_of_hour + 1.hour
39
- last = to.beginning_of_hour
40
- return Telemetry::Execution.between(from, to).count if first >= last
41
-
42
- Telemetry::Execution.where(occurred_at: from...first).count +
43
- Telemetry::Rollup.for_type(Telemetry::Execution::KINDS).where(bucket: first...last).sum(:count) +
44
- Telemetry::Execution.where(occurred_at: last..to).count
45
- end
46
-
47
- # Rows arrive newest-first, so the head of each group is the profile the
48
- # page links to.
49
- def groups(rows)
50
- rows.group_by(&:group_hash).map { |group_hash, group|
51
- newest = group.first
52
- { group_hash: group_hash, name: newest.execution_preview, profile_id: newest.id, count: group.size,
53
- avg_duration: (group.sum { |row| row.duration.to_i } / group.size.to_f / 1000.0).round(2),
54
- max_samples: group.filter_map(&:samples).max, last_seen_at: newest.occurred_at }
55
- }.sort_by { |g| -g[:count] }.first(100)
56
- end
57
-
58
- def profile_row(row)
59
- { id: row.id, profiler: row.profiler, mode: row.mode, interval: row.interval, duration: row.duration_ms.round(2),
60
- samples: row.samples, stacks_bytes: row.stacks_bytes, group_hash: row.group_hash, execution_id: row.execution_id,
61
- execution_preview: row.execution_preview, execution_source: row.execution_source, occurred_at: row.occurred_at,
62
- deploy: row.deploy, server: row.server }
63
- end
64
- end
65
- end
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class QueriesController < DashboardController
5
- def index
6
- from, to = window_range
7
- page_data = nil
8
- page = lambda do
9
- page_data ||= telemetry do
10
- scope = FilterQuery.apply(Telemetry::Query.with_sql, resource: :queries, query: params[:q], from: from, to: to)
11
- rows, meta = Telemetry::CursorPage.call(scope, cursor: params[:cursor], limit: params[:limit], order: :slowest,
12
- context: telemetry_cursor_context(:queries))
13
- [ rows.map { |q| query_row(q) }, meta ]
14
- end
15
- end
16
- render inertia: {
17
- queries: -> { grouped("query", limit: 200, order: params[:sort], dir: params[:dir]) },
18
- n_plus_ones: -> { n_plus_ones(from, to) }, slowest: InertiaRails.merge { page.call.first },
19
- pagination: -> { page.call.last }, q: params[:q].to_s, summary: -> { summary_with_delta("query") },
20
- sort: params[:sort] || "count", dir: params[:dir] || "desc"
21
- }
22
- end
23
-
24
- def show
25
- group_hash = params[:id]
26
- rows = telemetry { Telemetry::Query.where(group_hash: group_hash).between(*window_range).recent.limit(100).with_sql.to_a }
27
- summary = telemetry { Telemetry::Rollup.summarize(Telemetry::Rollup.for_type("query").where(group_hash: group_hash).between(*window_range)) }
28
- sources = rows.filter_map(&:source).tally.sort_by { |_s, c| -c }.first(10)
29
- callers = rows.map(&:execution_preview).compact.tally.sort_by { |_s, c| -c }.first(10)
30
- render inertia: { group_hash: group_hash, sql: rows.first&.sql, summary: summary, series: series("query", group_hash: group_hash),
31
- sources: sources, callers: callers, roles: rows.filter_map(&:role).tally.sort_by { |_r, c| -c },
32
- explain: explain_sample(group_hash), samples: rows.first(50).map { |q| query_row(q) } }
33
- end
34
-
35
- private
36
-
37
- def n_plus_ones(from, to)
38
- telemetry do
39
- Telemetry::NPlusOne.between(from, to).group(:group_hash).pluck(:group_hash, Arel.sql("COUNT(*)"), Arel.sql("MAX(sql)"), Arel.sql("MAX(source)"), Arel.sql("MAX(count)"))
40
- .map { |g, c, sql, src, mx| { group_hash: g, occurrences: c, sql: sql, source: src, max_count: mx, suggestion: Telemetry::NPlusOne.new(sql: sql, source: src).suggestion } }
41
- .sort_by { |r| -r[:occurrences] }.first(50)
42
- end
43
- end
44
-
45
- # The newest sample in the group that the gem attached a query plan to.
46
- # Only slow queries get one (RAILWATCH_CAPTURE_QUERY_EXPLAIN), so this is
47
- # usually a different row than the newest sample overall.
48
- def explain_sample(group_hash)
49
- q = telemetry { Telemetry::Query.where(group_hash: group_hash).between(*window_range).where.not(explain: nil).recent.with_sql.first }
50
- return nil if q.nil?
51
- { plan: q.explain, occurred_at: q.occurred_at, duration: q.duration_ms.round(3), execution_id: q.execution_id }
52
- end
53
-
54
- def query_row(q)
55
- { id: q.id, sql: q.sql.first(300), duration: q.duration_ms.round(3), occurred_at: q.occurred_at, execution_id: q.execution_id,
56
- execution_preview: q.execution_preview, source: q.source, connection: q.connection, row_count: q.row_count, group_hash: q.group_hash }
57
- end
58
- end
59
- end
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Sentry-style release health: sessions per deploy, and the crash-free rates
4
- # read off them. The release is the deploy string the gem stamps on every
5
- # record it ships, so `show`'s :id is that string rather than a Deploy id.
6
- module Railwatch
7
- class ReleasesController < DashboardController
8
- SESSIONS_SHOWN = 50
9
- RELEASES_SHOWN = 50
10
- # SQLite reads the deploy back out of an issue's sample payload, which is
11
- # the release its latest occurrence came from.
12
- SAMPLE_DEPLOY = "json_extract(sample, '$.deploy') = ?"
13
-
14
- def index
15
- from, to = window_range
16
- deploys = environment.deploys.between(from, to).recent.limit(RELEASES_SHOWN).to_a
17
- new_issues = new_issue_counts(deploys, to)
18
- summary, series, adoption, health = telemetry do
19
- [ Telemetry::ReleaseHealth.for_deploy(nil, from, to),
20
- Telemetry::ReleaseHealth.series(nil, from, to, step: step_key),
21
- Telemetry::ReleaseHealth.adoption(from, to),
22
- deploys.to_h { |d| [ d.deploy, Telemetry::ReleaseHealth.for_deploy(d.deploy, from, to) ] } ]
23
- end
24
- releases = deploys.map do |d|
25
- health.fetch(d.deploy).merge(deploy: d.deploy, ref: d.short_ref, name: d.name, deployed_at: d.deployed_at,
26
- adoption: adoption[d.deploy] || 0.0, new_issues: new_issues[d.id].to_i)
27
- end
28
- render inertia: { releases: releases, series: series,
29
- summary: summary.merge(releases: adoption.count { |_deploy, share| share.positive? }) }
30
- end
31
-
32
- def show
33
- release = params[:id]
34
- deploy = environment.deploys.find_by(deploy: release)
35
- previous = deploy&.previous_deploy
36
- from, to = deploy ? deploy.window : window_range
37
- data = telemetry do
38
- { summary: Telemetry::ReleaseHealth.for_deploy(release, from, to),
39
- previous_summary: previous && Telemetry::ReleaseHealth.for_deploy(previous.deploy, *previous.window),
40
- series: Telemetry::ReleaseHealth.series(release, from, to, step: step_key),
41
- sessions: Telemetry::Session.where(deploy: release).recent.limit(SESSIONS_SHOWN).map { |s| session_row(s) } }
42
- end
43
- render inertia: data.merge(
44
- release: { deploy: release, ref: deploy&.short_ref || release.first(12), name: deploy&.name, deploy_id: deploy&.id,
45
- deployed_at: deploy&.deployed_at, previous_ref: previous&.short_ref },
46
- range: { from: from.iso8601, to: to.iso8601 },
47
- new_issues: issue_rows(environment.issues.where(first_seen_at: from..to).or(environment.issues.where(SAMPLE_DEPLOY, release))),
48
- resolved_issues: issue_rows(environment.issues.where(resolved_in_deploy: release)))
49
- end
50
-
51
- private
52
-
53
- def session_row(s)
54
- { id: s.id, session_id: s.session_id, source: s.source, status: s.status, user_ref: s.user_ref,
55
- duration: s.duration_ms, requests: s.requests, visits: s.visits, errors: s.error_count, occurred_at: s.occurred_at,
56
- started_at: s.started_at }
57
- end
58
-
59
- def issue_rows(scope)
60
- scope.recent.limit(50).map { |i| { id: i.id, key: i.key, title: i.title, status: i.status } }
61
- end
62
-
63
- # Issues first seen while each release was live -- one query for the table,
64
- # not one per row. A release is live until the next one deploys.
65
- def new_issue_counts(deploys, to)
66
- return {} if deploys.empty?
67
-
68
- ordered = deploys.sort_by(&:deployed_at)
69
- windows = ordered.each_cons(2).to_h { |release, following| [ release.id, release.deployed_at...following.deployed_at ] }
70
- windows[ordered.last.id] = ordered.last.deployed_at...to
71
- first_seen = environment.issues.where(first_seen_at: ordered.first.deployed_at..to).pluck(:first_seen_at)
72
- windows.transform_values { |window| first_seen.count { |at| window.cover?(at) } }
73
- end
74
- end
75
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class RequestsController < DashboardController
5
- def index
6
- routes = grouped("request", limit: 200, order: params[:sort], dir: params[:dir])
7
- routes = FilterQuery.filter_routes(routes, params[:q])
8
- render inertia: { routes: routes, series: series("request"), deploys: deploys_in_window,
9
- sort: params[:sort] || "count", dir: params[:dir] || "desc", q: params[:q].to_s }
10
- end
11
-
12
- # All requests to one route in the window.
13
- def route
14
- group_hash = params[:group_hash]
15
- rows = telemetry { Telemetry::Execution.requests.where(group_hash: group_hash).between(*window_range).recent.limit(200).to_a }
16
- render inertia: "requests/route", props: {
17
- route: rows.first&.name || telemetry { Telemetry::Rollup.for_type("request").where(group_hash: group_hash).pick(:name) },
18
- group_hash: group_hash, summary: summary_with_delta("request", group_hash: group_hash),
19
- series: series("request", group_hash: group_hash), deploys: deploys_in_window,
20
- requests: rows.map { |r| execution_row(r) }
21
- }
22
- end
23
-
24
- def show
25
- exe = telemetry { Telemetry::Execution.find_by!(execution_id: params[:id]) }
26
- render inertia: "executions/show", props: ExecutionPresenter.new(exe, environment).props
27
- end
28
-
29
- private
30
-
31
- def execution_row(r)
32
- { execution_id: r.execution_id, name: r.name, status: r.status, duration: r.duration_ms.round(2), occurred_at: r.occurred_at,
33
- user_ref: r.user_ref, tenant: r.app_tenant, exception_preview: r.exception_preview, inertia_component: r.inertia_component,
34
- queries: r.counters["queries"], deploy: r.deploy }
35
- end
36
- end
37
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class SavedViewsController < DashboardController
5
- def create
6
- view = environment.saved_views.new(create_params.merge(user: Viewer.user))
7
- if view.save
8
- redirect_back fallback_location: fallback, notice: "View saved"
9
- else
10
- redirect_back fallback_location: fallback, inertia: { errors: view.errors }
11
- end
12
- end
13
-
14
- def update
15
- view = environment.saved_views.find(params[:id])
16
- return head :forbidden unless editable?(view)
17
- if view.update(update_params)
18
- redirect_back fallback_location: fallback, notice: "View updated"
19
- else
20
- redirect_back fallback_location: fallback, inertia: { errors: view.errors }
21
- end
22
- end
23
-
24
- def destroy
25
- view = environment.saved_views.find(params[:id])
26
- return head :forbidden unless editable?(view)
27
- view.destroy
28
- redirect_back fallback_location: fallback, notice: "View deleted"
29
- end
30
-
31
- private
32
-
33
- # Its owner can change a view, and only its owner. The platform also lets
34
- # an account admin, so a shared view does not outlive the person who made
35
- # it; embedded mode has no account and no admin, and `|| true` here let
36
- # every dashboard user edit and delete everyone else's views. An app whose
37
- # dashboard_user resolver returns one identity for everybody is unaffected
38
- # either way: one owner, one editor.
39
- def editable?(view)
40
- view.viewer_id.to_s == Viewer.user.id.to_s
41
- end
42
-
43
- def fallback = application_environment_overview_path(application, environment)
44
-
45
- def create_params
46
- params.permit(:name, :page, :query, :window, :pinned, :shared, params: {})
47
- end
48
-
49
- def update_params
50
- params.permit(:name, :pinned, :shared)
51
- end
52
- end
53
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class ScheduledTasksController < DashboardController
5
- include TelemetryIdentity
6
-
7
- def index
8
- from, to = window_range
9
- tasks = telemetry do
10
- rows = Telemetry::Execution.scheduled.between(from, to).group(:task_key)
11
- .pluck(:task_key, Arel.sql("COUNT(*)"), Arel.sql("SUM(CASE WHEN outcome = 'failed' THEN 1 ELSE 0 END)"), Arel.sql("AVG(duration)"), Arel.sql("MAX(occurred_at)"))
12
- schedules = Telemetry::Execution.latest_schedules(rows.map(&:first))
13
- rows.map { |k, c, f, d, last| { task_key: k, runs: c, failed: f, avg: (d.to_f / 1000.0).round(1), last_run_at: last, schedule: schedules[k] } }
14
- end
15
- tasks.each { |t| t[:next_run_at] = next_run(t[:schedule]) }
16
- recent = telemetry do
17
- rows = Telemetry::Execution.scheduled.between(from, to).recent.limit(100).to_a
18
- people = origin_people(rows)
19
- rows.map do |row|
20
- { execution_id: row.execution_id, task_key: row.task_key, name: row.name, outcome: row.outcome,
21
- duration: row.duration_ms.round(1), occurred_at: row.occurred_at,
22
- exception_preview: row.exception_preview }.merge(origin_identity(row, people))
23
- end
24
- end
25
- render inertia: { tasks: tasks, runs: recent, missed: environment.issues.open.where("group_hash LIKE 'missed:%'").map { |i| { key: i.key, title: i.title, id: i.id } } }
26
- end
27
-
28
- def show
29
- exe = telemetry { Telemetry::Execution.find_by!(execution_id: params[:id]) }
30
- render inertia: "executions/show", props: ExecutionPresenter.new(exe, environment).props
31
- end
32
-
33
- private
34
-
35
- # Solid Queue schedules are cron or natural language ("every 5 minutes"),
36
- # both of which fugit parses; anything it can't parse has no next run.
37
- def next_run(schedule)
38
- return nil if schedule.blank?
39
- require "fugit"
40
- Fugit.parse(schedule)&.next_time&.to_t
41
- rescue StandardError
42
- nil
43
- end
44
- end
45
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class SpansController < DashboardController
5
- def index
6
- spans = grouped("span", limit: 200, order: params[:sort], dir: params[:dir])
7
- spans = apply_filters(spans, FilterQuery.parse(params[:q]).fetch(:fields))
8
- render inertia: { spans: spans, series: series("span"), summary: summary_with_delta("span"),
9
- sort: params[:sort] || "count", dir: params[:dir] || "desc", q: params[:q].to_s }
10
- end
11
-
12
- def show
13
- group_hash = params[:id]
14
- rows = telemetry { Telemetry::Span.where(group_hash: group_hash).between(*window_range).recent.limit(200).to_a }
15
- render inertia: {
16
- group_hash: group_hash,
17
- name: rows.first&.name || telemetry { Telemetry::Rollup.for_type("span").where(group_hash: group_hash).pick(:name) },
18
- summary: summary_with_delta("span", group_hash: group_hash), series: series("span", group_hash: group_hash),
19
- facets: attribute_facets(rows), samples: rows.first(50).map { |s| span_row(s) }
20
- }
21
- end
22
-
23
- private
24
-
25
- # Rollup rows for "span" carry the span name and a failure count, so
26
- # status:failed is "this group failed at least once in the window" rather
27
- # than a per-span lookup.
28
- def apply_filters(spans, fields)
29
- spans = spans.select { |s| s[:name].to_s.include?(fields["name"]) } if fields["name"].present?
30
- spans = spans.select { |s| s[:errors].positive? } if fields["status"] == "failed"
31
- spans
32
- end
33
-
34
- # Which attribute keys these spans carry, and the most common values for
35
- # each -- the span equivalent of the query page's "called from" breakdown.
36
- def attribute_facets(rows)
37
- rows.flat_map { |r| r.payload.to_a }.group_by(&:first).map { |key, pairs|
38
- { key: key, count: pairs.size, values: pairs.map { |_k, value| value.to_s }.tally.sort_by { |_v, c| -c }.first(5) }
39
- }.sort_by { |facet| -facet[:count] }.first(10)
40
- end
41
-
42
- def span_row(s)
43
- { id: s.id, name: s.name, duration: s.duration_ms.round(3), status: s.status, attributes: s.payload,
44
- occurred_at: s.occurred_at, execution_id: s.execution_id, execution_preview: s.execution_preview }
45
- end
46
- end
47
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class StorageOpsController < DashboardController
5
- def index
6
- from, to = window_range
7
- ops = telemetry do
8
- Telemetry::StorageOp.between(from, to).group(:service, :op).pluck(:service, :op, Arel.sql("COUNT(*)"), Arel.sql("AVG(duration)"), Arel.sql("MAX(duration)"))
9
- .map { |s, o, c, avg, mx| { service: s, op: o, count: c, avg: (avg.to_f / 1000.0).round(1), max: (mx.to_f / 1000.0).round(1) } }
10
- end
11
- recent = telemetry { Telemetry::StorageOp.between(from, to).recent.limit(100).map { |r| { id: r.id, service: r.service, op: r.op, key: r.key, duration: r.duration_ms.round(1), occurred_at: r.occurred_at, execution_id: r.execution_id, execution_preview: r.execution_preview } } }
12
- render inertia: { ops: ops, recent: recent }
13
- end
14
- end
15
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class TenantsController < DashboardController
5
- def index
6
- from, to = window_range
7
- tenants, summary = telemetry do
8
- rows = Telemetry::Tenant.index(from, to, q: params[:q], sort: params[:sort], dir: params[:dir])
9
- [ rows, Telemetry::Tenant.overview(rows, from, to) ]
10
- end
11
- render inertia: { tenants: tenants, summary: summary,
12
- sort: params[:sort] || "requests", dir: params[:dir] || "desc", q: params[:q].to_s }
13
- end
14
-
15
- # :id is the tenant string. Rails reads a dot in a path segment as a format
16
- # separator, so tenants like "acme.co" arrive percent-encoded (see the
17
- # tenants index page's tenantPath).
18
- def show
19
- tenant = params[:id]
20
- from, to = window_range
21
- data = telemetry do
22
- { tenant: tenant, summary: { current: Telemetry::Tenant.summary(tenant, from, to),
23
- previous: Telemetry::Tenant.summary(tenant, *window.previous.range) },
24
- series: Telemetry::Tenant.series(tenant, from, to, step: step_key), routes: Telemetry::Tenant.routes(tenant, from, to),
25
- jobs: Telemetry::Tenant.job_classes(tenant, from, to), exceptions: Telemetry::Tenant.exceptions(tenant, from, to),
26
- people: Telemetry::Tenant.people(tenant), recent_requests: Telemetry::Tenant.recent_requests(tenant, from, to) }
27
- end
28
- issues = environment.issues.where(group_hash: data[:exceptions].map { |e| e[:group_hash] }.uniq).index_by(&:group_hash)
29
- render inertia: data.merge(exceptions: data[:exceptions].map { |e| with_issue(e, issues) }, links: links(tenant))
30
- end
31
-
32
- private
33
-
34
- def with_issue(exception, issues)
35
- issue = issues[exception[:group_hash]]
36
- exception.merge(issue_id: issue&.id, issue_key: issue&.key)
37
- end
38
-
39
- def links(tenant)
40
- { logs: application_environment_logs_path(application, environment, window: window_key, q: "tenant:#{tenant}"),
41
- people: application_environment_people_path(application, environment, window: window_key) }
42
- end
43
- end
44
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class ThresholdsController < DashboardController
5
- def index
6
- render inertia: { thresholds: environment.thresholds.order(:target_kind, :target).map { |t| t.slice(:id, :target_kind, :target, :metric, :limit, :window_minutes).merge(description: t.description) },
7
- routes: telemetry { Telemetry::Rollup.for_type("request").where("bucket > ?", 7.days.ago).distinct.pluck(:name).sort },
8
- jobs: telemetry { Telemetry::Rollup.for_type("job_attempt").where("bucket > ?", 7.days.ago).distinct.pluck(:name).sort },
9
- kinds: Threshold::TARGET_KINDS, metrics: Threshold::METRICS,
10
- anomaly_rules: environment.anomaly_rules.order(:target_kind, :target).map { |r| r.slice(:id, :target_kind, :target, :metric, :deviation, :window_minutes, :baseline_days, :enabled, :last_fired_at).merge(description: r.description) },
11
- # Spend and tokens only exist on llm_calls, so the form
12
- # narrows with the kind rather than offering a rule that
13
- # could never fire.
14
- metrics_for_kind: Threshold::TARGET_KINDS.index_with { |k| Threshold.metrics_for(k) },
15
- units: Threshold::UNITS,
16
- llm_models: telemetry { Telemetry::Rollup.for_type("llm_call").where("bucket > ?", 7.days.ago).distinct.pluck(:name).compact.sort },
17
- anomaly_target_kinds: AnomalyRule::TARGET_KINDS, anomaly_metrics: AnomalyRule::METRICS,
18
- anomaly_metrics_for_kind: AnomalyRule::TARGET_KINDS.index_with { |k| AnomalyRule.metrics_for(k) } }
19
- end
20
-
21
- def create
22
- threshold = environment.thresholds.new(threshold_params)
23
- if threshold.save
24
- redirect_to application_environment_thresholds_path(application, environment), notice: "Threshold added"
25
- else
26
- redirect_to application_environment_thresholds_path(application, environment), inertia: { errors: threshold.errors }
27
- end
28
- end
29
-
30
- def update
31
- threshold = environment.thresholds.find(params[:id])
32
- threshold.update(threshold_params)
33
- redirect_to application_environment_thresholds_path(application, environment), notice: "Threshold updated"
34
- end
35
-
36
- def destroy
37
- environment.thresholds.find(params[:id]).destroy
38
- redirect_to application_environment_thresholds_path(application, environment), notice: "Threshold removed"
39
- end
40
-
41
- private
42
-
43
- def threshold_params
44
- params.permit(:target_kind, :target, :metric, :limit, :window_minutes)
45
- end
46
- end
47
- end
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # One distributed trace: every execution that shares a trace_id, nested by
4
- # the parent_id the gem propagates (the enqueuing execution's id for jobs,
5
- # the W3C traceparent parent-id for HTTP hops).
6
- module Railwatch
7
- class TracesController < DashboardController
8
- LIMIT = 200
9
-
10
- def show
11
- trace_id = params[:trace_id]
12
- executions, outgoing = telemetry do
13
- rows = Telemetry::Execution.where(trace_id: trace_id).order(:occurred_at).limit(LIMIT).to_a
14
- [ rows, Telemetry::OutgoingRequest.where(execution_id: rows.map(&:execution_id)).order(:occurred_at).group_by(&:execution_id) ]
15
- end
16
- raise ActiveRecord::RecordNotFound if executions.empty?
17
-
18
- @start = executions.first.occurred_at
19
- @outgoing = outgoing
20
- render inertia: { trace_id: trace_id, roots: tree(executions),
21
- span_count: executions.size + outgoing.values.sum(&:size),
22
- services: executions.filter_map(&:server).uniq, duration: total_duration(executions) }
23
- end
24
-
25
- private
26
-
27
- # An execution's parent is the one whose execution_id matches its
28
- # parent_id outright (a job enqueued by that execution) or whose id
29
- # matches once dashes are dropped and it is cut to the 16 hex chars a
30
- # W3C traceparent carries (an incoming HTTP hop). Anything whose parent
31
- # is missing -- the trace's entry point, or a hop whose caller was not
32
- # reported -- is a root.
33
- def tree(executions)
34
- by_id = executions.index_by(&:execution_id)
35
- by_short_id = executions.index_by { |e| e.execution_id.to_s.delete("-").first(16) }
36
- children = Hash.new { |hash, key| hash[key] = [] }
37
- roots = []
38
- executions.each do |exe|
39
- parent = exe.parent_id.presence && (by_id[exe.parent_id] || by_short_id[exe.parent_id])
40
- parent = nil if parent && parent.execution_id == exe.execution_id
41
- parent ? children[parent.execution_id] << exe : roots << exe
42
- end
43
- roots.map { |exe| node(exe, children) }
44
- end
45
-
46
- def node(exe, children)
47
- { execution: execution_row(exe), children: children[exe.execution_id].map { |child| node(child, children) } }
48
- end
49
-
50
- def execution_row(exe)
51
- { execution_id: exe.execution_id, kind: exe.kind, name: exe.name, status: exe.status, outcome: exe.outcome,
52
- parent_id: exe.parent_id, server: exe.server, occurred_at: exe.occurred_at,
53
- duration: exe.duration_ms.round(2), offset: offset(exe.occurred_at),
54
- outgoing: @outgoing.fetch(exe.execution_id, []).map { |o| outgoing_row(o) } }
55
- end
56
-
57
- def outgoing_row(o)
58
- { id: o.id, method: o.method, url: o.url.to_s.first(200), status_code: o.status_code,
59
- duration: o.duration_ms&.round(2), offset: offset(o.occurred_at) }
60
- end
61
-
62
- def offset(time)
63
- ((time - @start) * 1000.0).round(2)
64
- end
65
-
66
- # Wall-clock span of the whole trace: the last thing to finish, minus the
67
- # first thing to start.
68
- def total_duration(executions)
69
- executions.map { |e| offset(e.occurred_at) + e.duration_ms }.max.round(2)
70
- end
71
- end
72
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class TransactionsController < DashboardController
5
- def index
6
- from, to = window_range
7
- rows = grouped("transaction", limit: 200, order: params[:sort], dir: params[:dir])
8
- recent = telemetry { Telemetry::Transaction.between(from, to).recent.limit(50).map { |t| transaction_row(t) } }
9
- render inertia: { transactions: rows, series: series("transaction"), recent: recent,
10
- sort: params[:sort] || "count", dir: params[:dir] || "desc" }
11
- end
12
-
13
- private
14
-
15
- def transaction_row(t)
16
- { id: t.id, outcome: t.outcome, connection: t.connection, duration: t.duration_ms.round(3), statement_count: t.statement_count, occurred_at: t.occurred_at,
17
- execution_id: t.execution_id, execution_source: t.execution_source, execution_preview: t.execution_preview,
18
- group_hash: t.group_hash }
19
- end
20
- end
21
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Railwatch
4
- class ViewRendersController < DashboardController
5
- def index
6
- from, to = window_range
7
- fields = FilterQuery.parse(params[:q]).fetch(:fields)
8
- rows = grouped("view_render", limit: 200, order: params[:sort], dir: params[:dir])
9
- rows = rows.select { |r| r[:name].include?(fields["template"]) } if fields["template"].present?
10
- kinds = telemetry do
11
- Telemetry::Rollup.for_type("view_render").between(from, to).to_a.group_by(&:group_hash)
12
- .transform_values { |group_rows| group_rows.max_by(&:bucket).extra["kind"] }
13
- end
14
- rows.each { |r| r[:kind] = kinds[r[:group_hash]] }
15
- slowest = telemetry { Telemetry::ViewRender.between(from, to).order(duration: :desc).limit(30).map { |v| render_row(v) } }
16
- render inertia: { renders: rows, series: series("view_render"), slowest: slowest,
17
- sort: params[:sort] || "count", dir: params[:dir] || "desc", q: params[:q].to_s }
18
- end
19
-
20
- private
21
-
22
- def render_row(v)
23
- { id: v.id, identifier: v.identifier, kind: v.kind, layout: v.layout, duration: v.duration_ms.round(3),
24
- occurred_at: v.occurred_at, execution_id: v.execution_id, execution_source: v.execution_source,
25
- execution_preview: v.execution_preview, group_hash: v.group_hash }
26
- end
27
- end
28
- end