rails_pulse 0.4.0.pre.6 → 0.4.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b24fa9677dc2c9b2dd4042acadb464247dd64d2136836e2197a1afe5b90f3f21
4
- data.tar.gz: 2c45d0508fb24753b4d65b87b57afb19382b82277747a532e5182d6a6e2a8bdd
3
+ metadata.gz: f1a0c55a1386d00790c4bada3da5ed8b34c17e670c7bf1a0c8c55a55c662b16c
4
+ data.tar.gz: 6de45806d2c72656dcc2ef06e61e00a7dd013e826a69a3f12f49cecf9efd360e
5
5
  SHA512:
6
- metadata.gz: 83270431a2752bb64ada4380744b5fc087696c36109bfcdd157412eb9689124e0dc8d1945371978455a8a2baefceced920ff27f82c3d311c4cd297b73f8ae52f
7
- data.tar.gz: d3c00d842cebe64770e3039e0c50b029bdde9169590857ec17526a8a05bad7abf71eecebe0fe614ebd1f726a5567d8c4070f08831ccc5847b28c0018184c9ced
6
+ metadata.gz: a946a339e72c604b0202f28f2e5650f9034619c1737f308cc14a640331a9d25ed60cfa86fc03c2c5a5bc9ab0793ab661844c93bcd1d394d0eedf7984e2fca211
7
+ data.tar.gz: ebb59bb9f9c60c0a9b5ce5ab0a778d08396a841cab960b6f4f4a16a41ca86a38e5ab54e3902b949327b84f5bb6618a7de35366993fded467e8a3b6e5e946ddc6
data/CHANGELOG.md CHANGED
@@ -7,152 +7,107 @@ 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.6] - 2026-09-20
10
+ ## [0.4.1] - 2026-09-23
11
11
 
12
- ## [0.4.0.pre.5] - 2026-09-20
13
-
14
- ### Changed
15
-
16
- - **Services now autoload through Zeitwerk.** `app/services` was hidden from the Rails autoloader and wired up by hand, so services never reloaded in development and every new one had to be registered in the engine. They now load and reload like the rest of `app/`, with the few acronym-prone file names pinned so a host's `inflect.acronym` declarations cannot change the constants the gem expects.
17
-
18
- ### Removed
19
-
20
- - `RailsPulse.warm_metric_cache!` (a no-op) and `RailsPulse.clear_metric_cache!` (used `delete_matched`, which some cache stores do not support). Neither was referenced by the dashboard.
21
-
22
- ### Fixed
23
-
24
- - **Dashboard pages issue fewer queries.** Tag filtering now runs as subqueries inside each card and chart query instead of plucking every route, query and job id first; the route-backfill check is a single indexed query once every route has an action; and the dashboard's storage headline reuses table sizes for five minutes instead of measuring every table on every load.
25
- - **Lower per-query capture overhead.** The SQL, template and cache subscribers materialised the whole call stack on every event to find the calling app file; they now walk it lazily and stop at the first app frame, which cuts the per-event cost by half to two thirds on a typical controller stack.
26
- - **Summary aggregation writes each period in a handful of statements.** Every route, query and job summary used to be found and saved individually, so an hour with a few hundred routes and queries cost over 700 SQL statements; rows are now upserted in bulk against the summaries unique index.
27
- - **Background tracking no longer spawns a thread per request.** A burst of traffic used to fan out into one writer thread per request, each holding one of the app's database connections, so the app's own request threads could wait seconds for a connection. One writer thread per process now drains a bounded queue on a single connection; when the queue is full the newest request is dropped and counted rather than slowing the app. Adds `config.async_queue_size` (default 1000), `RailsPulse::Tracker.stats`, and an exit hook that drains the queue on restart. SQL normalisation and N+1 detection have moved off the request thread as well.
28
- - **Metric card sparklines are now correct in time zones east of UTC.** Daily buckets were computed from the stored UTC timestamp, so in zones such as London, Melbourne or Tokyo every card showed each day's value a day early with the latest day at zero, and half-hour zones got empty hourly sparklines. Grouping now follows `config.time_zone`, and the gem no longer adds `group_by_date` / `group_by_hour` to the host's `ActiveRecord::Relation`.
29
- - **Summary aggregation now runs its transaction on the Rails Pulse connection.** On separate-database installs it was opened on the host's primary database, so a failure part-way through a period could leave partial summaries behind.
30
- - **The dashboard's own HTTP, mailer, job and storage events are no longer recorded.** These subscribers skipped the recursion guard that SQL and template events already honoured.
31
- - **Storage page reports real table sizes again.** A leftover screenshot fixture replaced every table's live count, size, and age with hard-coded sample numbers in any environment other than `test`.
32
- - **Standalone dashboard settings forms no longer fail CSRF verification.** The standalone server (`rails_pulse_server`) used the plain `rack-session` gem's `Rack::Session::Cookie`, which knows nothing about Rails' CSRF handling: a token generated for a form is only written into the session by `commit_csrf_token`, a hook that only Rails' own `ActionDispatch::Session::CookieStore` calls. Every generated token was silently discarded, so every submission failed verification. Switched to `ActionDispatch::Cookies` + `ActionDispatch::Session::CookieStore` (seeding the `action_dispatch.*` env Rails normally sets up before reaching the engine).
33
- - **Standalone dashboard no longer 404s on the time range and global filters pickers.** Those forms submit `POST` with a hidden `_method=patch` field — the standard verb-override trick — which the mounted engine translates via the host app's default middleware stack. The standalone server (`rails_pulse_server`) builds its own minimal Rack stack and never added `Rack::MethodOverride`, so the request reached routing as a plain `POST` and 404'd against the `PATCH`-only route.
34
- - **Cleanup no longer risks statement timeouts on large tables.** `CleanupService`'s orphan checks for queries, routes, jobs, and exception groups used a `NOT IN` subquery, which some databases (notably PostgreSQL at scale) execute by materializing and rescanning the full subquery result instead of using an index. Switched to a correlated `NOT EXISTS`, which lets the planner use an index per row. A stalled cleanup stage previously blocked all later stages, including hourly summary pruning. (#253)
35
- - **Idle periods no longer trigger false "summary job not running" warnings.** `SummaryJob` now records a zero-count overall summary for hours/days with no requests, so the dashboard banner, `rails_pulse:status`, the storage-pressure card, and count-based cleanup no longer mistake "no traffic" for "job stopped running." (#250)
36
- - **Custom date range charts on the Routes, Queries, and Jobs pages no longer render blank when the server's OS timezone differs from `config.time_zone`.** A custom range was rounded to a day/hour boundary in whatever offset the parsed time happened to carry rather than `Time.zone`, so the boundary could land hours away from where summary data is actually bucketed — every chart series came back all-nil while the metric cards and table (queried differently) kept showing data, which was the visible symptom.
37
- - **Dashboard charts and metric cards now honor a custom date range instead of always showing the trailing days.** The dashboard collapsed the selected range to a day count and had every chart, card, and sparkline re-derive "the last N days ending now" from it, so a range in the past rendered recent data under the selected range's labels, and the day count itself was truncated by integer division. Charts and cards now bucket the exact selected range.
38
- - **Custom date range no longer 500s the dashboard on Marshal-backed session stores.** The custom range was written with symbol keys but read back expecting string keys, so stores that preserve symbols (e.g. `activerecord-session_store`) broke every page until the session was cleared; both shapes are now accepted and unreadable preferences fall back to the default range. (#252)
39
- - **`config.logger` is now honored.** `RailsPulse.logger` previously ignored a custom logger set in the initializer and always wrote to the tagged `Rails.logger`; the configured logger now receives all Rails Pulse log output. (#244)
40
- - **Cached SQL reads no longer captured as operations.** Query-cache hits were going through the same stack-walk and operation-allocation path as real queries, adding measurable overhead on requests with heavy cache reuse. `config.ignored_queries` now also works — it was previously validated but never consulted when collecting SQL operations.
41
- - **Dashboard status bar badges are now all clickable.** Routes, Queries, and Jobs badges link to their respective pages, matching Exceptions and Storage.
42
- - **Standalone auth notice logged once per process.** The "standalone dashboard ignores config.authentication_method / config.authorize" notice kept its once-only flag on each controller class, so it repeated for every engine controller a visitor reached. The flag now lives on `RailsPulse::Standalone` and the notice is logged once per process.
43
-
44
- ## [0.4.0.pre.4] - 2026-09-07
45
-
46
- ### Fixed
47
-
48
- - Dependency and packaging fixes only; no user-facing changes beyond 0.4.0.pre.3.
12
+ ### Added
49
13
 
50
- ## [0.4.0.pre.3] - 2026-09-06
14
+ - A test that fails, naming the key, when a default in `RailsPulse::Configuration` and the install template drift apart. (#272)
51
15
 
52
- ### Added
16
+ ### Changed
53
17
 
54
- - **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.
55
- - **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.
56
- - **`rails rails_pulse:status`.** Reports schema, migration, and tracking/auth config state in one command, and exits 1 if anything needs action before deploying.
57
- - **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`.
18
+ - Documented what a tracked request costs, measured against the bounded writer: about 25 microseconds of request-thread overhead per request on SQLite and PostgreSQL. (#275)
19
+ - The release gate and CI now fail if anything under `app/` branches on `Rails.env`, so a code path the test suite cannot reach can no longer ship. (#273)
20
+ - Loosened the `ransack` dependency from `~> 4.0` to `>= 4.0, < 6` so host apps can upgrade to ransack 5 without dropping Rails Pulse.
21
+ - `rake test` now refuses to run when the dummy database was last set up for a different adapter, instead of loading that adapter's `schema.rb` and failing two index tests. (#276)
58
22
 
59
23
  ### Fixed
60
24
 
61
- - Fixed breadcrumb links becoming protocol-relative (`//queries`) when the engine is mounted at `/`.
62
- - Fixed the standalone dashboard server 404ing on its own stylesheets and scripts.
63
- - Fixed asset responses using mixed-case headers, which `Rack::Lint` rejects under Rack 3.
64
- - Fixed `rails_pulse_server` ignoring `RAILS_ENV`/`RACK_ENV` and always booting `rackup` in development mode.
65
- - Fixed the standalone server requiring a literal `SECRET_KEY_BASE` instead of falling back to the host app's.
66
-
67
- ### Security
25
+ - SQL normalization no longer raises `Regexp::TimeoutError` on long queries, which could fail the whole request when N+1 detection ran during collection.
26
+ - **Metric card headline numbers now honour the global tag filter.** Disabling a tag changed each card's sparkline but not the number beside it, so the two disagreed. Both are now computed over the same summaries. (#274)
68
27
 
69
- - Updated development dependencies mail, net-imap, and json for several CVEs (gem dev/CI only).
70
- - Updated development dependencies nokogiri, loofah, and rails-html-sanitizer for several CVEs (gem dev/CI only).
71
- - 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.
28
+ ## [0.4.0] - 2026-09-22
72
29
 
73
- ### Changed
30
+ 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.
74
31
 
75
- - `rails generate rails_pulse:upgrade` now reports unrun migrations and outstanding route backfill instead of saying everything is up to date.
76
- - Cleaned up `rake test` output — no more RDoc warnings, stray blank lines, or leaked log noise.
32
+ ### Upgrading from 0.3.x
77
33
 
78
- ## [0.4.0.pre.2] - 2026-09-04
34
+ Applies to every 0.3.x release (0.3.0 through 0.3.3). **Back up your database first.**
79
35
 
80
- ### Fixed
36
+ ```bash
37
+ bundle update rails_pulse
38
+ rails generate rails_pulse:upgrade
39
+ rails db:migrate # separate Pulse database: rails db:migrate:rails_pulse
40
+ rails rails_pulse:migrate_routes # required — schema migrate alone leaves Action empty
41
+ rails rails_pulse:status # exits 1 while anything still needs action
42
+ ```
81
43
 
82
- - Fixed `rails generate rails_pulse:upgrade` writing a migration that failed to parse when a column comment contained an escaped quote.
83
- - 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.
44
+ 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.
84
45
 
85
- ## [0.4.0.pre.1] - 2026-09-03
46
+ 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`.
86
47
 
87
- 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.
48
+ 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.
88
49
 
89
50
  ### Security
90
51
 
91
- - Job failure messages (`rails_pulse_job_runs.error_message`) are now redacted the same way exception messages are.
92
- - CSRF protection is now declared directly by the engine instead of depending on the host's `load_defaults` version.
93
- - Fixed `authentication_method` granting access when it returned a falsy-but-not-`false`/redirect value; it's now fail-closed.
94
- - Hardened EXPLAIN analysis against SQL injection and added a statement timeout.
95
- - Bounded deployment API input (revision length, metadata size, future timestamps) and capped the `rails_pulse_deployments` row count.
96
- - Backtrace source snippets are now limited to `app/`, `lib/`, and `config/routes.rb`, instead of any file under `Rails.root`.
97
- - The standalone dashboard's session cookie is now `Secure` in production.
52
+ - Job failure messages (`rails_pulse_job_runs.error_message`) are redacted the same way exception messages are.
53
+ - CSRF protection is declared directly by the engine instead of depending on the host's `load_defaults` version.
54
+ - 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.
55
+ - EXPLAIN analysis is hardened against SQL injection and runs under a statement timeout.
56
+ - Deployment API input is bounded (revision length, metadata size, future timestamps) and the deployments table is capped.
57
+ - Backtrace source snippets are limited to `app/`, `lib/` and `config/routes.rb` instead of any file under `Rails.root`.
58
+ - The standalone dashboard's session cookie is `Secure` in production.
59
+ - Development dependencies updated for several CVEs (gem dev/CI only). Host apps should keep their own Rails current.
98
60
 
99
61
  ### Added
100
62
 
101
- - **Exception tracking.** Captures unhandled exceptions from requests and jobs, with grouping, backtraces, and redacted params in a new Exceptions tab.
102
- - **`track_exceptions` config option**, off by default for existing installs and on by default for new ones.
103
- - **`capture_exception_params` config option** to include filtered request params with each exception occurrence.
104
- - **`exception_message_filter` config option** for app-specific redaction beyond the built-in rules.
105
- - **`authorize` config option** — a fail-closed predicate for gating dashboard access; now the recommended approach.
106
- - **The upgrade generator now syncs new initializer settings** into the host's config file without overwriting existing values.
63
+ - **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.
64
+ - **`config.authorize`**, a fail-closed predicate for gating dashboard access and now the recommended way to secure it.
65
+ - **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.
66
+ - **`rails rails_pulse:status`** reports schema, migration, route backfill, initializer and summary state in one command and exits 1 when something needs action.
67
+ - **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.
68
+ - **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.
69
+ - **`config.async_queue_size`** bounds the background writer queue (default 1000), and `RailsPulse::Tracker.stats` reports what was dropped.
70
+ - 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.
107
71
 
108
72
  ### Changed
109
73
 
110
- - **BREAKING — route identity is now `[controller_action, path]`**, so different HTTP methods on the same path are tracked as distinct routes.
111
- - **A one-time `rails rails_pulse:migrate_routes` backfill is required** after migrating; a schema migrate alone leaves the Action column empty.
112
- - **The JavaScript bundle is 66% smaller** (2.19 MB → 759 KB) after tree-shaking ECharts.
74
+ - **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.
75
+ - **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.
76
+ - **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.
77
+ - **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.
78
+ - **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`.
79
+ - Services under `app/services` autoload and reload through Zeitwerk like the rest of the engine.
80
+ - The JavaScript bundle is 66% smaller (2.19 MB to 759 KB) after tree-shaking ECharts.
81
+ - Ruby 3.1 is the minimum. The gem declared 3.0 but could not install there.
113
82
 
114
83
  ### Removed
115
84
 
116
- - **BREAKING — `rails_pulse_routes.method` is dropped**; the HTTP verb now lives on each request. Restart all processes together after migrating.
117
- - **BREAKING — the route migration is irreversible.** Back up your database first.
118
- - Removed three unused Stimulus controllers (`form`, `timezone`, `period_selector`).
119
- - Removed `theme.js`, a chart theme that was immediately overwritten and would have defeated ECharts tree-shaking.
120
- - Removed dead CSS: unused css-zero ports, old period-selector styles, and a disabled toolbox option.
121
- - `csp-test.js` is no longer shipped in the published gem.
85
+ - **BREAKING: `rails_pulse_routes.method`** (see Changed).
86
+ - `RailsPulse.warm_metric_cache!` and `RailsPulse.clear_metric_cache!`.
87
+ - The `group_by_date` / `group_by_hour` methods the gem added to the host's `ActiveRecord::Relation`.
88
+ - Three unused Stimulus controllers (`form`, `timezone`, `period_selector`), an unused chart theme, and dead CSS.
122
89
 
123
90
  ### Fixed
124
91
 
125
- - Fixed tag filters not matching tags containing `_` on SQLite.
126
- - Fixed various hand-edited or malformed query strings causing 500s instead of falling back to defaults.
127
- - Fixed chart click/zoom handlers accumulating on every chart tab switch, slowing clicks down over time.
128
- - Fixed zooming to the first column of a category chart resetting the range instead of applying it.
129
- - Fixed hover popovers throwing after a table refresh replaced the underlying element.
130
- - Fixed the time range selector's hover border being invisible due to an undefined CSS variable.
131
- - Fixed a duplicate CSS rule that made popover placement depend on file load order.
132
- - Fixed upgrading when using a separate database installation.
133
- - Separate-database installs now set `schema_dump: false` so `db:migrate` doesn't dump or load `db/rails_pulse_structure.sql` (#189).
134
- - Fixed `assets:precompile` OOMing on memory-constrained hosts by not registering dashboard assets with Sprockets.
135
- - Fixed SQLite's `schema.rb` dropping the partial unique index on unrecognised routes, which over-constrained paths on `db:schema:load`.
136
-
137
- ### Upgrading from 0.3.x
138
-
139
- Applies to every 0.3.x release (0.3.0 through 0.3.3). **Back up your database first.**
140
-
141
- ```bash
142
- bundle update rails_pulse
143
- rails generate rails_pulse:upgrade
144
- rails db:migrate # separate Pulse database: rails db:migrate:rails_pulse
145
- rails rails_pulse:migrate_routes # required — schema migrate alone leaves Action empty
146
- ```
147
-
148
- Then restart **all** processes together, not as a rolling deploy.
149
-
150
- Separate-database hosts: add `schema_dump: false` to the `rails_pulse` entry in
151
- `config/database.yml` and delete `db/rails_pulse_structure.sql` if it exists. Do not
152
- run `db:setup` / `db:prepare` as a substitute for `db:migrate:rails_pulse`.
153
-
154
- Exception tracking stays off after upgrading. Set `config.track_exceptions = true`
155
- once you have reviewed what is captured.
92
+ - **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.
93
+ - **Metric card sparklines are correct in time zones east of UTC.** Daily buckets follow `config.time_zone` instead of the stored UTC timestamp.
94
+ - **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`.
95
+ - A custom date range no longer 500s the dashboard on Marshal-backed session stores such as `activerecord-session_store`. (#252)
96
+ - Idle periods no longer trigger false "summary job not running" warnings; `SummaryJob` records a zero-count summary for periods with no traffic. (#250)
97
+ - The storage page reports real table sizes again instead of a leftover screenshot fixture's sample numbers.
98
+ - Dashboard status bar badges for routes, queries and jobs are clickable, matching exceptions and storage.
99
+ - 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.
100
+ - Tag filters match tags containing `_` on SQLite, and malformed query strings fall back to defaults instead of returning 500.
101
+ - **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.
102
+ - Cached SQL reads are no longer captured as operations, and `config.ignored_queries` is now applied (it was validated but never consulted).
103
+ - The dashboard's own HTTP, mailer, job and storage events are no longer recorded.
104
+ - `config.logger` is honoured; a custom logger set in the initializer receives all Rails Pulse output. (#244)
105
+ - 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.
106
+ - 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)
107
+ - **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.
108
+ - 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)
109
+ - SQLite's `schema.rb` keeps the partial unique index on unrecognised routes across `db:schema:load`.
110
+ - Asset responses use lowercase headers, which `Rack::Lint` requires under Rack 3.
156
111
 
157
112
  ## [0.3.3] - 2026-06-23
158
113
 
@@ -234,8 +189,8 @@ No changelog entry — see git history.
234
189
 
235
190
  No changelog entry — see git history.
236
191
 
237
- [Unreleased]: https://github.com/railspulse/rails_pulse/compare/v0.4.0.pre.1...HEAD
238
- [0.4.0.pre.1]: https://github.com/railspulse/rails_pulse/compare/v0.3.3...v0.4.0.pre.1
192
+ [Unreleased]: https://github.com/railspulse/rails_pulse/compare/v0.4.0...HEAD
193
+ [0.4.0]: https://github.com/railspulse/rails_pulse/compare/v0.3.3...v0.4.0
239
194
  [0.3.3]: https://github.com/railspulse/rails_pulse/compare/v0.3.2...v0.3.3
240
195
  [0.3.0]: https://github.com/railspulse/rails_pulse/compare/v0.2.7...v0.3.0
241
196
  [0.2.7]: https://github.com/railspulse/rails_pulse/compare/v0.2.6...v0.2.7
data/README.md CHANGED
@@ -89,7 +89,7 @@ production:
89
89
 
90
90
  Open `http://localhost:3000/rails_pulse`. That's the whole setup.
91
91
 
92
- Requirements: Ruby 3.1+, Rails 7.1+ (tested on 7.2, 8.0 and 8.1), SQLite, PostgreSQL or MySQL. Until 0.4.0 ships, pin the pre-release with `gem "rails_pulse", "~> 0.4.0.pre"`.
92
+ Requirements: Ruby 3.1+, Rails 7.2+ (tested on 7.2, 8.0 and 8.1), SQLite, PostgreSQL or MySQL.
93
93
 
94
94
  Full install guide, including a separate database and plain cron: [railspulse.com/documentation/installation](https://railspulse.com/documentation/installation)
95
95
 
@@ -62,10 +62,10 @@ module RailsPulse
62
62
  period_start: range_start..now
63
63
  )
64
64
 
65
- # Apply tag filters if available
66
- if respond_to?(:disabled_tags, true) && respond_to?(:show_non_tagged, true)
67
- query = query.with_tag_filters(@disabled_tags, @show_non_tagged)
68
- end
65
+ # The same tag filter the sparkline applies, so the headline number and
66
+ # the sparkline beside it are computed over the same summaries. Cards
67
+ # receive the filter as instance variables; nil means unfiltered.
68
+ query = query.with_tag_filters(@disabled_tags || [], @show_non_tagged != false)
69
69
 
70
70
  # Filter to specific resource if provided
71
71
  query = query.where(summarizable_id: subject_id) if subject_id
@@ -1,5 +1,15 @@
1
1
  module RailsPulse
2
2
  class SqlQueryNormalizer
3
+ # Above this length, skip the regex-based literal scan in favor of a
4
+ # slower-per-char but backtracking-free manual scan. Regex matching here
5
+ # is linear (benchmarked at ~0.4s for an 8,000,000-char literal under a
6
+ # 1s Regexp.timeout), but a host app can configure Regexp.timeout far
7
+ # below that, and very long queries are rare enough that the fast path
8
+ # isn't worth the risk. 100_000 chars leaves generous headroom below
9
+ # where even an aggressive timeout config could plausibly trip — see
10
+ # issue #286.
11
+ LONG_QUERY_THRESHOLD = 100_000
12
+
3
13
  # Smart normalization: preserve table/column names, replace only literal values
4
14
  def self.normalize(query_string)
5
15
  new(query_string).normalize
@@ -72,6 +82,14 @@ module RailsPulse
72
82
  def replace_literal_values(query)
73
83
  normalized = query.dup
74
84
 
85
+ # Replace string literals (single quotes) FIRST so a very long literal
86
+ # collapses to a single "?" before any other regex in this method has
87
+ # to scan across it — see LONG_QUERY_THRESHOLD and issue #286.
88
+ normalized = replace_quoted_literals(normalized, "'")
89
+
90
+ # Replace double-quoted string literals (not protected identifiers)
91
+ normalized = replace_quoted_literals(normalized, '"')
92
+
75
93
  # Replace floating-point numbers FIRST (before integers) to avoid double replacement
76
94
  normalized = normalized.gsub(/(?<![a-zA-Z_])\b\d+\.\d+\b(?![a-zA-Z_])/, "?")
77
95
 
@@ -79,18 +97,78 @@ module RailsPulse
79
97
  # Negative lookbehind/lookahead prevents replacing numbers in table/column names
80
98
  normalized = normalized.gsub(/(?<![a-zA-Z_])\b\d+\b(?![a-zA-Z_])/, "?")
81
99
 
82
- # Replace string literals (single quotes)
83
- normalized = normalized.gsub(/'(?:[^']|'')*'/, "?")
84
-
85
- # Replace double-quoted string literals (not protected identifiers)
86
- normalized = normalized.gsub(/"(?:[^"]|"")*"/, "?")
87
-
88
100
  # Handle boolean literals
89
101
  normalized = normalized.gsub(/\b(true|false)\b/i, "?")
90
102
 
91
103
  normalized
92
104
  end
93
105
 
106
+ # Replaces quote_char-delimited string literals with a single placeholder.
107
+ # Uses a regex for the common case (fast — see benchmarks in
108
+ # LONG_QUERY_THRESHOLD's comment) and falls back to a manual scan for
109
+ # very long queries, where that regex risks Regexp::TimeoutError under
110
+ # an aggressively configured Regexp.timeout — see issue #286.
111
+ def replace_quoted_literals(query, quote_char)
112
+ if query.length < LONG_QUERY_THRESHOLD
113
+ pattern = quote_char == "'" ? /'(?:[^']|'')*'/ : /"(?:[^"]|"")*"/
114
+ query.gsub(pattern, "?")
115
+ else
116
+ scan_quoted_literals(query, quote_char)
117
+ end
118
+ end
119
+
120
+ # Linear, non-regex scan used above LONG_QUERY_THRESHOLD. A quote left
121
+ # unterminated for the rest of the string is left untouched rather than
122
+ # rescanned from every possible start position, which is what would make
123
+ # a naive manual reimplementation quadratic.
124
+ def scan_quoted_literals(query, quote_char)
125
+ result = +""
126
+ i = 0
127
+ len = query.length
128
+
129
+ while i < len
130
+ char = query[i]
131
+
132
+ unless char == quote_char
133
+ result << char
134
+ i += 1
135
+ next
136
+ end
137
+
138
+ close_index = find_closing_quote(query, i + 1, quote_char)
139
+
140
+ if close_index
141
+ result << "?"
142
+ i = close_index + 1
143
+ else
144
+ result << query[i..]
145
+ break
146
+ end
147
+ end
148
+
149
+ result
150
+ end
151
+
152
+ # Returns the index of the unescaped closing quote_char starting the
153
+ # search at `start`, treating a doubled quote_char (e.g. '') as an
154
+ # escaped literal quote rather than a terminator. Returns nil if the
155
+ # string is never closed.
156
+ def find_closing_quote(query, start, quote_char)
157
+ j = start
158
+ len = query.length
159
+
160
+ while j < len
161
+ if query[j] == quote_char
162
+ return j unless query[j + 1] == quote_char
163
+ j += 2
164
+ else
165
+ j += 1
166
+ end
167
+ end
168
+
169
+ nil
170
+ end
171
+
94
172
  def handle_special_constructs(query)
95
173
  normalized = normalize_in_clauses(query)
96
174
  normalized.gsub(/\bBETWEEN\s+\?\s+AND\s+\?/i, "BETWEEN ? AND ?")
@@ -69,7 +69,7 @@
69
69
  </div>
70
70
 
71
71
  <div class="flex items-center gap">
72
- <%= link_to rails_pulse.root_path, class: "flex items-center gap mie-2" do %>
72
+ <%= link_to rails_pulse.root_path, class: "flex items-center gap mie-2", title: "Rails Pulse v#{RailsPulse::VERSION}" do %>
73
73
  <span class="font-bold">Rails Pulse</span>
74
74
  <% end %>
75
75
  <nav class="flex items-center gap text-sm text-subtle show@md" style="--column-gap: 1rem">
@@ -1,3 +1,3 @@
1
1
  module RailsPulse
2
- VERSION = "0.4.0.pre.6"
2
+ VERSION = "0.4.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_pulse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0.pre.6
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Harvey
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-20 00:00:00.000000000 Z
11
+ date: 2026-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -48,16 +48,22 @@ dependencies:
48
48
  name: ransack
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - "~>"
51
+ - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '4.0'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '6'
54
57
  type: :runtime
55
58
  prerelease: false
56
59
  version_requirements: !ruby/object:Gem::Requirement
57
60
  requirements:
58
- - - "~>"
61
+ - - ">="
59
62
  - !ruby/object:Gem::Version
60
63
  version: '4.0'
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '6'
61
67
  - !ruby/object:Gem::Dependency
62
68
  name: sqlite3
63
69
  requirement: !ruby/object:Gem::Requirement
@@ -545,7 +551,7 @@ metadata:
545
551
  bug_tracker_uri: https://github.com/railspulse/rails_pulse/issues
546
552
  rubygems_mfa_required: 'true'
547
553
  post_install_message: |
548
- Rails Pulse 0.4.0.pre.6 installed.
554
+ Rails Pulse 0.4.1 installed.
549
555
 
550
556
  UPGRADING from any 0.3.x? This release changes route identity and DROPS
551
557
  rails_pulse_routes.method. Back up your database first, then run: