rails_pulse 0.4.0.pre.4 → 0.4.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -87
  3. data/README.md +83 -111
  4. data/app/controllers/concerns/session_filters_concern.rb +4 -3
  5. data/app/controllers/concerns/time_range_concern.rb +29 -19
  6. data/app/controllers/rails_pulse/application_controller.rb +9 -5
  7. data/app/controllers/rails_pulse/dashboard_controller.rb +20 -11
  8. data/app/controllers/rails_pulse/routes_controller.rb +46 -0
  9. data/app/helpers/rails_pulse/application_helper.rb +4 -0
  10. data/app/javascript/rails_pulse/controllers/chart_controller.js +27 -0
  11. data/app/models/rails_pulse/cards/base.rb +61 -25
  12. data/app/models/rails_pulse/dashboard/charts/response_time_percentiles.rb +38 -15
  13. data/app/models/rails_pulse/dashboard/charts/throughput_and_errors.rb +36 -13
  14. data/app/models/rails_pulse/dashboard/concerns/time_range_helper.rb +3 -0
  15. data/app/models/rails_pulse/dashboard/health_summary.rb +3 -1
  16. data/app/models/rails_pulse/dashboard/needs_attention.rb +3 -1
  17. data/app/models/rails_pulse/dashboard/storage_status.rb +38 -68
  18. data/app/models/rails_pulse/exceptions/cards/exception_rate.rb +1 -5
  19. data/app/models/rails_pulse/exceptions/cards/open_groups.rb +4 -4
  20. data/app/models/rails_pulse/exceptions/cards/total_occurrences.rb +3 -4
  21. data/app/models/rails_pulse/jobs/cards/failure_rate.rb +7 -20
  22. data/app/models/rails_pulse/jobs/cards/p95_duration.rb +5 -8
  23. data/app/models/rails_pulse/jobs/cards/total_runs.rb +4 -4
  24. data/app/models/rails_pulse/queries/cards/average_query_times.rb +6 -9
  25. data/app/models/rails_pulse/queries/cards/database_load.rb +7 -10
  26. data/app/models/rails_pulse/queries/cards/execution_rate.rb +4 -6
  27. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +5 -8
  28. data/app/models/rails_pulse/route.rb +7 -1
  29. data/app/models/rails_pulse/routes/cards/error_rates.rb +5 -11
  30. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +3 -1
  31. data/app/models/rails_pulse/routes/cards/request_count_totals.rb +4 -6
  32. data/app/models/rails_pulse/summary.rb +13 -18
  33. data/app/models/rails_pulse/time_range_preference.rb +28 -0
  34. data/app/models/rails_pulse/time_window.rb +61 -0
  35. data/app/services/rails_pulse/summary_service.rb +112 -162
  36. data/app/services/rails_pulse/tag_filter_service.rb +10 -1
  37. data/app/views/layouts/rails_pulse/_time_range_selector.html.erb +2 -2
  38. data/app/views/rails_pulse/dashboard/index.html.erb +6 -3
  39. data/app/views/rails_pulse/routes/_archived_summary_table.html.erb +46 -0
  40. data/app/views/rails_pulse/routes/_archived_table_pagination.html.erb +28 -0
  41. data/app/views/rails_pulse/routes/show.html.erb +14 -2
  42. data/lib/generators/rails_pulse/install_generator.rb +5 -2
  43. data/lib/generators/rails_pulse/templates/rails_pulse.rb +7 -2
  44. data/lib/rails_pulse/cleanup_service.rb +7 -7
  45. data/lib/rails_pulse/configuration.rb +9 -1
  46. data/lib/rails_pulse/engine.rb +14 -42
  47. data/lib/rails_pulse/middleware/request_collector.rb +2 -17
  48. data/lib/rails_pulse/standalone.rb +12 -0
  49. data/lib/rails_pulse/subscribers/operation_subscriber.rb +45 -3
  50. data/lib/rails_pulse/tracker.rb +241 -25
  51. data/lib/rails_pulse/version.rb +1 -1
  52. data/lib/rails_pulse.rb +3 -16
  53. data/lib/rails_pulse_server.ru +37 -23
  54. data/public/rails-pulse-assets/rails-pulse.js +1 -1
  55. metadata +7 -9
  56. data/app/assets/images/rails_pulse/dashboard.png +0 -0
  57. data/app/assets/images/rails_pulse/query-show.png +0 -0
  58. data/app/assets/images/rails_pulse/request-show.png +0 -0
  59. data/app/assets/images/rails_pulse/request.png +0 -0
  60. data/app/assets/images/rails_pulse/route-show.png +0 -0
  61. data/lib/rails_pulse/extensions/active_record.rb +0 -124
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50a1c2016eee54e3ceda7d6e390669b7dc3f427a637cc975a50e9d682262411a
4
- data.tar.gz: dfd20c8b99bdb1f3986aa143fb8cfe5aaf3935f7ba7cd43e4c6883721de30ad7
3
+ metadata.gz: 851f283c6c93bde26b1f79d3053a9cb7888381eedb0cd82118fdfbae6abf2a59
4
+ data.tar.gz: 622ea59d374bac1a3aa0b1dea374d86537b8bf815c1546b00c1545ee1e6787f7
5
5
  SHA512:
6
- metadata.gz: caaf4f2932c7c639e7684fe3a8f4a789b4fe2d7a6b888d9cbeaf302487319efe758b91699f0eef1372cdad385f12269a1a78d9989bf29f855c20bf86c1c322e2
7
- data.tar.gz: 852e0524002c525ca784c981c21234a750a2e5d9bc29024290c49d653df4fc806edd0c23e767b9e26be945ed81fafa37cf7474a7354e89ff533cf92a142eafda
6
+ metadata.gz: f4462e133722e0ef971e3f3c75fe0ea26f23c8167f24d31a8817550c6b91fe1cfd0bf6a35f59d29bd32cc432a9951de77fecdbb09e131fb385ddfe628d6ab660
7
+ data.tar.gz: 0c7d9706fc3a128b7c48fc4d09b78a698ff00bdd2e9b110e360c8124d5fccb5e552e4d351da70623f5d392c4c69916dbd39f930e0c1ef62eac2d3832db481cb5
data/CHANGELOG.md CHANGED
@@ -7,114 +7,91 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.4.0.pre.4] - 2026-09-07
10
+ ## [0.4.0] - 2026-09-22
11
11
 
12
- ## [0.4.0.pre.3] - 2026-09-06
12
+ ## [0.4.0] - 2026-09-22
13
13
 
14
- ### Added
15
-
16
- - **Shell-based deployment tracking.** `rake rails_pulse:record_deployment` and the new `rake rails_pulse:finish_deployment[revision]` let release scripts record and close deployments without the HTTP API or a token.
17
- - **Standalone mode improvements.** Dashboard links now resolve correctly when served at `/`, and a new `config.standalone_authentication_method` (with an HTTP Basic fallback) replaces host-app authentication, which the standalone process can't use.
18
- - **`rails rails_pulse:status`.** Reports schema, migration, and tracking/auth config state in one command, and exits 1 if anything needs action before deploying.
19
- - **Schema drift guard.** `RailsPulse::SchemaCheck` detects tables or columns the running gem expects but that haven't been migrated yet, and pauses tracking (dashboard returns 503) with a clear warning instead of erroring. Disable with `config.schema_check_enabled = false`.
20
-
21
- ### Fixed
22
-
23
- - Fixed breadcrumb links becoming protocol-relative (`//queries`) when the engine is mounted at `/`.
24
- - Fixed the standalone dashboard server 404ing on its own stylesheets and scripts.
25
- - Fixed asset responses using mixed-case headers, which `Rack::Lint` rejects under Rack 3.
26
- - Fixed `rails_pulse_server` ignoring `RAILS_ENV`/`RACK_ENV` and always booting `rackup` in development mode.
27
- - Fixed the standalone server requiring a literal `SECRET_KEY_BASE` instead of falling back to the host app's.
14
+ Route identity changes in this release and the schema migration is irreversible, so upgrading from any 0.3.x release needs a backup and a one-time data migration. Start with "Upgrading from 0.3.x" below.
28
15
 
29
- ### Security
30
-
31
- - Updated development dependencies mail, net-imap, and json for several CVEs (gem dev/CI only).
32
- - Updated development dependencies nokogiri, loofah, and rails-html-sanitizer for several CVEs (gem dev/CI only).
33
- - Updated development dependencies Rails, puma, websocket-driver, and concurrent-ruby for Dependabot's critical/high alerts. Host apps should update their own Rails to 8.1.3.1.
34
-
35
- ### Changed
36
-
37
- - `rails generate rails_pulse:upgrade` now reports unrun migrations and outstanding route backfill instead of saying everything is up to date.
38
- - Cleaned up `rake test` output — no more RDoc warnings, stray blank lines, or leaked log noise.
16
+ ### Upgrading from 0.3.x
39
17
 
40
- ## [0.4.0.pre.2] - 2026-09-04
18
+ Applies to every 0.3.x release (0.3.0 through 0.3.3). **Back up your database first.**
41
19
 
42
- ### Fixed
20
+ ```bash
21
+ bundle update rails_pulse
22
+ rails generate rails_pulse:upgrade
23
+ rails db:migrate # separate Pulse database: rails db:migrate:rails_pulse
24
+ rails rails_pulse:migrate_routes # required — schema migrate alone leaves Action empty
25
+ rails rails_pulse:status # exits 1 while anything still needs action
26
+ ```
43
27
 
44
- - Fixed `rails generate rails_pulse:upgrade` writing a migration that failed to parse when a column comment contained an escaped quote.
45
- - Fixed background tracking writes corrupting the test database connection under transactional tests. Existing installs should add `config.async = false if Rails.env.test?` to their initializer.
28
+ Then restart **all** processes together, not as a rolling deploy. A 0.3.x process left running against the migrated schema stops tracking and 500s on the routes page. If the new gem is deployed before its migrations run, tracking pauses and the dashboard answers 503 with these commands until the schema is current.
46
29
 
47
- ## [0.4.0.pre.1] - 2026-09-03
30
+ Separate-database hosts: add `schema_dump: false` to the `rails_pulse` entry in `config/database.yml` and delete `db/rails_pulse_structure.sql` if it exists. Do not run `db:setup` / `db:prepare` as a substitute for `db:migrate:rails_pulse`.
48
31
 
49
- This release contains a **breaking schema change** and requires a one-time data migration. Back up your database first — the route migration is irreversible. See "Upgrading from 0.3.x" below.
32
+ The upgrade generator appends this version's new settings to `config/initializers/rails_pulse.rb` without changing existing values; review them with `git diff`. Exception tracking is inserted as `config.track_exceptions = false`; set it to `true` once you have reviewed what is captured. Authentication is now on outside development and test, not only in production, so configure `config.authorize` before deploying to staging.
50
33
 
51
34
  ### Security
52
35
 
53
- - Job failure messages (`rails_pulse_job_runs.error_message`) are now redacted the same way exception messages are.
54
- - CSRF protection is now declared directly by the engine instead of depending on the host's `load_defaults` version.
55
- - Fixed `authentication_method` granting access when it returned a falsy-but-not-`false`/redirect value; it's now fail-closed.
56
- - Hardened EXPLAIN analysis against SQL injection and added a statement timeout.
57
- - Bounded deployment API input (revision length, metadata size, future timestamps) and capped the `rails_pulse_deployments` row count.
58
- - Backtrace source snippets are now limited to `app/`, `lib/`, and `config/routes.rb`, instead of any file under `Rails.root`.
59
- - The standalone dashboard's session cookie is now `Secure` in production.
36
+ - Job failure messages (`rails_pulse_job_runs.error_message`) are redacted the same way exception messages are.
37
+ - CSRF protection is declared directly by the engine instead of depending on the host's `load_defaults` version.
38
+ - Authentication hooks fail closed: an `authentication_method` that returns a falsy-but-not-`false` or redirect value no longer grants access, and authentication is on by default outside development and test rather than only in production.
39
+ - EXPLAIN analysis is hardened against SQL injection and runs under a statement timeout.
40
+ - Deployment API input is bounded (revision length, metadata size, future timestamps) and the deployments table is capped.
41
+ - Backtrace source snippets are limited to `app/`, `lib/` and `config/routes.rb` instead of any file under `Rails.root`.
42
+ - The standalone dashboard's session cookie is `Secure` in production.
43
+ - Development dependencies updated for several CVEs (gem dev/CI only). Host apps should keep their own Rails current.
60
44
 
61
45
  ### Added
62
46
 
63
- - **Exception tracking.** Captures unhandled exceptions from requests and jobs, with grouping, backtraces, and redacted params in a new Exceptions tab.
64
- - **`track_exceptions` config option**, off by default for existing installs and on by default for new ones.
65
- - **`capture_exception_params` config option** to include filtered request params with each exception occurrence.
66
- - **`exception_message_filter` config option** for app-specific redaction beyond the built-in rules.
67
- - **`authorize` config option** — a fail-closed predicate for gating dashboard access; now the recommended approach.
68
- - **The upgrade generator now syncs new initializer settings** into the host's config file without overwriting existing values.
47
+ - **Exception tracking.** Unhandled exceptions from requests and jobs are grouped by class and location, with backtraces and redacted params, in a new Exceptions tab. Off after an upgrade and on for new installs (`config.track_exceptions`); `config.capture_exception_params` and `config.exception_message_filter` control what is stored.
48
+ - **`config.authorize`**, a fail-closed predicate for gating dashboard access and now the recommended way to secure it.
49
+ - **Schema drift guard.** When the gem is newer than its tables (deployed before `db:migrate`, or a rolling restart), tracking pauses and the dashboard answers 503 with the upgrade commands instead of erroring on every request. `config.schema_check_enabled = false` turns it off.
50
+ - **`rails rails_pulse:status`** reports schema, migration, route backfill, initializer and summary state in one command and exits 1 when something needs action.
51
+ - **Shell-based deployment tracking.** `rails rails_pulse:record_deployment[revision]` and `rails rails_pulse:finish_deployment[revision]` record and close deployments from release scripts without the HTTP API or a token.
52
+ - **Standalone dashboard authentication.** `config.standalone_authentication_method` (HTTP Basic against `RAILS_PULSE_USERNAME` / `RAILS_PULSE_PASSWORD` by default) replaces the host hooks, which the standalone process cannot run.
53
+ - **`config.async_queue_size`** bounds the background writer queue (default 1000), and `RailsPulse::Tracker.stats` reports what was dropped.
54
+ - The upgrade generator syncs new initializer settings into the host's initializer without overwriting existing values, and reports unrun migrations and outstanding route backfill instead of saying everything is up to date.
69
55
 
70
56
  ### Changed
71
57
 
72
- - **BREAKING — route identity is now `[controller_action, path]`**, so different HTTP methods on the same path are tracked as distinct routes.
73
- - **A one-time `rails rails_pulse:migrate_routes` backfill is required** after migrating; a schema migrate alone leaves the Action column empty.
74
- - **The JavaScript bundle is 66% smaller** (2.19 MB → 759 KB) after tree-shaking ECharts.
58
+ - **BREAKING: route identity is now `[controller_action, path]`.** Different HTTP methods on the same path are tracked as distinct routes, and a one-time `rails rails_pulse:migrate_routes` backfill is required after migrating.
59
+ - **BREAKING: `rails_pulse_routes.method` is dropped and the migration is irreversible.** The HTTP verb now lives on each request. Restart all processes together after migrating.
60
+ - **One writer thread per process.** Background tracking no longer spawns a thread per request, which could exhaust the app's connection pool under a burst. A single writer drains a bounded queue on one connection and drops the newest request when the queue is full rather than slowing the app; SQL normalisation and N+1 detection run there too, off the request thread.
61
+ - **Summary aggregation upserts each period in bulk** instead of one statement per route, query and job, and runs its transaction on the Rails Pulse connection so separate-database installs cannot be left with partial summaries.
62
+ - **Dashboard assets are no longer registered with Sprockets**, which fixes `assets:precompile` running out of memory on small hosts. `rails_pulse:install_assets` copies them into `public/assets` after precompile so `config.asset_host` and CDN-only CSP keep working; remove any `rails-pulse.js` / `rails-pulse.css` entries from `config.assets.precompile`.
63
+ - Services under `app/services` autoload and reload through Zeitwerk like the rest of the engine.
64
+ - The JavaScript bundle is 66% smaller (2.19 MB to 759 KB) after tree-shaking ECharts.
65
+ - Ruby 3.1 is the minimum. The gem declared 3.0 but could not install there.
75
66
 
76
67
  ### Removed
77
68
 
78
- - **BREAKING — `rails_pulse_routes.method` is dropped**; the HTTP verb now lives on each request. Restart all processes together after migrating.
79
- - **BREAKING — the route migration is irreversible.** Back up your database first.
80
- - Removed three unused Stimulus controllers (`form`, `timezone`, `period_selector`).
81
- - Removed `theme.js`, a chart theme that was immediately overwritten and would have defeated ECharts tree-shaking.
82
- - Removed dead CSS: unused css-zero ports, old period-selector styles, and a disabled toolbox option.
83
- - `csp-test.js` is no longer shipped in the published gem.
69
+ - **BREAKING: `rails_pulse_routes.method`** (see Changed).
70
+ - `RailsPulse.warm_metric_cache!` and `RailsPulse.clear_metric_cache!`.
71
+ - The `group_by_date` / `group_by_hour` methods the gem added to the host's `ActiveRecord::Relation`.
72
+ - Three unused Stimulus controllers (`form`, `timezone`, `period_selector`), an unused chart theme, and dead CSS.
84
73
 
85
74
  ### Fixed
86
75
 
87
- - Fixed tag filters not matching tags containing `_` on SQLite.
88
- - Fixed various hand-edited or malformed query strings causing 500s instead of falling back to defaults.
89
- - Fixed chart click/zoom handlers accumulating on every chart tab switch, slowing clicks down over time.
90
- - Fixed zooming to the first column of a category chart resetting the range instead of applying it.
91
- - Fixed hover popovers throwing after a table refresh replaced the underlying element.
92
- - Fixed the time range selector's hover border being invisible due to an undefined CSS variable.
93
- - Fixed a duplicate CSS rule that made popover placement depend on file load order.
94
- - Fixed upgrading when using a separate database installation.
95
- - Separate-database installs now set `schema_dump: false` so `db:migrate` doesn't dump or load `db/rails_pulse_structure.sql` (#189).
96
- - Fixed `assets:precompile` OOMing on memory-constrained hosts by not registering dashboard assets with Sprockets.
97
- - Fixed SQLite's `schema.rb` dropping the partial unique index on unrecognised routes, which over-constrained paths on `db:schema:load`.
98
-
99
- ### Upgrading from 0.3.x
100
-
101
- Applies to every 0.3.x release (0.3.0 through 0.3.3). **Back up your database first.**
102
-
103
- ```bash
104
- bundle update rails_pulse
105
- rails generate rails_pulse:upgrade
106
- rails db:migrate # separate Pulse database: rails db:migrate:rails_pulse
107
- rails rails_pulse:migrate_routes # required — schema migrate alone leaves Action empty
108
- ```
109
-
110
- Then restart **all** processes together, not as a rolling deploy.
111
-
112
- Separate-database hosts: add `schema_dump: false` to the `rails_pulse` entry in
113
- `config/database.yml` and delete `db/rails_pulse_structure.sql` if it exists. Do not
114
- run `db:setup` / `db:prepare` as a substitute for `db:migrate:rails_pulse`.
115
-
116
- Exception tracking stays off after upgrading. Set `config.track_exceptions = true`
117
- once you have reviewed what is captured.
76
+ - **Dashboard pages issue far fewer queries.** Tag filtering runs as subqueries inside each card and chart query, the route-backfill check is a single indexed query, and table sizes are cached for five minutes.
77
+ - **Metric card sparklines are correct in time zones east of UTC.** Daily buckets follow `config.time_zone` instead of the stored UTC timestamp.
78
+ - **Charts and metric cards honour a custom date range** instead of always showing the trailing days, and custom ranges no longer render blank charts when the server's OS time zone differs from `config.time_zone`.
79
+ - A custom date range no longer 500s the dashboard on Marshal-backed session stores such as `activerecord-session_store`. (#252)
80
+ - Idle periods no longer trigger false "summary job not running" warnings; `SummaryJob` records a zero-count summary for periods with no traffic. (#250)
81
+ - The storage page reports real table sizes again instead of a leftover screenshot fixture's sample numbers.
82
+ - Dashboard status bar badges for routes, queries and jobs are clickable, matching exceptions and storage.
83
+ - Chart click/zoom handlers no longer accumulate on every tab switch, zooming to the first column of a category chart applies the range, hover popovers no longer throw after a table refresh, and the time range selector's hover border is visible.
84
+ - Tag filters match tags containing `_` on SQLite, and malformed query strings fall back to defaults instead of returning 500.
85
+ - **Lower per-query capture overhead.** The SQL, template and cache subscribers walk the call stack lazily and stop at the first app frame instead of materialising the whole stack on every event.
86
+ - Cached SQL reads are no longer captured as operations, and `config.ignored_queries` is now applied (it was validated but never consulted).
87
+ - The dashboard's own HTTP, mailer, job and storage events are no longer recorded.
88
+ - `config.logger` is honoured; a custom logger set in the initializer receives all Rails Pulse output. (#244)
89
+ - Background tracking writes no longer corrupt the test database connection under transactional tests. The initializer sets `config.async = false if Rails.env.test?`, and the tracker also writes inline whenever it detects a connection shared across threads.
90
+ - Cleanup no longer risks statement timeouts on large tables: orphan checks use a correlated `NOT EXISTS` instead of `NOT IN`, so one stalled stage no longer blocks the rest. (#253)
91
+ - **Standalone dashboard.** Settings forms no longer fail CSRF verification, the time range and filter pickers no longer 404, its own stylesheets and scripts are served, `RAILS_ENV` / `RACK_ENV` are respected, `SECRET_KEY_BASE` falls back to the host's, breadcrumb links are no longer protocol-relative when served at `/`, and the ignored-host-authentication notice is logged once per process.
92
+ - Upgrading a separate-database install works, and those installs set `schema_dump: false` so `db:migrate` does not dump or load `db/rails_pulse_structure.sql`. (#189)
93
+ - SQLite's `schema.rb` keeps the partial unique index on unrecognised routes across `db:schema:load`.
94
+ - Asset responses use lowercase headers, which `Rack::Lint` requires under Rack 3.
118
95
 
119
96
  ## [0.3.3] - 2026-06-23
120
97
 
@@ -196,8 +173,8 @@ No changelog entry — see git history.
196
173
 
197
174
  No changelog entry — see git history.
198
175
 
199
- [Unreleased]: https://github.com/railspulse/rails_pulse/compare/v0.4.0.pre.1...HEAD
200
- [0.4.0.pre.1]: https://github.com/railspulse/rails_pulse/compare/v0.3.3...v0.4.0.pre.1
176
+ [Unreleased]: https://github.com/railspulse/rails_pulse/compare/v0.4.0...HEAD
177
+ [0.4.0]: https://github.com/railspulse/rails_pulse/compare/v0.3.3...v0.4.0
201
178
  [0.3.3]: https://github.com/railspulse/rails_pulse/compare/v0.3.2...v0.3.3
202
179
  [0.3.0]: https://github.com/railspulse/rails_pulse/compare/v0.2.7...v0.3.0
203
180
  [0.2.7]: https://github.com/railspulse/rails_pulse/compare/v0.2.6...v0.2.7
data/README.md CHANGED
@@ -1,166 +1,138 @@
1
1
  <div align="center">
2
- <img src="app/assets/images/rails_pulse/rails-pulse-logo.png" alt="Rails Pulse" width="200" />
2
+ <img src="app/assets/images/rails_pulse/rails-pulse-logo.png" alt="Rails Pulse" width="160" />
3
3
  </div>
4
4
 
5
- ---
6
-
7
5
  # Rails Pulse
8
6
 
9
- **Self-hosted performance monitoring for Rails apps**
7
+ **Performance monitoring that lives inside your Rails app.** Slow requests, N+1 queries, background jobs and exceptions, stored in your own database. No agent, no account, no data leaving your servers.
10
8
 
11
9
  ![Gem Version](https://img.shields.io/gem/v/rails_pulse)
12
10
  ![Rails Version](https://img.shields.io/badge/Rails-7.2%2B-blue)
11
+ ![Ruby Version](https://img.shields.io/badge/Ruby-3.1%2B-red)
13
12
  ![License](https://img.shields.io/badge/License-MIT-green)
14
- ![Ruby Version](https://img.shields.io/badge/Ruby-3.0%2B-red)
15
13
 
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.
14
+ Rails Pulse is a Rails engine. It hooks into the instrumentation Rails already emits, writes what it sees to a handful of tables, and mounts a dashboard that shows you where the time went. Install the gem, run one migration, schedule two jobs, and you have monitoring that works the same on SQLite, PostgreSQL and MySQL.
15
+
16
+
17
+ <picture>
18
+ <source media="(prefers-color-scheme: dark)" srcset=".github/images/dashboard-dark.png">
19
+ <img src=".github/images/dashboard-light.png" alt="Rails Pulse dashboard: health bar for routes, queries, jobs, exceptions and storage; P95 response time, request rate and error rate with sparklines; response time percentiles against service level objective lines; and a ranked list of jobs and routes needing attention" width="100%">
20
+ </picture>
21
+
22
+ ## What you get
17
23
 
18
- <table border="0">
19
- <tr border="0" style="border:0">
20
- <td border="0" style="border:0">
21
- <img src="app/assets/images/rails_pulse/dashboard.png" alt="Dashboard" width="400" /></td>
22
- <td style="border:0"><img src="app/assets/images/rails_pulse/request-show.png" alt="Request detail" width="400" /></td>
24
+ - **The state of the app in one screen.** A health bar counts healthy, slow and critical routes, queries, jobs and exception groups. A ranked "needs attention" list tells you what to fix first.
25
+ - **Every request, broken down.** Each request stores its route, status, duration and a timeline of the SQL, view, cache, HTTP, mailer and Active Storage operations inside it.
26
+ - **Queries you can act on.** SQL is normalised and fingerprinted, so you see execution counts and P95 per statement shape, N+1 patterns, an EXPLAIN plan and index suggestions.
27
+ - **Jobs and exceptions in the same place.** Duration, queue wait and failure rate for every Active Job class on any adapter. Unhandled exceptions from requests and jobs grouped by class and location, with filtered params and backtraces.
28
+ - **Numbers over time.** Hourly, daily, weekly and monthly summaries with P50, P95 and P99, your service level objectives drawn as lines on the charts, and a marker for every deploy so a regression lines up with the release that caused it.
29
+ - **Built for production.** Tracking is queued off the request thread and dropped rather than blocked under load. If the gem is deployed before its migrations, tracking pauses and tells you what to run. Retention is enforced by age and by row count so the tables never grow without bound.
30
+
31
+ <table>
32
+ <tr>
33
+ <td width="50%" valign="top">
34
+ <picture>
35
+ <source media="(prefers-color-scheme: dark)" srcset=".github/images/request-dark.png">
36
+ <img src=".github/images/request-light.png" alt="Request detail: duration, status and response size, a performance breakdown by database, view and application time, and a request trace showing action, view and database operations on a timeline">
37
+ </picture>
38
+ <p align="center"><sub>A request and where its time went</sub></p>
39
+ </td>
40
+ <td width="50%" valign="top">
41
+ <picture>
42
+ <source media="(prefers-color-scheme: dark)" srcset=".github/images/query-dark.png">
43
+ <img src=".github/images/query-light.png" alt="Query diagnostics: query characteristics, an issue detected, an optimisation suggestion to add a composite index, and the execution plan">
44
+ </picture>
45
+ <p align="center"><sub>Diagnostics and an index suggestion for one query</sub></p>
46
+ </td>
23
47
  </tr>
24
48
  <tr>
25
- <td style="border:0"><img src="app/assets/images/rails_pulse/query-show.png" alt="Query detail" width="400" /></td>
26
- <td style="border:0"><img src="app/assets/images/rails_pulse/route-show.png" alt="Route detail" width="400" /></td>
49
+ <td colspan="2">
50
+ <picture>
51
+ <source media="(prefers-color-scheme: dark)" srcset=".github/images/route-dark.png">
52
+ <img src=".github/images/route-light.png" alt="Route detail: P95 response time, request rate and error rate cards, and a two-week P95 and P99 chart with service level objective lines and vertical deploy markers">
53
+ </picture>
54
+ <p align="center"><sub>One route over two weeks, with objective lines and deploy markers</sub></p>
55
+ </td>
27
56
  </tr>
28
57
  </table>
29
58
 
30
- ## Installation
31
-
32
- Add to your Gemfile:
59
+ ## Quick start
33
60
 
34
61
  ```ruby
35
- gem 'rails_pulse'
62
+ # Gemfile
63
+ gem "rails_pulse"
36
64
  ```
37
65
 
38
- Run the installer:
39
-
40
66
  ```bash
41
67
  bundle install
42
68
  rails generate rails_pulse:install
43
69
  rails db:migrate
44
70
  ```
45
71
 
46
- Mount the dashboard in `config/routes.rb`:
47
-
48
72
  ```ruby
49
- Rails.application.routes.draw do
50
- mount RailsPulse::Engine => "/rails_pulse"
51
- end
73
+ # config/routes.rb
74
+ mount RailsPulse::Engine => "/rails_pulse"
52
75
  ```
53
76
 
54
- Schedule the background jobs:
55
-
56
- ```ruby
57
- RailsPulse::SummaryJob.perform_later # cron: 5 * * * *
58
- RailsPulse::CleanupJob.perform_later # cron: 0 1 * * *
77
+ Schedule the summary job hourly and the cleanup job daily with whatever your queue adapter provides. With Solid Queue:
78
+
79
+ ```yaml
80
+ # config/recurring.yml
81
+ production:
82
+ rails_pulse_summary:
83
+ class: RailsPulse::SummaryJob
84
+ schedule: "5 * * * *"
85
+ rails_pulse_cleanup:
86
+ class: RailsPulse::CleanupJob
87
+ schedule: "0 1 * * *"
59
88
  ```
60
89
 
61
- Your dashboard is now at `http://localhost:3000/rails_pulse`.
90
+ Open `http://localhost:3000/rails_pulse`. That's the whole setup.
62
91
 
63
- ## Upgrading
92
+ Requirements: Ruby 3.1+, Rails 7.2+ (tested on 7.2, 8.0 and 8.1), SQLite, PostgreSQL or MySQL.
64
93
 
65
- From 0.3.3:
94
+ Full install guide, including a separate database and plain cron: [railspulse.com/documentation/installation](https://railspulse.com/documentation/installation)
66
95
 
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.
96
+ ## Going further
75
97
 
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
- To see where an install stands at any point — schema, unrun migration files, route backfill, initializer — run `rails rails_pulse:status`. It exits 1 when something needs action, so it can gate a deploy.
79
-
80
- Exception tracking is **off** for existing installs. The generator inserts `config.track_exceptions = false`; set it to `true` after migrating to opt in:
81
-
82
- ```ruby
83
- config.track_exceptions = true
84
- config.capture_exception_params = true # params are filtered via Rails' filter_parameters
85
- ```
86
-
87
- 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.
88
-
89
- 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`.
90
-
91
- Full install guide: [railspulse.com/documentation/installation](https://railspulse.com/documentation/installation)
92
-
93
- Separate database setup: [railspulse.com/documentation/database](https://railspulse.com/documentation/database)
94
-
95
- ## Configuration
96
-
97
- Rails Pulse works out of the box with sensible defaults. To customise, edit `config/initializers/rails_pulse.rb`:
98
+ **Lock it down.** The dashboard is authenticated by default outside development and test. Point it at your own auth with a predicate; anything but `true` is a 403.
98
99
 
99
100
  ```ruby
100
101
  RailsPulse.configure do |config|
101
- config.enabled = true
102
-
103
- config.request_thresholds = {
104
- slow: 700,
105
- very_slow: 2000,
106
- critical: 4000
107
- }
108
-
109
- config.track_jobs = true
110
- config.capture_job_arguments = false # keep false to protect sensitive data
111
-
112
- config.track_exceptions = true
113
- config.capture_exception_params = true # params are filtered via Rails' filter_parameters
114
-
115
- config.full_retention_period = 30.days
102
+ config.authorize = ->(controller) { controller.current_user&.admin? }
116
103
  end
117
104
  ```
118
105
 
119
- Full configuration reference: [railspulse.com/documentation/advanced](https://railspulse.com/documentation/advanced)
106
+ With nothing configured it falls back to HTTP Basic against `RAILS_PULSE_USERNAME` and `RAILS_PULSE_PASSWORD`. [Authentication guide](https://railspulse.com/documentation/authentication)
120
107
 
121
- ## Authentication
108
+ **Tune it.** Thresholds for slow, very slow and critical, service level objectives per percentile, what to ignore, what to tag, how long to keep. All in `config/initializers/rails_pulse.rb`. [Configuration reference](https://railspulse.com/documentation/advanced)
122
109
 
123
- 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).
110
+ **Run the dashboard on its own.** `bundle exec rails_pulse_server` serves the UI from a separate process with its own health endpoint, so a slow report never competes with your app for a thread. [Deployment modes](https://railspulse.com/documentation/deployment-modes)
124
111
 
125
- The simplest hook is a predicate that receives the controller and returns `true` to allow — anything else is a 403:
126
-
127
- ```ruby
128
- RailsPulse.configure do |config|
129
- config.authorize = ->(controller) { controller.current_user&.admin? }
130
- end
131
- ```
112
+ **Mark your deploys.** `rails rails_pulse:record_deployment[sha]` from a release script, or `POST /rails_pulse/deployments` with a token from CI, and every chart draws a line at that moment.
132
113
 
133
- If you need to redirect to a login page instead, use `authentication_method`, which runs inside the controller and denies by rendering or redirecting:
114
+ **Keep it in its own database.** `rails generate rails_pulse:install --database=separate` puts the tables somewhere your primary never has to vacuum. [Database setup](https://railspulse.com/documentation/database)
134
115
 
135
- ```ruby
136
- RailsPulse.configure do |config|
137
- config.authentication_redirect_path = "/login"
116
+ ## Upgrading
138
117
 
139
- config.authentication_method = proc {
140
- unless user_signed_in? && current_user.admin?
141
- redirect_to main_app.root_path, alert: "Access denied"
142
- end
143
- }
144
- end
118
+ ```bash
119
+ bundle update rails_pulse
120
+ rails generate rails_pulse:upgrade
121
+ rails db:migrate # separate Pulse database: rails db:migrate:rails_pulse
122
+ rails rails_pulse:status # exits 1 while anything still needs action
145
123
  ```
146
124
 
147
- 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`.
148
-
149
- Authentication guide: [railspulse.com/documentation/authentication](https://railspulse.com/documentation/authentication)
150
-
151
- ## Features
152
-
153
- - **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.
154
- - **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.
155
- - **Job tracking** — monitors background job duration, queue wait time, and failure rates. Works with any Active Job adapter.
156
- - **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.
157
- - **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.
158
- - **No data leaves your app** — everything is stored in your own database. No third-party cloud, no SaaS subscription, no outbound connections.
159
- - **Low overhead** — tracking is async and uses a thread-local flag to skip recording Rails Pulse's own internal requests.
125
+ Upgrading from 0.3.x to 0.4? **Back up first**, run `rails rails_pulse:migrate_routes` after migrating, and restart every process together. The details are in the [changelog](CHANGELOG.md).
160
126
 
161
127
  ## Contributing
162
128
 
163
- Bug reports and pull requests are welcome on [GitHub](https://github.com/railspulse/rails_pulse).
129
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/railspulse/rails_pulse). `docs/` explains how the pieces fit and why they are built the way they are.
130
+
131
+ ```bash
132
+ git config core.hooksPath .githooks # once, after cloning
133
+ DB=sqlite3 rake test # or DB=postgresql / DB=mysql2
134
+ bundle exec rubocop
135
+ ```
164
136
 
165
137
  ## License
166
138
 
@@ -24,10 +24,11 @@ module SessionFiltersConcern
24
24
  session_global_filters["disabled_tags"] || []
25
25
  end
26
26
 
27
- # Returns the time range preference from session
28
- # Can be a symbol/string for presets or a hash for custom ranges
27
+ # Returns the time range preference from session, normalized so every
28
+ # session serializer yields the same shape: a String preset name, a
29
+ # string-keyed Hash for a custom range, or nil for anything unrecognized.
29
30
  def session_time_range_preference
30
- session[:time_range_preference]
31
+ RailsPulse::TimeRangePreference.normalize(session[:time_range_preference])
31
32
  end
32
33
 
33
34
  # Returns whether deployment markers should be shown by default
@@ -41,6 +41,11 @@ module TimeRangeConcern
41
41
  end_time = Time.zone.now
42
42
  selected_time_range = default_key
43
43
 
44
+ # Normalized up front so a symbol-keyed hash from a Marshal-backed session
45
+ # store reads the same as the string-keyed one the cookie store produces;
46
+ # anything unrecognized becomes nil and falls through to later priorities.
47
+ session_preference = RailsPulse::TimeRangePreference.normalize(session[:time_range_preference])
48
+
44
49
  # Priority 1: Page-specific preset from dropdown (check this first!)
45
50
  if ransack_params[:period_start_range].present? && ransack_params[:period_start_range].to_sym != :custom
46
51
  # Predefined time range from dropdown
@@ -76,25 +81,22 @@ module TimeRangeConcern
76
81
  selected_time_range = :custom
77
82
  end
78
83
  # Priority 4: Time range selector (from session)
79
- elsif session[:time_range_preference].present?
80
- preference = session[:time_range_preference]
81
- if preference.is_a?(Hash) && preference["type"] == "custom"
82
- # Custom range from time range selector
83
- start_time = parse_time_param(preference["start_time"]) || start_time
84
- end_time = parse_time_param(preference["end_time"]) || end_time
85
- selected_time_range = :custom
86
- else
87
- # Preset from time range selector
88
- selected_time_range = preference.to_sym
89
- start_time =
90
- case selected_time_range
91
- when :last_24_hours then 1.day.ago
92
- when :last_7_days then 1.week.ago
93
- when :last_14_days then 2.weeks.ago
94
- when :last_30_days then 1.month.ago
95
- else start_time
96
- end
97
- end
84
+ elsif RailsPulse::TimeRangePreference.custom?(session_preference)
85
+ # Custom range from time range selector
86
+ start_time = parse_time_param(session_preference["start_time"]) || start_time
87
+ end_time = parse_time_param(session_preference["end_time"]) || end_time
88
+ selected_time_range = :custom
89
+ elsif session_preference.is_a?(String) && session_preference.present?
90
+ # Preset from time range selector
91
+ selected_time_range = session_preference.to_sym
92
+ start_time =
93
+ case selected_time_range
94
+ when :last_24_hours then 1.day.ago
95
+ when :last_7_days then 1.week.ago
96
+ when :last_14_days then 2.weeks.ago
97
+ when :last_30_days then 1.month.ago
98
+ else start_time
99
+ end
98
100
  # Priority 5: Global filters (from session)
99
101
  elsif session_global_filters["start_time"].present? || session_global_filters["end_time"].present?
100
102
  start_time = parse_time_param(session_global_filters["start_time"]) || start_time
@@ -105,6 +107,14 @@ module TimeRangeConcern
105
107
 
106
108
  time_diff = (end_time.to_i - start_time.to_i) / 3600.0
107
109
 
110
+ # in_time_zone before rounding: a parsed custom-range string carries the
111
+ # server OS's local offset, not Time.zone, and beginning_of_day/_hour
112
+ # round in whatever offset the receiver has. Summary data is always
113
+ # bucketed by Time.zone, so without this the boundary can miss every
114
+ # summary row and render charts empty. No-op for already-Time.zone values.
115
+ start_time = start_time.in_time_zone
116
+ end_time = end_time.in_time_zone
117
+
108
118
  if time_diff <= 25
109
119
  start_time = start_time.beginning_of_hour
110
120
  end_time = end_time.end_of_hour
@@ -86,10 +86,12 @@ module RailsPulse
86
86
  session[:time_range_preference] = preset if TIME_RANGE_PRESETS.include?(preset)
87
87
  elsif start_time && end_time
88
88
  # Store custom range
89
+ # String keys: a Marshal-backed session store would otherwise hand the
90
+ # symbol keys back verbatim, and the readers expect the JSON shape.
89
91
  session[:time_range_preference] = {
90
- type: "custom",
91
- start_time: start_time,
92
- end_time: end_time
92
+ "type" => "custom",
93
+ "start_time" => start_time,
94
+ "end_time" => end_time
93
95
  }
94
96
  end
95
97
 
@@ -172,11 +174,13 @@ module RailsPulse
172
174
  run_authentication_method(hook)
173
175
  end
174
176
 
177
+ # Logged once per process, not once per controller: the flag lives on
178
+ # RailsPulse::Standalone, so RequestsController and QueriesController do
179
+ # not each repeat it on their first request.
175
180
  def note_ignored_host_authentication(config)
176
181
  return if config.authentication_method.nil? && config.authorize.nil?
177
- return if self.class.instance_variable_get(:@_rails_pulse_standalone_auth_noted)
182
+ return unless RailsPulse.note_host_authentication_ignored!
178
183
 
179
- self.class.instance_variable_set(:@_rails_pulse_standalone_auth_noted, true)
180
184
  logger.info "RailsPulse: standalone dashboard ignores config.authentication_method / config.authorize " \
181
185
  "(host session helpers are not available here); using HTTP Basic auth. " \
182
186
  "Set config.standalone_authentication_method to customise."