rails_pulse 0.3.3 → 0.4.0.pre.2

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 (169) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +299 -0
  3. data/README.md +44 -3
  4. data/app/assets/stylesheets/rails_pulse/application.css +21 -32
  5. data/app/assets/stylesheets/rails_pulse/components/backtrace.css +238 -0
  6. data/app/assets/stylesheets/rails_pulse/components/badge.css +32 -0
  7. data/app/assets/stylesheets/rails_pulse/components/button.css +2 -36
  8. data/app/assets/stylesheets/rails_pulse/components/card.css +8 -11
  9. data/app/assets/stylesheets/rails_pulse/components/chart.css +16 -3
  10. data/app/assets/stylesheets/rails_pulse/components/csp_safe_positioning.css +5 -39
  11. data/app/assets/stylesheets/rails_pulse/components/dashboard.css +0 -52
  12. data/app/assets/stylesheets/rails_pulse/components/deployments.css +39 -0
  13. data/app/assets/stylesheets/rails_pulse/components/dialog.css +0 -5
  14. data/app/assets/stylesheets/rails_pulse/components/input.css +0 -14
  15. data/app/assets/stylesheets/rails_pulse/components/layouts.css +0 -20
  16. data/app/assets/stylesheets/rails_pulse/components/menu.css +0 -16
  17. data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +0 -16
  18. data/app/assets/stylesheets/rails_pulse/components/storage.css +187 -0
  19. data/app/assets/stylesheets/rails_pulse/components/tags.css +0 -14
  20. data/app/assets/stylesheets/rails_pulse/components/time_range_selector.css +5 -5
  21. data/app/assets/stylesheets/rails_pulse/components/utilities.css +0 -17
  22. data/app/controllers/concerns/chart_table_concern.rb +15 -3
  23. data/app/controllers/concerns/ransack_params_concern.rb +18 -0
  24. data/app/controllers/concerns/response_range_concern.rb +1 -1
  25. data/app/controllers/concerns/time_range_concern.rb +28 -14
  26. data/app/controllers/rails_pulse/application_controller.rb +104 -31
  27. data/app/controllers/rails_pulse/dashboard_controller.rb +9 -0
  28. data/app/controllers/rails_pulse/deployments_controller.rb +26 -5
  29. data/app/controllers/rails_pulse/exception_occurrences_controller.rb +19 -0
  30. data/app/controllers/rails_pulse/exceptions_controller.rb +119 -0
  31. data/app/controllers/rails_pulse/job_runs_controller.rb +1 -1
  32. data/app/controllers/rails_pulse/requests_controller.rb +1 -1
  33. data/app/controllers/rails_pulse/routes_controller.rb +7 -1
  34. data/app/controllers/rails_pulse/storage_controller.rb +7 -0
  35. data/app/controllers/rails_pulse/tags_controller.rb +2 -2
  36. data/app/helpers/rails_pulse/application_helper.rb +1 -0
  37. data/app/helpers/rails_pulse/backtrace_helper.rb +85 -0
  38. data/app/helpers/rails_pulse/breadcrumbs_helper.rb +4 -1
  39. data/app/helpers/rails_pulse/chart_helper.rb +0 -3
  40. data/app/helpers/rails_pulse/route_helper.rb +8 -16
  41. data/app/javascript/rails_pulse/application.js +29 -8
  42. data/app/javascript/rails_pulse/controllers/clipboard_controller.js +58 -0
  43. data/app/javascript/rails_pulse/controllers/index_controller.js +16 -10
  44. data/app/javascript/rails_pulse/controllers/pagination_controller.js +6 -2
  45. data/app/javascript/rails_pulse/controllers/popover_controller.js +4 -0
  46. data/app/models/concerns/rails_pulse/taggable.rb +11 -6
  47. data/app/models/rails_pulse/cards/base.rb +53 -0
  48. data/app/models/rails_pulse/dashboard/health_summary.rb +52 -0
  49. data/app/models/rails_pulse/dashboard/needs_attention.rb +73 -1
  50. data/app/models/rails_pulse/dashboard/storage_status.rb +490 -0
  51. data/app/models/rails_pulse/deployment.rb +47 -1
  52. data/app/models/rails_pulse/exception_group.rb +37 -0
  53. data/app/models/rails_pulse/exception_occurrence.rb +27 -0
  54. data/app/models/rails_pulse/exceptions/cards/exception_rate.rb +116 -0
  55. data/app/models/rails_pulse/exceptions/cards/open_groups.rb +49 -0
  56. data/app/models/rails_pulse/exceptions/cards/total_occurrences.rb +62 -0
  57. data/app/models/rails_pulse/exceptions/charts/occurrence_volume.rb +56 -0
  58. data/app/models/rails_pulse/exceptions/raw_data.rb +216 -0
  59. data/app/models/rails_pulse/jobs/cards/p95_duration.rb +8 -2
  60. data/app/models/rails_pulse/operation.rb +13 -4
  61. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +8 -3
  62. data/app/models/rails_pulse/request.rb +5 -1
  63. data/app/models/rails_pulse/route.rb +71 -21
  64. data/app/models/rails_pulse/routes/cards/error_rates.rb +6 -3
  65. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +8 -3
  66. data/app/models/rails_pulse/routes/tables/index.rb +8 -6
  67. data/app/models/rails_pulse/summary.rb +21 -10
  68. data/app/models/rails_pulse/tables/base.rb +4 -2
  69. data/app/services/rails_pulse/analysis/explain_plan_analyzer.rb +47 -12
  70. data/app/services/rails_pulse/exception_capture_service.rb +228 -0
  71. data/app/services/rails_pulse/exception_message_sanitizer.rb +152 -0
  72. data/app/services/rails_pulse/operations/change_point.rb +144 -0
  73. data/app/services/rails_pulse/operations/compare.rb +92 -0
  74. data/app/services/rails_pulse/operations/comparison.rb +126 -0
  75. data/app/services/rails_pulse/operations/metric.rb +63 -0
  76. data/app/services/rails_pulse/operations/series.rb +84 -0
  77. data/app/services/rails_pulse/operations/subject.rb +77 -0
  78. data/app/services/rails_pulse/route_controller_action_backfiller.rb +110 -0
  79. data/app/services/rails_pulse/route_merger.rb +99 -0
  80. data/app/services/rails_pulse/route_migrator.rb +139 -0
  81. data/app/services/rails_pulse/route_path_normalizer.rb +79 -0
  82. data/app/services/rails_pulse/route_recognizer.rb +47 -0
  83. data/app/services/rails_pulse/summary_service.rb +45 -0
  84. data/app/services/rails_pulse/tag_filter_service.rb +5 -3
  85. data/app/views/layouts/rails_pulse/_menu_items.html.erb +7 -0
  86. data/app/views/layouts/rails_pulse/_onboarding_banner.html.erb +8 -0
  87. data/app/views/layouts/rails_pulse/_setup_screen.html.erb +5 -5
  88. data/app/views/layouts/rails_pulse/application.html.erb +4 -4
  89. data/app/views/rails_pulse/components/_empty_state.html.erb +1 -1
  90. data/app/views/rails_pulse/components/_panel.html.erb +16 -6
  91. data/app/views/rails_pulse/components/_raw_data.html.erb +34 -0
  92. data/app/views/rails_pulse/dashboard/_chart_panel.html.erb +6 -8
  93. data/app/views/rails_pulse/dashboard/_deployments_panel.html.erb +64 -0
  94. data/app/views/rails_pulse/dashboard/_health_badge.html.erb +21 -10
  95. data/app/views/rails_pulse/dashboard/_storage_panel.html.erb +67 -0
  96. data/app/views/rails_pulse/dashboard/index.html.erb +22 -1
  97. data/app/views/rails_pulse/deployments/_status_badge.html.erb +5 -0
  98. data/app/views/rails_pulse/deployments/_table.html.erb +29 -0
  99. data/app/views/rails_pulse/deployments/index.html.erb +15 -0
  100. data/app/views/rails_pulse/deployments/show.html.erb +62 -0
  101. data/app/views/rails_pulse/exception_occurrences/show.html.erb +45 -0
  102. data/app/views/rails_pulse/exceptions/_backtrace.html.erb +33 -0
  103. data/app/views/rails_pulse/exceptions/_metric_cards.html.erb +6 -0
  104. data/app/views/rails_pulse/exceptions/_occurrences_table.html.erb +23 -0
  105. data/app/views/rails_pulse/exceptions/_status_badge.html.erb +6 -0
  106. data/app/views/rails_pulse/exceptions/_table.html.erb +33 -0
  107. data/app/views/rails_pulse/exceptions/index.html.erb +66 -0
  108. data/app/views/rails_pulse/exceptions/show.html.erb +52 -0
  109. data/app/views/rails_pulse/queries/_raw_data.html.erb +1 -23
  110. data/app/views/rails_pulse/requests/_table.html.erb +1 -1
  111. data/app/views/rails_pulse/routes/_requests_table.html.erb +5 -0
  112. data/app/views/rails_pulse/routes/_table.html.erb +22 -8
  113. data/app/views/rails_pulse/routes/index.html.erb +5 -1
  114. data/app/views/rails_pulse/routes/show.html.erb +5 -0
  115. data/app/views/rails_pulse/storage/show.html.erb +123 -0
  116. data/config/routes.rb +13 -4
  117. data/db/rails_pulse_migrate/20260506000001_create_rails_pulse_exceptions.rb +50 -0
  118. data/db/rails_pulse_migrate/20260507000001_add_actual_query_to_operations.rb +10 -5
  119. data/db/rails_pulse_migrate/20260610000001_add_controller_action_to_rails_pulse_routes.rb +8 -0
  120. data/db/rails_pulse_migrate/20260610000002_change_rails_pulse_routes_to_multi_verb_model.rb +185 -0
  121. data/db/rails_pulse_migrate/20260610000003_add_null_action_unique_index_to_routes.rb +32 -0
  122. data/db/rails_pulse_migrate/20260823000001_add_location_to_exception_groups.rb +12 -0
  123. data/db/rails_pulse_schema.rb +54 -6
  124. data/lib/generators/rails_pulse/base_methods.rb +2 -0
  125. data/lib/generators/rails_pulse/install_generator.rb +5 -0
  126. data/lib/generators/rails_pulse/schema_parser.rb +22 -2
  127. data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +54 -6
  128. data/lib/generators/rails_pulse/templates/migrations/upgrade_rails_pulse_tables.rb +1 -1
  129. data/lib/generators/rails_pulse/templates/rails_pulse.rb +157 -16
  130. data/lib/generators/rails_pulse/upgrade_generator.rb +166 -38
  131. data/lib/rails_pulse/cleanup_service.rb +110 -15
  132. data/lib/rails_pulse/configuration.rb +125 -6
  133. data/lib/rails_pulse/engine.rb +32 -17
  134. data/lib/rails_pulse/installers/config_updater.rb +202 -0
  135. data/lib/rails_pulse/job_run_collector.rb +43 -16
  136. data/lib/rails_pulse/middleware/asset_server.rb +11 -0
  137. data/lib/rails_pulse/middleware/request_collector.rb +9 -4
  138. data/lib/rails_pulse/packaged_assets.rb +147 -0
  139. data/lib/rails_pulse/route_indexes.rb +66 -0
  140. data/lib/rails_pulse/subscribers/exception_subscriber.rb +36 -0
  141. data/lib/rails_pulse/tracker.rb +29 -10
  142. data/lib/rails_pulse/version.rb +1 -1
  143. data/lib/rails_pulse.rb +1 -0
  144. data/lib/rails_pulse_server.ru +16 -47
  145. data/lib/tasks/rails_pulse.rake +83 -4
  146. data/lib/tasks/rails_pulse_assets.rake +15 -0
  147. data/lib/tasks/rails_pulse_tasks.rake +29 -0
  148. data/public/rails-pulse-assets/rails-pulse-icons.js +6 -2
  149. data/public/rails-pulse-assets/rails-pulse.css +1 -1
  150. data/public/rails-pulse-assets/rails-pulse.js +18 -117
  151. metadata +78 -37
  152. data/Rakefile +0 -554
  153. data/app/javascript/rails_pulse/controllers/form_controller.js +0 -39
  154. data/app/javascript/rails_pulse/controllers/period_selector_controller.js +0 -19
  155. data/app/javascript/rails_pulse/controllers/timezone_controller.js +0 -89
  156. data/app/javascript/rails_pulse/theme.js +0 -416
  157. data/config/brakeman.ignore +0 -190
  158. data/config/brakeman.yml +0 -68
  159. data/config/initializers/rails_pulse.rb +0 -287
  160. data/public/rails-pulse-assets/csp-test.js +0 -120
  161. data/public/rails-pulse-assets/rails-pulse-icons.js.map +0 -13
  162. data/public/rails-pulse-assets/rails-pulse.css.map +0 -1
  163. data/public/rails-pulse-assets/rails-pulse.js.map +0 -7
  164. data/vendor/assets/javascripts/rails-pulse-icons.js +0 -93
  165. data/vendor/assets/javascripts/rails-pulse-icons.js.map +0 -13
  166. data/vendor/assets/javascripts/rails-pulse.js +0 -157
  167. data/vendor/assets/javascripts/rails-pulse.js.map +0 -7
  168. data/vendor/assets/stylesheets/rails-pulse.css +0 -1
  169. data/vendor/assets/stylesheets/rails-pulse.css.map +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36c07de8920dddd7bb85571c3c01b94c197866dfeb45c471cad71dc829c61601
4
- data.tar.gz: c4cfbade2a92decf14272c76b32028918dab1bdc60d61bf95e9b6a9b69b0e07b
3
+ metadata.gz: b102fc4776d7fe701b47011e02d682f47a2784500103f3cb7d7598a757b174ff
4
+ data.tar.gz: 34f4055047c1bb9151c9e811f14c4e90fe83e52508c61ea4433929b2b431d568
5
5
  SHA512:
6
- metadata.gz: 65565ffc47a14f6b4402dd3c83bddd8c422732460b91bf4a07655cc9de034d8535e0ccc679bbeb83e33ca83694d2891868dc60ee2e90243f20c0503287bba8b2
7
- data.tar.gz: b2b2c03e714bd1081ee152aadcc72f892b5a5927d2823725b10eb4ddaebaceedc5216ac48fbfe56372cb77f9b3701645f9eea27c62d400ad39ed4bef0840717c
6
+ metadata.gz: 974fcf20833ab11e46a00db96d21590f32a699a73de8d2af229d1fa5559f0817c107ba684f4ada34cdb067a77f6293ecc11ee3d24f7911f9fe074353a2991d79
7
+ data.tar.gz: 507afdae03f11fef23d0bbd7cc5ee8e8123656e41efbc219a10f8b6682602bfb7dcfa91e9ac424352d68ad05b3bceed4070eead220d053713f56aa1ade7bec01
data/CHANGELOG.md ADDED
@@ -0,0 +1,299 @@
1
+ # Changelog
2
+
3
+ All notable changes to Rails Pulse will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.4.0.pre.2] - 2026-09-04
11
+
12
+ ### Fixed
13
+
14
+ - **`rails generate rails_pulse:upgrade` wrote a migration that would not parse.**
15
+ The schema parser matched a column comment with `comment: "([^"]*)"`, which stops
16
+ at the first escaped quote. The `http_methods` comment embeds a `["GET","POST"]`
17
+ example, so it was truncated mid-escape and the trailing backslash escaped the
18
+ closing quote of the generated `comment:` string — the copied
19
+ `upgrade_rails_pulse_tables.rb` then failed to load with
20
+ `unterminated string meets end of file`. Comments are now matched as complete Ruby
21
+ string literals, decoded with `String#undump`, and re-escaped with `String#inspect`
22
+ when the migration is rendered.
23
+
24
+ - **Background tracking writes corrupted the host app's test database connection.**
25
+ Under `use_transactional_tests` Rails pins one connection per pool and hands that
26
+ same connection to every thread, so the tracker's writer thread interleaved its
27
+ INSERTs with the test's own statements on a single socket. PostgreSQL reported
28
+ this as `message type 0x5a arrived from server while idle` and
29
+ `server sent data ("D" message) without prior row description`, and suites hung
30
+ or failed at random. The gem's own dummy app avoided it with
31
+ `config.async = false if Rails.env.test?`, but the installed initializer never
32
+ shipped that line. The tracker now writes inline whenever the pool's connection
33
+ is shared across threads, and `rails generate rails_pulse:install` adds the
34
+ test-environment line to the initializer. Existing installs should add
35
+ `config.async = false if Rails.env.test?` to `config/initializers/rails_pulse.rb`.
36
+
37
+ ## [0.4.0.pre.1] - 2026-09-03
38
+
39
+ This release contains a **breaking schema change** and requires a one-time data
40
+ migration. Read "Upgrading from 0.3.x" below before deploying. Take a database
41
+ backup first: the route migration is irreversible.
42
+
43
+ ### Security
44
+
45
+ - **Job failure messages are redacted and bounded.** `rails_pulse_job_runs.error_message`
46
+ used to be stored raw and unbounded. It now goes through the same sanitizer as
47
+ exception messages: `key=value` / `key: value` / `"key":"value"` fragments are
48
+ masked whenever the key matches `config.filter_parameters` (same rules as logged
49
+ params), and MySQL `Duplicate entry '<value>'` values are redacted alongside
50
+ PostgreSQL `DETAIL:` clauses.
51
+ - **CSRF protection is now declared by the engine.** `RailsPulse::ApplicationController`
52
+ calls `protect_from_forgery with: :exception` itself instead of inheriting it from the
53
+ host's `default_protect_from_forgery`, which only `load_defaults 5.2+` enables. Hosts
54
+ on an older `load_defaults` had no CSRF protection on any engine endpoint (tags,
55
+ exception status, query reanalyze, session filters). Brakeman's `CheckForgerySetting`
56
+ is no longer skipped.
57
+ - **Authentication hooks fail closed.** An `authentication_method` that returned
58
+ `false` without rendering or redirecting used to grant access — `proc { current_user&.admin? }`
59
+ looked right and let every non-admin in. Literal `false` is now a 403; `nil`
60
+ (what the documented `unless … redirect_to … end` style returns on success) still allows.
61
+ - **EXPLAIN hardening.** The query analyzer now refuses SQL containing `;`, `--` or `/*`
62
+ anywhere (PostgreSQL's `execute` runs every statement in a string, so a captured
63
+ `SELECT …; UPDATE …` would have re-run the UPDATE on each page view), runs the
64
+ PostgreSQL EXPLAIN inside `SET LOCAL transaction_read_only = on` with a 5s
65
+ `statement_timeout` instead of `Timeout.timeout`, and always rolls its savepoint back.
66
+ - **Deployments API input is bounded.** `revision` is capped at 255 characters,
67
+ serialized `metadata` at 4 KB, and `started_at` may be at most one hour in the future
68
+ (a forged future marker would have stamped every later exception with a revision
69
+ that never shipped). `rails_pulse_deployments` now has a count cap
70
+ (`max_table_records`, default 1 000) enforced by `CleanupService`; deployments are
71
+ never pruned by age.
72
+ - **Backtrace source snippets are limited to `app/`, `lib/` and `config/routes.rb`**
73
+ (`.rb`, `.erb`, `.rake`, `.haml`, `.slim`, `.jbuilder`). Previously any file under
74
+ `Rails.root` was readable through a backtrace frame, including initializers with
75
+ inline keys, `config/database.yml`, and `.env`.
76
+ - **Standalone dashboard session cookie is `Secure` in production** (override with
77
+ `RAILS_PULSE_INSECURE_SESSION=1`). The rackup's "no Rails app" branch, which could
78
+ never boot (`NameError: uninitialized constant Rails`), is replaced with a clear
79
+ error; the deployment docs now give the working invocation.
80
+
81
+ ### Added
82
+
83
+ - **Exception tracking** — Captures unhandled exceptions from web requests and
84
+ background jobs, groups them by class and location, and displays backtraces (first
85
+ 50 frames) with filtered request params in a new Exceptions tab. Exception
86
+ messages are redacted before storage: `key=value` / `key: value` / `"key":"value"`
87
+ fragments are masked whenever the key matches `config.filter_parameters`, and
88
+ MySQL `Duplicate entry '<value>'` values are redacted alongside PostgreSQL
89
+ `DETAIL:` clauses.
90
+ - **`track_exceptions` config option** — Gem default is `false`, so existing installs
91
+ do not begin capturing on upgrade; the upgrade generator inserts `false`
92
+ explicitly. The install template sets `true` for new apps.
93
+ - **`capture_exception_params` config option** — Captures request params alongside
94
+ each occurrence, filtered via Rails' `filter_parameters`. Occurrences whose params
95
+ exceed 10 KB after filtering are stored without params. Default `true`, and only
96
+ consulted while `track_exceptions` is on.
97
+ - **`exception_message_filter` config option** — an optional `->(message, exception)`
98
+ hook applied after the built-in redaction for app-specific patterns. If the hook
99
+ raises, the message is stored as `[FILTERED]` rather than unfiltered.
100
+ - **`authorize` config option** — a fail-closed predicate, `->(controller) { … }`
101
+ (or a zero-argument proc run in the controller). Anything falsy is a 403. Runs after
102
+ `authentication_method` when both are set, and replaces the HTTP Basic fallback on
103
+ its own. This is now the recommended way to gate the dashboard.
104
+ - **Upgrade generator syncs new initializer settings** — Appends keys (and
105
+ `max_table_records` entries) the host file does not already mention, without
106
+ rewriting existing values. Review with `git diff`.
107
+
108
+ ### Changed
109
+
110
+ - **BREAKING — route identity is now `[controller_action, path]`.** GET `/users`
111
+ (`users#index`) and POST `/users` (`users#create`) are distinct routes. Dynamic
112
+ paths are normalized at capture time (`/posts/42` → `/posts/:id`).
113
+ - **A one-time backfill is mandatory.** `rails rails_pulse:migrate_routes` backfills
114
+ `controller_action` (from the live router, then from request history), collapses
115
+ historical literal paths, and merges routes sharing an action (GET+POST `/sign_in`
116
+ → `sessions#new`). A schema migrate alone leaves the Action column empty. The
117
+ upgrade generator and the dashboard both warn when the step is outstanding.
118
+ - **The JavaScript bundle is 66% smaller: 2.19 MB → 759 KB (754 KB → 248 KB gzipped).**
119
+ ECharts is now tree-shaken — `application.js` imports from `echarts/core` and
120
+ registers only the series and components the dashboard uses, instead of the
121
+ `echarts` barrel that bundled every chart type including maps, sankey and gauge.
122
+ Charts outside that set must now register their module explicitly; see
123
+ `docs/stimulus_echarts_usage.md`. The stylesheet is 5% smaller.
124
+
125
+ ### Removed
126
+
127
+ - **BREAKING — `rails_pulse_routes.method` is dropped.** The HTTP verb now lives on
128
+ each request (`rails_pulse_requests.method`); a route carries an `http_methods`
129
+ array instead. All application, worker, and dashboard processes must be restarted
130
+ together against the migrated schema. A rolling restart that leaves 0.3.x
131
+ processes running will silently drop request tracking and 500 the routes dashboard.
132
+ - **BREAKING — the route migration is irreversible.** `db:rollback` raises
133
+ `ActiveRecord::IrreversibleMigration` on
134
+ `ChangeRailsPulseRoutesToMultiVerbModel`, and routes merged by
135
+ `rails_pulse:migrate_routes` are deleted with no audit trail. Recovery from a bad
136
+ upgrade is restore-from-backup only.
137
+ - **Three unreachable Stimulus controllers** — `form`, `timezone` and
138
+ `period_selector` — were bundled and registered despite no view referencing them.
139
+ `timezone` also ran a `MutationObserver` over the whole document body.
140
+ - **`theme.js`**, which registered a chart theme that `application.js` immediately
141
+ overwrote. Its UMD wrapper called `require('echarts')`, which alone would have
142
+ defeated the tree-shaking above.
143
+ - **Dead CSS**: unused css-zero ports, the removed dashboard period selector's styles,
144
+ and the disabled `toolbox` chart option.
145
+ - **`csp-test.js` is no longer shipped in the gem.** Its page is mounted only when
146
+ `Rails.env.local?`, so it was unreachable in a published gem. It remains in the
147
+ repository for development and the CSP compliance system test.
148
+
149
+ ### Fixed
150
+
151
+ - **Tag filters work on SQLite for tags containing `_`.** The `LIKE` patterns were
152
+ escaped but issued without an `ESCAPE` clause; SQLite has no default escape
153
+ character, so `my_tag` never matched and "hide records tagged my_tag" hid nothing.
154
+ All three adapters now get an explicit `ESCAPE '!'`.
155
+ - **Hand-edited query strings no longer 500.** `?q=string`, unparseable custom date
156
+ ranges, unparseable `occurred_at` filters, a non-array `enabled_tags`, and
157
+ oversized or unparseable session time filters all fall back to defaults instead of
158
+ raising. Session-stored presets and performance thresholds are validated against
159
+ the known values.
160
+ - **Chart click/zoom got slower the more you switched chart tabs.** The requests,
161
+ routes and queries index pages attached a fresh `document` mouseup listener every
162
+ time a chart tab was re-selected, and only the most recent one was ever removed.
163
+ After N switches a single click anywhere on the page re-ran the zoom handler N
164
+ times. The listener is now bound once per controller.
165
+ - **Zooming onto the first column of a category chart reset the range.** Visible-range
166
+ extraction used `dataZoom.startValue || 0`, so a legitimate index of `0` fell through
167
+ to the full-axis fallback.
168
+ - **Hover popovers could throw after a table refresh.** `popover_controller` left its
169
+ show/hide timers running on disconnect, so a pending 700ms open could fire against
170
+ an element a fetch had already replaced.
171
+ - **The time range selector's hover border was invisible.** It referenced
172
+ `--color-border-emphasis`, which is defined nowhere, so the declaration was invalid
173
+ at computed-value time and the border fell back to `currentColor`. Its light-mode
174
+ rules also never applied on a first visit, because `data-color-scheme` is only set
175
+ after an explicit toggle.
176
+ - **`[popover].positioned` was declared twice** at equal specificity, so correct
177
+ popover placement depended on CSS file load order. Merged into one rule.
178
+ - Fixed upgrading when using a separate database installation.
179
+ - Separate-database installs set `schema_dump: false`, so `db:migrate` no longer
180
+ dumps or loads `db/rails_pulse_structure.sql` (#189).
181
+ - Fixed `assets:precompile` failing on memory-constrained hosts by not registering
182
+ dashboard assets with the host Sprockets pipeline. Precompile copies the pre-built
183
+ files into `public/assets` (digested, no compressor) so `config.asset_host` and
184
+ CDN-only CSP work; development and hosts without a pipeline still use
185
+ `/rails-pulse-assets/<gem-version>/...`.
186
+ - SQLite `schema.rb` now keeps the partial unique index on unrecognised routes
187
+ (`WHERE controller_action IS NULL`). A raw `CREATE INDEX` was dumped without the
188
+ predicate, so `db:schema:load` and parallel tests unique-constrained every path.
189
+
190
+ ### Upgrading from 0.3.x
191
+
192
+ Applies to every 0.3.x release (0.3.0 through 0.3.3). **Back up your database first.**
193
+
194
+ ```bash
195
+ bundle update rails_pulse
196
+ rails generate rails_pulse:upgrade
197
+ rails db:migrate # separate Pulse database: rails db:migrate:rails_pulse
198
+ rails rails_pulse:migrate_routes # required — schema migrate alone leaves Action empty
199
+ ```
200
+
201
+ Then restart **all** processes together, not as a rolling deploy.
202
+
203
+ Separate-database hosts: add `schema_dump: false` to the `rails_pulse` entry in
204
+ `config/database.yml` and delete `db/rails_pulse_structure.sql` if it exists. Do not
205
+ run `db:setup` / `db:prepare` as a substitute for `db:migrate:rails_pulse`.
206
+
207
+ Exception tracking stays off after upgrading. Set `config.track_exceptions = true`
208
+ once you have reviewed what is captured.
209
+
210
+ ## [0.3.3] - 2026-06-23
211
+
212
+ - **Deployment tracking** — Record deployments via `POST /rails_pulse/deployments` or `rake rails_pulse:record_deployment[sha]`. Deployments appear as vertical marker lines on performance charts so you can correlate releases with regressions
213
+ - **`deployment_api_token` config option** — Secures the deployments endpoint with a token header for CI/CD use
214
+ - All multi-series charts now use a native ECharts time axis (`[timestamp_ms, value]` pairs) instead of a separate labels array, enabling deployment markers and better zoom behaviour
215
+
216
+ ## [0.3.0] - 2026-04-30
217
+
218
+ This is the largest release to date — a full UI overhaul across every section of
219
+ the dashboard. Charts are now switchable, the dashboard surfaces health status
220
+ automatically, and the underlying controller and model code has been substantially
221
+ refactored. Existing installs need to run the two new migrations. Hopefully future
222
+ releases will be smaller and more incremental than this one.
223
+
224
+ ### Added
225
+
226
+ - **New chart types across all sections** — Routes, queries, and jobs now have dedicated chart panels with switchable chart types (response time percentiles, request volume, error rate, execution volume, duration, failure rate)
227
+ - **Dashboard health summary** — New `HealthSummary` model surfaces an overall health status and highlights routes/queries/jobs that need attention
228
+ - **Dashboard "Needs Attention" section** — Automatically surfaces slow routes, high error rates, and problematic jobs without manual digging
229
+ - **Storage pressure indicator** — Tracks and displays database storage growth so you know when to adjust retention settings
230
+ - **Flame graph view for requests** — Request detail pages now include a flame graph visualisation of operation timing
231
+ - **P95 duration tracking for jobs** — Job runs now record and display p95 duration alongside average duration
232
+ - **Database load metric for queries** — New card and chart tracking cumulative database load (execution count × avg duration) over time
233
+ - **Diagnostic fields for queries** — Query show pages now surface diagnostic information alongside existing analysis
234
+ - **Chart series toggle** — Show/hide individual series on charts without leaving the page
235
+ - **Performance status concern** — Shared `HasPerformanceStatus` concern for models that report a health status
236
+ - **Metric strip component** — New compact summary strip component for displaying multiple metrics inline
237
+ - **Setup banner** — Onboarding banner shown to users who haven't completed setup
238
+ - **Time range selector** — Redesigned time range UI with a custom date range option
239
+ - **Suggestions service** — Query show pages surface optimisation suggestions (caching, controller, SQL, HTTP, view) via dedicated suggestion services
240
+ - **Statistics module** — New `RailsPulse::Statistics` module for shared statistical calculations
241
+ - **Cleanup stats reporter** — Detailed reporting on what the cleanup job removed each run
242
+ - **Cleanup task runner** — Extracted cleanup orchestration into `CleanupTaskRunner` for testability
243
+ - **Config and migration installers** — Extracted install/upgrade logic into `ConfigInstaller` and `MigrationInstaller` classes
244
+ - **Schema parser** — New `SchemaParser` for reading and diffing schema state during upgrades
245
+ - **Icon helper** — Centralised `IconHelper` for rendering SVG icons
246
+ - **Route helper** — Centralised `RouteHelper` for building internal dashboard links
247
+ - **CSP helper** — Dedicated `CspHelper` for content security policy nonce management
248
+ - **Backfill summaries job** — New job to backfill summary records for historical data
249
+
250
+ ### Changed
251
+
252
+ - **Dashboard redesigned** — Full dashboard overhaul with health summary, attention sections, and chart panels replacing the previous card layout
253
+ - **Routes, queries, and jobs index pages redesigned** — Consistent layout with metric cards, switchable chart tabs, and paginated tables
254
+ - **Metric card component updated** — Cards now support a richer data structure with trend indicators
255
+ - **Application controller refactored** — Controller concerns split into focused modules (`ChartTableConcern`, `MetricCardConcern`, `PaginationConcern`, `SessionFiltersConcern`, `TagFilterConcern`, `TimeRangeConcern`, `ZoomRangeConcern`)
256
+ - **`normalized_sql` column expanded to `text`** — Removes the 1000-character limit on PostgreSQL and MySQL (migration provided for existing installs)
257
+ - **Diagnostic fields added to queries table** — New columns captured at instrumentation time
258
+ - **Summary job and service refactored** — Cleaner separation between job scheduling and summary calculation logic
259
+ - **Cleanup job refactored** — Now uses `CleanupTaskRunner` and reports statistics via `CleanupStatsReporter`
260
+ - **Configuration expanded** — New configuration options exposed in the initializer template
261
+ - **Upgrade generator simplified** — Generator now delegates to `MigrationInstaller` and `ConfigInstaller`
262
+ - **Asset server middleware updated** — Improved asset serving reliability
263
+ - **Request collector middleware updated** — Performance improvements and cleaner instrumentation
264
+ - **Operation subscriber refactored** — Cleaner event handling and reduced complexity
265
+ - **Seeds refactored** — Dummy app seeds split into focused files under `db/seeds/rails_pulse/`
266
+ - **Test coverage significantly expanded** — Controller concerns, helpers, models, services, and system tests all substantially extended
267
+
268
+ ### Removed
269
+
270
+ - `StatusHelper` removed — status rendering consolidated into model concerns and view components
271
+ - `ChartFormatters` helper removed — chart formatting moved into chart model classes
272
+ - `chart_formatters_test.rb` removed alongside the helper
273
+ - Slow queries and slow routes table models removed — replaced by the Needs Attention system
274
+ - Unused `average_response_time` and `p95_response_time` dashboard chart classes removed
275
+ - `requests/tables/index.rb` and related test removed — requests index now uses a shared approach
276
+ - `routes/cards/average_response_times` and `routes/charts/average_response_times` removed — replaced by percentile-based equivalents
277
+ - `jobs/cards/average_duration` and `jobs/cards/total_jobs` removed — replaced by updated cards
278
+ - Timezone controller removed
279
+ - Application mailer stub removed
280
+
281
+ ## [0.2.7] - 2026-04-17
282
+
283
+ No changelog entry — see git history.
284
+
285
+ ## [0.2.6] - 2026-04-15
286
+
287
+ No changelog entry — see git history.
288
+
289
+ ## [0.2.5] - 2026-04-14
290
+
291
+ No changelog entry — see git history.
292
+
293
+ [Unreleased]: https://github.com/railspulse/rails_pulse/compare/v0.4.0.pre.1...HEAD
294
+ [0.4.0.pre.1]: https://github.com/railspulse/rails_pulse/compare/v0.3.3...v0.4.0.pre.1
295
+ [0.3.3]: https://github.com/railspulse/rails_pulse/compare/v0.3.2...v0.3.3
296
+ [0.3.0]: https://github.com/railspulse/rails_pulse/compare/v0.2.7...v0.3.0
297
+ [0.2.7]: https://github.com/railspulse/rails_pulse/compare/v0.2.6...v0.2.7
298
+ [0.2.6]: https://github.com/railspulse/rails_pulse/compare/v0.2.5...v0.2.6
299
+ [0.2.5]: https://github.com/railspulse/rails_pulse/releases/tag/v0.2.5
data/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  ![License](https://img.shields.io/badge/License-MIT-green)
14
14
  ![Ruby Version](https://img.shields.io/badge/Ruby-3.0%2B-red)
15
15
 
16
- Rails Pulse is a Rails engine that monitors your app's performance from the inside. It tracks slow requests, N+1 queries, SQL performance, and background jobs. All data stays in your own database, no third-party cloud, no SaaS subscription, no data leaving your servers.
16
+ Rails Pulse is a Rails engine that monitors your app's performance from the inside. It tracks slow requests, N+1 queries, SQL performance, background jobs, and unhandled exceptions. All data stays in your own database, no third-party cloud, no SaaS subscription, no data leaving your servers.
17
17
 
18
18
  <table border="0">
19
19
  <tr border="0" style="border:0">
@@ -60,6 +60,32 @@ RailsPulse::CleanupJob.perform_later # cron: 0 1 * * *
60
60
 
61
61
  Your dashboard is now at `http://localhost:3000/rails_pulse`.
62
62
 
63
+ ## Upgrading
64
+
65
+ From 0.3.3:
66
+
67
+ ```bash
68
+ bundle update rails_pulse
69
+ rails generate rails_pulse:upgrade
70
+ rails db:migrate # or: rails db:migrate:rails_pulse
71
+ rails rails_pulse:migrate_routes # required — fills Action and merges same-action paths
72
+ ```
73
+
74
+ Restart all processes after migrate. This release changes how routes are stored (`method` moves off the route onto each request), so mixed old/new processes are not supported.
75
+
76
+ The upgrade generator appends new settings to `config/initializers/rails_pulse.rb` without rewriting what you already set. Review with `git diff` and keep or discard hunks.
77
+
78
+ Exception tracking is **off** for existing installs. The generator inserts `config.track_exceptions = false`; set it to `true` after migrating to opt in:
79
+
80
+ ```ruby
81
+ config.track_exceptions = true
82
+ config.capture_exception_params = true # params are filtered via Rails' filter_parameters
83
+ ```
84
+
85
+ Separate-database hosts: set `schema_dump: false` on the `rails_pulse` entry in `config/database.yml` and delete `db/rails_pulse_structure.sql` if that file exists.
86
+
87
+ If you previously added `rails-pulse.js` / `rails-pulse.css` to `config.assets.precompile`, remove those entries — the gem no longer registers dashboard assets with Sprockets (that re-minify OOMs small hosts). Production deploys that use `config.asset_host` or a CDN-only CSP should run `assets:precompile` so `rails_pulse:install_assets` copies digested files into `public/assets`.
88
+
63
89
  Full install guide: [railspulse.com/documentation/installation](https://railspulse.com/documentation/installation)
64
90
 
65
91
  Separate database setup: [railspulse.com/documentation/database](https://railspulse.com/documentation/database)
@@ -81,6 +107,9 @@ RailsPulse.configure do |config|
81
107
  config.track_jobs = true
82
108
  config.capture_job_arguments = false # keep false to protect sensitive data
83
109
 
110
+ config.track_exceptions = true
111
+ config.capture_exception_params = true # params are filtered via Rails' filter_parameters
112
+
84
113
  config.full_retention_period = 30.days
85
114
  end
86
115
  ```
@@ -89,11 +118,20 @@ Full configuration reference: [railspulse.com/documentation/advanced](https://ra
89
118
 
90
119
  ## Authentication
91
120
 
92
- Rails Pulse has no built-in authentication, you protect the dashboard using your app's existing auth. Add an `authentication_method` to the initializer:
121
+ Rails Pulse has no built-in user accounts; you protect the dashboard using your app's existing auth. Authentication is on by default outside development and test, and with nothing configured it falls back to HTTP Basic against `RAILS_PULSE_USERNAME` / `RAILS_PULSE_PASSWORD` (denying everything if the password is unset).
122
+
123
+ The simplest hook is a predicate that receives the controller and returns `true` to allow — anything else is a 403:
124
+
125
+ ```ruby
126
+ RailsPulse.configure do |config|
127
+ config.authorize = ->(controller) { controller.current_user&.admin? }
128
+ end
129
+ ```
130
+
131
+ If you need to redirect to a login page instead, use `authentication_method`, which runs inside the controller and denies by rendering or redirecting:
93
132
 
94
133
  ```ruby
95
134
  RailsPulse.configure do |config|
96
- config.authentication_enabled = true
97
135
  config.authentication_redirect_path = "/login"
98
136
 
99
137
  config.authentication_method = proc {
@@ -104,6 +142,8 @@ RailsPulse.configure do |config|
104
142
  end
105
143
  ```
106
144
 
145
+ Returning `false` from `authentication_method` without responding is also treated as a denial, but `nil` (what `unless … end` returns on success) allows the request — so keep predicate-style checks in `authorize`.
146
+
107
147
  Authentication guide: [railspulse.com/documentation/authentication](https://railspulse.com/documentation/authentication)
108
148
 
109
149
  ## Features
@@ -111,6 +151,7 @@ Authentication guide: [railspulse.com/documentation/authentication](https://rail
111
151
  - **Request monitoring** — every request is timed and stored with its route, status, SQL count, and duration. Slow requests are flagged automatically based on thresholds you control.
112
152
  - **Query analysis** — captures the queries behind each request, detects N+1 patterns, and tracks normalized SQL across requests so you can see which queries are hurting you in production, not just in development.
113
153
  - **Job tracking** — monitors background job duration, queue wait time, and failure rates. Works with any Active Job adapter.
154
+ - **Exception tracking** — captures unhandled exceptions from web requests and background jobs, groups them by class and location, and shows full backtraces with filtered request params. See recurring errors in production without a separate error monitoring service.
114
155
  - **System health bar** — at-a-glance dashboard summary showing healthy, slow, and critical counts across your routes, queries, jobs, and storage. Lets you see the overall state of your app before drilling into specifics.
115
156
  - **No data leaves your app** — everything is stored in your own database. No third-party cloud, no SaaS subscription, no outbound connections.
116
157
  - **Low overhead** — tracking is async and uses a thread-local flag to skip recording Rails Pulse's own internal requests.
@@ -41,6 +41,27 @@ html[data-color-scheme="dark"] .badge--negative-inverse {
41
41
  --badge-background: rgb(47, 47, 47);
42
42
  }
43
43
 
44
+ html[data-color-scheme="dark"] .badge--get {
45
+ --badge-background: var(--blue-950);
46
+ --badge-color: var(--blue-200);
47
+ }
48
+
49
+ html[data-color-scheme="dark"] .badge--post {
50
+ --badge-background: var(--green-950);
51
+ --badge-color: var(--green-200);
52
+ }
53
+
54
+ html[data-color-scheme="dark"] .badge--put,
55
+ html[data-color-scheme="dark"] .badge--patch {
56
+ --badge-background: var(--amber-950);
57
+ --badge-color: var(--amber-200);
58
+ }
59
+
60
+ html[data-color-scheme="dark"] .badge--delete {
61
+ --badge-background: var(--red-950);
62
+ --badge-color: var(--red-200);
63
+ }
64
+
44
65
  html[data-color-scheme="dark"] .input {
45
66
  --input-background: #535252;
46
67
  --input-border-color: #7e7d7d;
@@ -161,19 +182,6 @@ html[data-color-scheme="dark"] .input {
161
182
  z-index: 10;
162
183
  }
163
184
 
164
- .flame-bar-label {
165
- font-size: 11px;
166
- white-space: nowrap;
167
- overflow: hidden;
168
- text-overflow: ellipsis;
169
- padding: 0 5px;
170
- line-height: 20px;
171
- color: rgba(255, 255, 255, 0.92);
172
- display: block;
173
- pointer-events: none;
174
- user-select: none;
175
- }
176
-
177
185
  .flame-time-axis {
178
186
  position: relative;
179
187
  height: 20px;
@@ -232,22 +240,3 @@ html[data-color-scheme="dark"] .input {
232
240
  font-size: 11px;
233
241
  }
234
242
 
235
- /* REQUEST OPERATIONS BAR */
236
- .bar-container {
237
- height:10px;
238
- position:relative
239
- }
240
- .bar {
241
- background-color:#727579;
242
- height:100%;
243
- position:absolute;
244
- top:0
245
- }
246
- .bar:first-child {
247
- border-bottom-left-radius:1px;
248
- border-top-left-radius:1px
249
- }
250
- .bar:last-child {
251
- border-bottom-right-radius:1px;
252
- border-top-right-radius:1px
253
- }