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,145 +0,0 @@
1
- import { Link, router, usePage } from "@inertiajs/react"
2
- import { Check, ChevronsUpDown, Plus } from "lucide-react"
3
-
4
- import { LiveDot } from "@/components/railwatch/live-dot"
5
- import {
6
- DropdownMenu,
7
- DropdownMenuContent,
8
- DropdownMenuItem,
9
- DropdownMenuLabel,
10
- DropdownMenuSeparator,
11
- DropdownMenuTrigger,
12
- } from "@/components/ui/dropdown-menu"
13
- import {
14
- SidebarMenu,
15
- SidebarMenuButton,
16
- SidebarMenuItem,
17
- } from "@/components/ui/sidebar"
18
- import { cn } from "@/lib/utils"
19
- import {
20
- applicationEnvironmentOverviewPath,
21
- newApplicationPath,
22
- } from "@/routes"
23
- import type { SharedProps } from "@/types"
24
-
25
- function Monogram({ name, className }: { name: string; className?: string }) {
26
- return (
27
- <span
28
- className={cn(
29
- "bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-md font-mono text-xs font-semibold",
30
- className,
31
- )}
32
- >
33
- {name.slice(0, 2).toUpperCase()}
34
- </span>
35
- )
36
- }
37
-
38
- // Application · environment switcher at the top of the sidebar. Mirrors the
39
- // Nightwatch app card: monogram, application name, environment name, and
40
- // a dropdown listing every environment across the account's applications.
41
- export function EnvSwitcher() {
42
- const { applications, environment, embedded } = usePage<SharedProps>().props
43
- if (applications.length === 0) return null
44
- // Account-level pages (dashboard, issues) have no environment; fall back to
45
- // the first application so the card still names something real.
46
- const currentApp =
47
- applications.find((a) => a.id === environment?.application_id) ??
48
- applications[0]
49
- // Embedded: one application, one environment, nothing to switch to. The
50
- // card stays as a header (which app, which environment) with no menu.
51
- if (embedded) {
52
- const env = environment ?? currentApp.environments[0]
53
- return (
54
- <SidebarMenu>
55
- <SidebarMenuItem>
56
- <SidebarMenuButton
57
- size="lg"
58
- asChild
59
- className="border-0 py-2 pr-3 pl-2 hover:bg-transparent"
60
- >
61
- <Link
62
- href={applicationEnvironmentOverviewPath(currentApp.id, env.id)}
63
- prefetch
64
- >
65
- <Monogram name={currentApp.name} />
66
- <span className="grid flex-1 text-left leading-tight">
67
- <span className="truncate text-sm font-semibold">
68
- {currentApp.name}
69
- </span>
70
- <span className="text-muted-foreground truncate text-xs">
71
- {env.name}
72
- </span>
73
- </span>
74
- </Link>
75
- </SidebarMenuButton>
76
- </SidebarMenuItem>
77
- </SidebarMenu>
78
- )
79
- }
80
- return (
81
- <SidebarMenu>
82
- <SidebarMenuItem>
83
- <DropdownMenu>
84
- <DropdownMenuTrigger asChild>
85
- <SidebarMenuButton
86
- size="lg"
87
- className="data-[state=open]:bg-sidebar-accent hover:bg-sidebar-accent border-0 py-2 pr-3 pl-2"
88
- >
89
- <Monogram name={currentApp.name} />
90
- <span className="grid flex-1 text-left leading-tight">
91
- <span className="truncate text-sm font-semibold">
92
- {currentApp.name}
93
- </span>
94
- <span className="text-muted-foreground truncate text-xs">
95
- {environment
96
- ? environment.name
97
- : `${currentApp.environments.length} environment${currentApp.environments.length === 1 ? "" : "s"}`}
98
- </span>
99
- </span>
100
- <ChevronsUpDown className="size-3.5 opacity-60" />
101
- </SidebarMenuButton>
102
- </DropdownMenuTrigger>
103
- <DropdownMenuContent
104
- align="start"
105
- className="w-(--radix-dropdown-menu-trigger-width) min-w-64"
106
- >
107
- {applications.map((app) => (
108
- <div key={app.id}>
109
- <DropdownMenuLabel className="label-caps">
110
- {app.name}
111
- </DropdownMenuLabel>
112
- {app.environments.map((env) => (
113
- <DropdownMenuItem
114
- key={env.id}
115
- onSelect={() =>
116
- router.visit(
117
- applicationEnvironmentOverviewPath(app.id, env.id),
118
- )
119
- }
120
- >
121
- <LiveDot
122
- lastSeenAt={env.paused ? null : env.last_seen_at}
123
- thresholdMs={10 * 60 * 1000}
124
- />
125
- <span className="flex-1">{env.name}</span>
126
- {env.id === environment?.id && (
127
- <Check className="size-3.5" />
128
- )}
129
- </DropdownMenuItem>
130
- ))}
131
- </div>
132
- ))}
133
- <DropdownMenuSeparator />
134
- <DropdownMenuItem
135
- onSelect={() => router.visit(newApplicationPath())}
136
- >
137
- <Plus className="size-3.5" />
138
- Add application
139
- </DropdownMenuItem>
140
- </DropdownMenuContent>
141
- </DropdownMenu>
142
- </SidebarMenuItem>
143
- </SidebarMenu>
144
- )
145
- }
@@ -1,153 +0,0 @@
1
- import { Filter as FilterIcon, Search, X } from "lucide-react"
2
- import { useState } from "react"
3
-
4
- import { Badge } from "@/components/ui/badge"
5
- import { Button } from "@/components/ui/button"
6
- import {
7
- Command,
8
- CommandEmpty,
9
- CommandGroup,
10
- CommandItem,
11
- CommandList,
12
- } from "@/components/ui/command"
13
- import {
14
- Popover,
15
- PopoverContent,
16
- PopoverTrigger,
17
- } from "@/components/ui/popover"
18
- import { parseFilter, serializeFilter } from "@/lib/filter"
19
-
20
- export interface FilterField {
21
- key: string
22
- label: string
23
- options?: string[]
24
- }
25
-
26
- // key:value key2:value2 free text query bar: chips for parsed field tokens,
27
- // a raw-text input for typing (Enter/blur commits), and a field/option
28
- // autocomplete popover. Full width on phones, a search-box on desktop.
29
- export function FilterBar({
30
- value,
31
- fields,
32
- onChange,
33
- placeholder = "Filter...",
34
- }: {
35
- value: string
36
- fields: FilterField[]
37
- onChange: (query: string) => void
38
- placeholder?: string
39
- }) {
40
- const [draft, setDraft] = useState(value)
41
- const [open, setOpen] = useState(false)
42
- // Re-sync the draft when `value` changes from outside (e.g. a chip removal
43
- // round-tripping through the server, or browser back/forward) without an
44
- // effect: https://react.dev/learn/you-might-not-need-an-effect
45
- const [syncedValue, setSyncedValue] = useState(value)
46
- if (value !== syncedValue) {
47
- setSyncedValue(value)
48
- setDraft(value)
49
- }
50
- const parsed = parseFilter(value)
51
-
52
- const removeField = (key: string) => {
53
- const next = { ...parsed.fields }
54
- delete next[key]
55
- const q = serializeFilter({ text: parsed.text, fields: next })
56
- setDraft(q)
57
- onChange(q)
58
- }
59
-
60
- const applyToken = (key: string, val: string) => {
61
- const q = serializeFilter({
62
- text: parsed.text,
63
- fields: { ...parsed.fields, [key]: val },
64
- })
65
- setDraft(q)
66
- onChange(q)
67
- setOpen(false)
68
- }
69
-
70
- const startToken = (key: string) => {
71
- setDraft((d) => `${d}${d && !d.endsWith(" ") ? " " : ""}${key}:`)
72
- setOpen(false)
73
- }
74
-
75
- const commit = () => onChange(draft)
76
-
77
- return (
78
- <div className="flex flex-wrap items-center gap-2">
79
- <div className="bg-card focus-within:border-ring flex h-8 min-w-0 flex-1 items-center gap-1.5 rounded-md border px-2 md:max-w-md md:flex-none md:basis-80">
80
- <Search className="text-muted-foreground size-3.5 shrink-0" />
81
- {Object.entries(parsed.fields).map(([key, val]) => (
82
- <Badge
83
- key={key}
84
- variant="secondary"
85
- className="shrink-0 gap-1 rounded-sm px-1.5 font-mono text-[10px]"
86
- >
87
- {key}:{val}
88
- <button
89
- type="button"
90
- onClick={() => removeField(key)}
91
- className="hover:text-destructive"
92
- aria-label={`Remove ${key} filter`}
93
- >
94
- <X className="size-3" />
95
- </button>
96
- </Badge>
97
- ))}
98
- <input
99
- data-filter-bar-input
100
- value={draft}
101
- onChange={(e) => setDraft(e.target.value)}
102
- onKeyDown={(e) => e.key === "Enter" && commit()}
103
- onBlur={commit}
104
- placeholder={placeholder}
105
- className="placeholder:text-muted-foreground min-w-0 flex-1 bg-transparent font-mono text-xs outline-none"
106
- />
107
- <kbd className="bg-muted text-muted-foreground hidden rounded px-1 font-mono text-[10px] md:inline">
108
- /
109
- </kbd>
110
- </div>
111
- <Popover open={open} onOpenChange={setOpen}>
112
- <PopoverTrigger asChild>
113
- <Button
114
- type="button"
115
- variant="outline"
116
- size="sm"
117
- className="h-8 px-2"
118
- aria-label="Add filter"
119
- >
120
- <FilterIcon className="size-3.5" />
121
- </Button>
122
- </PopoverTrigger>
123
- <PopoverContent className="w-56 p-0" align="start">
124
- <Command>
125
- <CommandList>
126
- <CommandEmpty>No fields.</CommandEmpty>
127
- {fields.map((f) =>
128
- f.options ? (
129
- <CommandGroup key={f.key} heading={f.label}>
130
- {f.options.map((opt) => (
131
- <CommandItem
132
- key={opt}
133
- onSelect={() => applyToken(f.key, opt)}
134
- >
135
- {opt}
136
- </CommandItem>
137
- ))}
138
- </CommandGroup>
139
- ) : (
140
- <CommandGroup key={f.key} heading={f.label}>
141
- <CommandItem onSelect={() => startToken(f.key)}>
142
- {f.key}:…
143
- </CommandItem>
144
- </CommandGroup>
145
- ),
146
- )}
147
- </CommandList>
148
- </Command>
149
- </PopoverContent>
150
- </Popover>
151
- </div>
152
- )
153
- }
@@ -1,324 +0,0 @@
1
- import { useEffect, useMemo, useRef, useState } from "react"
2
-
3
- import { DataTable } from "@/components/railwatch/data-table"
4
- import { Segmented, SegmentedItem } from "@/components/railwatch/segmented"
5
- import { Input } from "@/components/ui/input"
6
- import {
7
- type FlameNode,
8
- type FlameRect,
9
- hottestSelf,
10
- hottestTotal,
11
- layout,
12
- parseCollapsed,
13
- search,
14
- } from "@/lib/flamegraph"
15
- import { count, pct } from "@/lib/format"
16
- import { cn } from "@/lib/utils"
17
-
18
- const ROW = 18
19
- const HOTTEST = 15
20
-
21
- interface View {
22
- tree: FlameNode
23
- zoom: FlameNode[]
24
- hovered: { rect: FlameRect; x: number; y: number } | null
25
- }
26
-
27
- // Frames read "Class#method (path:line)". The gem strips the Rails root
28
- // from app paths, so an app frame's path starts with app/, lib/ or config/;
29
- // a gem frame's path starts with the gem's name (activerecord/...), Ruby's
30
- // own with ruby/ or <internal:, and a C function has no file at all. Only
31
- // the app's own frames get the warm end of the palette.
32
- function isAppFrame(name: string): boolean {
33
- const open = name.lastIndexOf(" (")
34
- const path = open >= 0 ? name.slice(open + 2) : ""
35
- return /^(app|lib|config|db|engines)\//.test(path)
36
- }
37
-
38
- function hashOf(value: string): number {
39
- let h = 0
40
- for (let i = 0; i < value.length; i++) h = (h * 31 + value.charCodeAt(i)) | 0
41
- return Math.abs(h)
42
- }
43
-
44
- // Colour is a stable hash of the frame name so the same frame keeps the same
45
- // shade between renders and between profiles — warm oranges/ambers for app
46
- // frames, desaturated slate for everything else.
47
- function frameColor(name: string): string {
48
- const h = hashOf(name)
49
- return isAppFrame(name)
50
- ? `hsl(${[24, 34, 14, 44, 4][h % 5]} 78% ${52 + (h % 3) * 6}%)`
51
- : `hsl(${[215, 220, 210][h % 3]} 14% ${58 + (h % 3) * 7}%)`
52
- }
53
-
54
- function truncate(name: string, width: number): string {
55
- const chars = Math.floor((width - 8) / 5.6)
56
- if (chars < 4) return ""
57
- return name.length <= chars ? name : name.slice(0, chars - 1) + "…"
58
- }
59
-
60
- function Tooltip({
61
- rect,
62
- total,
63
- x,
64
- y,
65
- }: {
66
- rect: FlameRect
67
- total: number
68
- x: number
69
- y: number
70
- }) {
71
- return (
72
- <div
73
- className="bg-popover text-popover-foreground pointer-events-none absolute z-10 max-w-[min(22rem,90vw)] rounded-md border p-2 shadow-md"
74
- style={{ left: Math.max(0, x - 8), top: y + 14 }}
75
- >
76
- <div className="font-mono text-[11px] break-all">{rect.node.name}</div>
77
- <div className="text-muted-foreground mt-1 flex flex-wrap gap-x-3 font-mono text-[10px] tabular-nums">
78
- <span>{count(rect.node.value)} samples</span>
79
- <span>{pct(rect.node.value, total)} of total</span>
80
- <span>{pct(rect.node.value, rect.parentValue)} of parent</span>
81
- {rect.node.self > 0 && <span>{count(rect.node.self)} self</span>}
82
- </div>
83
- </div>
84
- )
85
- }
86
-
87
- // An icicle chart (root on top, callees below) over collapsed stacks. Click
88
- // a frame to zoom into it; the breadcrumb walks back out.
89
- export function Flamegraph({
90
- collapsed,
91
- className,
92
- }: {
93
- collapsed: string
94
- className?: string
95
- }) {
96
- const tree = useMemo(() => parseCollapsed(collapsed), [collapsed])
97
- const [query, setQuery] = useState("")
98
- const [mode, setMode] = useState<"self" | "total">("self")
99
- // Zoom path and hover both point at nodes of one particular tree, so they
100
- // are stored with it: a new profile makes the whole view stale at once,
101
- // without an effect that resets them a render late.
102
- const [state, setState] = useState<View>({ tree, zoom: [], hovered: null })
103
- const view = state.tree === tree ? state : { tree, zoom: [], hovered: null }
104
- const zoom = view.zoom
105
-
106
- const containerRef = useRef<HTMLDivElement>(null)
107
- const [width, setWidth] = useState(0)
108
-
109
- useEffect(() => {
110
- const el = containerRef.current
111
- if (!el) return
112
- setWidth(el.clientWidth)
113
- const observer = new ResizeObserver(([entry]) =>
114
- setWidth(entry.contentRect.width),
115
- )
116
- observer.observe(el)
117
- return () => observer.disconnect()
118
- }, [])
119
-
120
- const root = zoom[zoom.length - 1] ?? tree
121
- const rects = useMemo(() => layout(root, width), [root, width])
122
- const matches = useMemo(() => search(tree, query), [tree, query])
123
- const hottest = useMemo(
124
- () =>
125
- mode === "self"
126
- ? hottestSelf(root, HOTTEST)
127
- : hottestTotal(root, HOTTEST),
128
- [root, mode],
129
- )
130
-
131
- const depth = rects.reduce((max, r) => Math.max(max, r.depth), 0)
132
- const searching = matches.size > 0
133
-
134
- if (tree.value === 0) {
135
- return (
136
- <p className="text-muted-foreground py-6 text-center text-sm">
137
- This profile recorded no samples.
138
- </p>
139
- )
140
- }
141
-
142
- return (
143
- <div className={cn("space-y-3", className)}>
144
- <div className="flex flex-wrap items-center gap-2">
145
- <Input
146
- value={query}
147
- onChange={(e) => setQuery(e.target.value)}
148
- placeholder="Search frames"
149
- className="h-8 max-w-56 font-mono text-xs"
150
- aria-label="Search frames"
151
- />
152
- {query !== "" && (
153
- <span className="text-muted-foreground font-mono text-[11px]">
154
- {matches.size} match{matches.size === 1 ? "" : "es"}
155
- </span>
156
- )}
157
- <div className="ml-auto flex flex-wrap items-center gap-1 font-mono text-[11px]">
158
- {[tree, ...zoom].map((node, i) => (
159
- <span key={node.id} className="flex items-center gap-1">
160
- {i > 0 && <span className="text-muted-foreground">/</span>}
161
- <button
162
- type="button"
163
- onClick={() => setState({ ...view, zoom: zoom.slice(0, i) })}
164
- disabled={i === zoom.length}
165
- className={cn(
166
- "max-w-40 truncate",
167
- i === zoom.length
168
- ? "text-foreground font-semibold"
169
- : "text-muted-foreground hover:underline",
170
- )}
171
- >
172
- {node.name}
173
- </button>
174
- </span>
175
- ))}
176
- </div>
177
- </div>
178
-
179
- <div
180
- ref={containerRef}
181
- className="bg-muted/20 relative overflow-hidden rounded-lg border"
182
- onMouseLeave={() => setState({ ...view, hovered: null })}
183
- >
184
- <svg
185
- width={width}
186
- height={(depth + 1) * ROW}
187
- role="img"
188
- aria-label={`Flamegraph of ${tree.value} samples`}
189
- >
190
- {rects.map((r) => {
191
- const label = truncate(r.node.name, r.width)
192
- return (
193
- <g
194
- key={r.id}
195
- onClick={() =>
196
- setState({
197
- ...view,
198
- zoom: [...zoom, ...pathTo(root, r.node)],
199
- })
200
- }
201
- onMouseMove={(e) => {
202
- const box = containerRef.current?.getBoundingClientRect()
203
- setState({
204
- ...view,
205
- hovered: {
206
- rect: r,
207
- x: e.clientX - (box?.left ?? 0),
208
- y: e.clientY - (box?.top ?? 0),
209
- },
210
- })
211
- }}
212
- className="cursor-pointer"
213
- >
214
- <rect
215
- x={r.x}
216
- y={r.depth * ROW}
217
- width={Math.max(0, r.width - 1)}
218
- height={ROW - 1}
219
- rx={2}
220
- fill={frameColor(r.node.name)}
221
- opacity={
222
- searching
223
- ? matches.has(r.id)
224
- ? 1
225
- : 0.18
226
- : view.hovered?.rect.id === r.id
227
- ? 1
228
- : 0.88
229
- }
230
- stroke={matches.has(r.id) ? "currentColor" : "none"}
231
- strokeWidth={1}
232
- />
233
- {label !== "" && (
234
- <text
235
- x={r.x + 4}
236
- y={r.depth * ROW + ROW / 2}
237
- dominantBaseline="central"
238
- className="pointer-events-none fill-black/85 font-mono text-[10px]"
239
- >
240
- {label}
241
- </text>
242
- )}
243
- </g>
244
- )
245
- })}
246
- </svg>
247
- {view.hovered && (
248
- <Tooltip
249
- rect={view.hovered.rect}
250
- total={tree.value}
251
- x={view.hovered.x}
252
- y={view.hovered.y}
253
- />
254
- )}
255
- </div>
256
-
257
- <div className="flex flex-wrap items-center justify-between gap-2">
258
- <h3 className="label-caps">Hottest frames</h3>
259
- <Segmented>
260
- <SegmentedItem
261
- active={mode === "self"}
262
- onClick={() => setMode("self")}
263
- >
264
- Self
265
- </SegmentedItem>
266
- <SegmentedItem
267
- active={mode === "total"}
268
- onClick={() => setMode("total")}
269
- >
270
- Total
271
- </SegmentedItem>
272
- </Segmented>
273
- </div>
274
- <DataTable
275
- rows={hottest}
276
- rowKey={(f) => f.name}
277
- empty="No frames in this view."
278
- columns={[
279
- {
280
- key: "name",
281
- header: "Frame",
282
- className: "max-w-0",
283
- cell: (f) => (
284
- <span className="block truncate font-mono text-xs" title={f.name}>
285
- {f.name}
286
- </span>
287
- ),
288
- },
289
- {
290
- key: "self",
291
- header: "Self",
292
- align: "right",
293
- cell: (f) => (
294
- <span className={mode === "self" ? "font-semibold" : ""}>
295
- {count(f.self)}
296
- </span>
297
- ),
298
- },
299
- {
300
- key: "total",
301
- header: "Total",
302
- align: "right",
303
- cell: (f) => (
304
- <span className={mode === "total" ? "font-semibold" : ""}>
305
- {count(f.total)}
306
- </span>
307
- ),
308
- },
309
- ]}
310
- />
311
- </div>
312
- )
313
- }
314
-
315
- // The chain of nodes from the current root down to `target`, so clicking a
316
- // frame deep in the graph pushes every step onto the breadcrumb.
317
- function pathTo(from: FlameNode, target: FlameNode): FlameNode[] {
318
- if (from === target) return []
319
- for (const child of from.children) {
320
- const rest = pathTo(child, target)
321
- if (rest.length > 0 || child === target) return [child, ...rest]
322
- }
323
- return []
324
- }