rails_pulse 0.4.0.pre.2 → 0.4.0.pre.4

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: b102fc4776d7fe701b47011e02d682f47a2784500103f3cb7d7598a757b174ff
4
- data.tar.gz: 34f4055047c1bb9151c9e811f14c4e90fe83e52508c61ea4433929b2b431d568
3
+ metadata.gz: 50a1c2016eee54e3ceda7d6e390669b7dc3f427a637cc975a50e9d682262411a
4
+ data.tar.gz: dfd20c8b99bdb1f3986aa143fb8cfe5aaf3935f7ba7cd43e4c6883721de30ad7
5
5
  SHA512:
6
- metadata.gz: 974fcf20833ab11e46a00db96d21590f32a699a73de8d2af229d1fa5559f0817c107ba684f4ada34cdb067a77f6293ecc11ee3d24f7911f9fe074353a2991d79
7
- data.tar.gz: 507afdae03f11fef23d0bbd7cc5ee8e8123656e41efbc219a10f8b6682602bfb7dcfa91e9ac424352d68ad05b3bceed4070eead220d053713f56aa1ade7bec01
6
+ metadata.gz: caaf4f2932c7c639e7684fe3a8f4a789b4fe2d7a6b888d9cbeaf302487319efe758b91699f0eef1372cdad385f12269a1a78d9989bf29f855c20bf86c1c322e2
7
+ data.tar.gz: 852e0524002c525ca784c981c21234a750a2e5d9bc29024290c49d653df4fc806edd0c23e767b9e26be945ed81fafa37cf7474a7354e89ff533cf92a142eafda
data/CHANGELOG.md CHANGED
@@ -7,185 +7,94 @@ 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
11
+
12
+ ## [0.4.0.pre.3] - 2026-09-06
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.
28
+
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.
39
+
10
40
  ## [0.4.0.pre.2] - 2026-09-04
11
41
 
12
42
  ### Fixed
13
43
 
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`.
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.
36
46
 
37
47
  ## [0.4.0.pre.1] - 2026-09-03
38
48
 
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.
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.
42
50
 
43
51
  ### Security
44
52
 
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.
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.
80
60
 
81
61
  ### Added
82
62
 
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`.
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.
107
69
 
108
70
  ### Changed
109
71
 
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.
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.
124
75
 
125
76
  ### Removed
126
77
 
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.
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.
148
84
 
149
85
  ### Fixed
150
86
 
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.
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.
178
94
  - 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.
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`.
189
98
 
190
99
  ### Upgrading from 0.3.x
191
100
 
@@ -209,21 +118,19 @@ once you have reviewed what is captured.
209
118
 
210
119
  ## [0.3.3] - 2026-06-23
211
120
 
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
121
+ - **Deployment tracking** — Record deployments via `POST /rails_pulse/deployments` or `rake rails_pulse:record_deployment[sha]`, shown as marker lines on performance charts.
122
+ - **`deployment_api_token` config option** — Secures the deployments endpoint with a token header for CI/CD use.
123
+ - Charts now use a native ECharts time axis instead of a separate labels array, enabling deployment markers and better zoom behaviour.
215
124
 
216
125
  ## [0.3.0] - 2026-04-30
217
126
 
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.
127
+ This is the largest release to date — a full UI overhaul with switchable charts and
128
+ automatic health status across the dashboard. Existing installs need to run two new
129
+ migrations.
223
130
 
224
131
  ### Added
225
132
 
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)
133
+ - **New chart types across all sections** — Routes, queries, and jobs now have dedicated, switchable chart panels (percentiles, volume, error/failure rate, duration)
227
134
  - **Dashboard health summary** — New `HealthSummary` model surfaces an overall health status and highlights routes/queries/jobs that need attention
228
135
  - **Dashboard "Needs Attention" section** — Automatically surfaces slow routes, high error rates, and problematic jobs without manual digging
229
136
  - **Storage pressure indicator** — Tracks and displays database storage growth so you know when to adjust retention settings
@@ -249,16 +156,16 @@ releases will be smaller and more incremental than this one.
249
156
 
250
157
  ### Changed
251
158
 
252
- - **Dashboard redesigned** — Full dashboard overhaul with health summary, attention sections, and chart panels replacing the previous card layout
159
+ - **Dashboard redesigned** — Full overhaul with health summary, attention sections, and chart panels replacing the previous card layout
253
160
  - **Routes, queries, and jobs index pages redesigned** — Consistent layout with metric cards, switchable chart tabs, and paginated tables
254
161
  - **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)
162
+ - **Application controller refactored** — Split into focused concerns (`ChartTableConcern`, `MetricCardConcern`, `PaginationConcern`, `SessionFiltersConcern`, `TagFilterConcern`, `TimeRangeConcern`, `ZoomRangeConcern`)
163
+ - **`normalized_sql` column expanded to `text`** — Removes the 1000-character limit on PostgreSQL and MySQL (migration provided)
257
164
  - **Diagnostic fields added to queries table** — New columns captured at instrumentation time
258
165
  - **Summary job and service refactored** — Cleaner separation between job scheduling and summary calculation logic
259
166
  - **Cleanup job refactored** — Now uses `CleanupTaskRunner` and reports statistics via `CleanupStatsReporter`
260
167
  - **Configuration expanded** — New configuration options exposed in the initializer template
261
- - **Upgrade generator simplified** — Generator now delegates to `MigrationInstaller` and `ConfigInstaller`
168
+ - **Upgrade generator simplified** — Now delegates to `MigrationInstaller` and `ConfigInstaller`
262
169
  - **Asset server middleware updated** — Improved asset serving reliability
263
170
  - **Request collector middleware updated** — Performance improvements and cleaner instrumentation
264
171
  - **Operation subscriber refactored** — Cleaner event handling and reduced complexity
@@ -269,10 +176,9 @@ releases will be smaller and more incremental than this one.
269
176
 
270
177
  - `StatusHelper` removed — status rendering consolidated into model concerns and view components
271
178
  - `ChartFormatters` helper removed — chart formatting moved into chart model classes
272
- - `chart_formatters_test.rb` removed alongside the helper
273
179
  - Slow queries and slow routes table models removed — replaced by the Needs Attention system
274
180
  - 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
181
+ - `requests/tables/index.rb` removed — requests index now uses a shared approach
276
182
  - `routes/cards/average_response_times` and `routes/charts/average_response_times` removed — replaced by percentile-based equivalents
277
183
  - `jobs/cards/average_duration` and `jobs/cards/total_jobs` removed — replaced by updated cards
278
184
  - Timezone controller removed
data/README.md CHANGED
@@ -75,6 +75,8 @@ Restart all processes after migrate. This release changes how routes are stored
75
75
 
76
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
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
+
78
80
  Exception tracking is **off** for existing installs. The generator inserts `config.track_exceptions = false`; set it to `true` after migrating to opt in:
79
81
 
80
82
  ```ruby
@@ -15,6 +15,7 @@ module RailsPulse
15
15
  protect_from_forgery with: :exception
16
16
 
17
17
  before_action :authenticate_rails_pulse_user!
18
+ before_action :require_current_schema!
18
19
  before_action :set_onboarding_state
19
20
  before_action :load_deployment_markers
20
21
 
@@ -132,6 +133,8 @@ module RailsPulse
132
133
  config = RailsPulse.configuration
133
134
  return unless config.authentication_enabled
134
135
 
136
+ return authenticate_standalone!(config) if RailsPulse.standalone?
137
+
135
138
  if config.authentication_method.nil? && config.authorize.nil?
136
139
  return fallback_http_basic_auth
137
140
  end
@@ -144,7 +147,39 @@ module RailsPulse
144
147
  run_authorize_predicate(config.authorize) if config.authorize
145
148
  rescue StandardError => e
146
149
  logger.warn "RailsPulse authentication failed: #{e.message}"
147
- redirect_to config.authentication_redirect_path
150
+ # The redirect target is a host app page; from the standalone server
151
+ # it would point at a route this process does not serve.
152
+ if RailsPulse.standalone?
153
+ render plain: "Forbidden", status: :forbidden
154
+ else
155
+ redirect_to config.authentication_redirect_path
156
+ end
157
+ end
158
+
159
+ # The standalone server has the host's models but not its middleware:
160
+ # no host session store, Warden, or Devise helpers, and a different
161
+ # hostname so no shared cookie. A session-based `authentication_method`
162
+ # or `authorize` would raise (rescued above into a useless redirect) or,
163
+ # worse, be written so that it silently allows. Ignore both here; use
164
+ # `standalone_authentication_method` when set, else HTTP Basic.
165
+ def authenticate_standalone!(config)
166
+ hook = config.standalone_authentication_method
167
+ if hook.nil?
168
+ note_ignored_host_authentication(config)
169
+ return fallback_http_basic_auth
170
+ end
171
+
172
+ run_authentication_method(hook)
173
+ end
174
+
175
+ def note_ignored_host_authentication(config)
176
+ return if config.authentication_method.nil? && config.authorize.nil?
177
+ return if self.class.instance_variable_get(:@_rails_pulse_standalone_auth_noted)
178
+
179
+ self.class.instance_variable_set(:@_rails_pulse_standalone_auth_noted, true)
180
+ logger.info "RailsPulse: standalone dashboard ignores config.authentication_method / config.authorize " \
181
+ "(host session helpers are not available here); using HTTP Basic auth. " \
182
+ "Set config.standalone_authentication_method to customise."
148
183
  end
149
184
 
150
185
  def run_authentication_method(hook)
@@ -196,6 +231,30 @@ module RailsPulse
196
231
  end
197
232
  end
198
233
 
234
+ # New gem code against un-migrated tables would otherwise 500 on every
235
+ # page that touches a missing column. Runs after authentication so the
236
+ # schema report is not exposed to anonymous callers.
237
+ def require_current_schema!
238
+ return if RailsPulse::SchemaCheck.current?
239
+
240
+ RailsPulse::SchemaCheck.warn_once!
241
+ missing = RailsPulse::SchemaCheck.missing
242
+
243
+ respond_to do |format|
244
+ format.html do
245
+ render template: "rails_pulse/shared/schema_outdated", layout: false,
246
+ status: :service_unavailable, locals: { missing: missing }
247
+ end
248
+ format.any do
249
+ render json: {
250
+ error: "Rails Pulse #{RailsPulse::VERSION} requires a schema upgrade",
251
+ missing: missing,
252
+ instructions: RailsPulse::SchemaCheck::UPGRADE_INSTRUCTIONS
253
+ }, status: :service_unavailable
254
+ end
255
+ end
256
+ end
257
+
199
258
  def load_deployment_markers
200
259
  @deployment_markers = []
201
260
  @show_deployment_markers = session_show_deployment_markers
@@ -36,10 +36,7 @@ module RailsPulse
36
36
  end
37
37
 
38
38
  def finish
39
- deployment = RailsPulse::Deployment
40
- .where(revision: finish_params[:revision])
41
- .order(started_at: :desc)
42
- .first
39
+ deployment = RailsPulse::Deployment.latest_for_revision(finish_params[:revision])
43
40
 
44
41
  return render json: { status: "error", error: "Deployment not found" }, status: :not_found unless deployment
45
42
 
@@ -30,13 +30,16 @@ module RailsPulse
30
30
  # Only keep segments after the mount point
31
31
  path_segments = path_segments[(mount_end_index + 1)..-1]
32
32
 
33
- # Build the engine root path directly from mount segments (avoids relying on named route helper)
34
- engine_root = "/" + mount_segments.join("/")
33
+ # Build the engine root path directly from mount segments (avoids relying on named route helper).
34
+ # Served at "/" (standalone server, or a host that mounts the engine at root) the prefix
35
+ # is empty, not "/": segments are appended as "/#{segment}", and "/" + "/queries" would be
36
+ # "//queries" — a protocol-relative URL that browsers resolve to a host called "queries".
37
+ engine_root = mount_segments.empty? ? "" : "/" + mount_segments.join("/")
35
38
 
36
39
  # Start with the Home link
37
40
  crumbs = [ {
38
41
  title: "Home",
39
- path: engine_root,
42
+ path: engine_root.empty? ? "/" : engine_root,
40
43
  current: path_segments.empty?
41
44
  } ]
42
45
 
@@ -18,6 +18,12 @@ module RailsPulse
18
18
  scope :for_range, ->(start_time, end_time) { where(started_at: start_time..end_time) }
19
19
  scope :recent, -> { order(started_at: :desc) }
20
20
 
21
+ # A revision can be deployed more than once (rollbacks, re-deploys); both
22
+ # the finish API endpoint and rails_pulse:finish_deployment close this row.
23
+ def self.latest_for_revision(revision)
24
+ where(revision: revision).order(started_at: :desc).first
25
+ end
26
+
21
27
  def self.ransackable_attributes(auth_object = nil)
22
28
  %w[id revision started_at finished_at created_at]
23
29
  end
@@ -19,6 +19,8 @@ module RailsPulse
19
19
 
20
20
  class << self
21
21
  def capture(exception, request_url: nil, request_method: nil, request_params: nil, environment: nil, deploy_sha: nil)
22
+ return unless RailsPulse::SchemaCheck.tracking_allowed?
23
+
22
24
  new(exception, request_url: request_url, request_method: request_method, request_params: request_params, environment: environment, deploy_sha: deploy_sha).capture
23
25
  end
24
26
 
@@ -6,13 +6,9 @@ module RailsPulse
6
6
 
7
7
  def call
8
8
  results = { updated: 0, skipped: 0, already_set: 0, merged: 0 }
9
- total = RailsPulse::Route.count
10
- processed = 0
11
9
  RailsPulse::Route.find_each do |route|
12
10
  process_route(route, results)
13
- processed += 1
14
11
  end
15
- puts "" if total > 0
16
12
  consolidate_unrecognized_duplicates(results)
17
13
  results
18
14
  end
@@ -0,0 +1,96 @@
1
+ <%# Rendered with layout: false from ApplicationController#require_current_schema!.
2
+ Mirrors the application layout's setup screen (same head, stylesheet and
3
+ setup-* classes) without depending on the before_actions or table reads the
4
+ real layout needs — the schema behind this page is, by definition, not one
5
+ this gem version can query. %>
6
+ <!DOCTYPE html>
7
+ <html data-color-scheme="light">
8
+ <head>
9
+ <title>Schema upgrade required — Rails Pulse</title>
10
+ <meta name="viewport" content="width=device-width,initial-scale=1">
11
+ <%= csp_meta_tag %>
12
+
13
+ <script nonce="<%= rails_pulse_csp_nonce %>">
14
+ (function() {
15
+ var storageKey = 'color-scheme';
16
+ var root = document.documentElement;
17
+ var scheme = 'light';
18
+
19
+ try {
20
+ var stored = localStorage.getItem(storageKey);
21
+ if (stored === 'dark' || stored === 'light') {
22
+ scheme = stored;
23
+ } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
24
+ scheme = 'dark';
25
+ }
26
+ } catch (error) {
27
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
28
+ scheme = 'dark';
29
+ }
30
+ }
31
+
32
+ root.setAttribute('data-color-scheme', scheme);
33
+ })();
34
+ </script>
35
+
36
+ <%# tag.link/script (not stylesheet_link_tag): middleware fallback paths must stay off asset_host. %>
37
+ <%= tag.link rel: "stylesheet", href: rails_pulse.asset_path("rails-pulse.css") %>
38
+ <%= tag.script "", src: rails_pulse.asset_path("rails-pulse-icons.js"), defer: true, nonce: rails_pulse_csp_nonce %>
39
+ </head>
40
+
41
+ <body class="header-layout">
42
+ <main id="main">
43
+ <div class="container flex flex-col gap">
44
+ <div class="setup-screen">
45
+ <div class="setup-screen__brand">
46
+ <%= tag.img src: rails_pulse.asset_path("rails-pulse-logo.png"),
47
+ width: 72,
48
+ height: 72,
49
+ alt: "Rails Pulse",
50
+ class: "setup-screen__logo" %>
51
+ <p class="setup-screen__brand-name">Rails Pulse</p>
52
+ </div>
53
+
54
+ <div class="setup-banner setup-banner--negative">
55
+ <p class="setup-banner__title">Rails Pulse <%= RailsPulse::VERSION %> needs a schema upgrade</p>
56
+ <p class="setup-banner__body">
57
+ The gem in this process is newer than the Rails Pulse tables it is connected to.
58
+ Request, job and exception tracking is paused, and the dashboard is unavailable,
59
+ until the database is migrated. Missing from the database:
60
+ </p>
61
+ <ul class="setup-banner__body">
62
+ <% missing.each do |table, columns| %>
63
+ <li>
64
+ <code><%= table %></code>
65
+ <% if columns == [ :table ] %>
66
+ — table does not exist
67
+ <% else %>
68
+ — columns: <%= safe_join(columns.map { |column| tag.code(column) }, ", ") %>
69
+ <% end %>
70
+ </li>
71
+ <% end %>
72
+ </ul>
73
+ <p class="setup-banner__body">Run these from the application root, then restart every process (web and workers) together:</p>
74
+ <% RailsPulse::SchemaCheck::UPGRADE_INSTRUCTIONS.each do |instruction| %>
75
+ <% command, _, comment = instruction.partition("#") %>
76
+ <code class="setup-banner__command"><span class="setup-banner__command-prefix">$</span>
77
+ <%= command.strip %><% if comment.present? %>
78
+ <span class="setup-banner__command-prefix"># <%= comment.strip %></span><% end %></code>
79
+ <% end %>
80
+ <p class="setup-banner__body">
81
+ A rolling restart that leaves older processes running against the migrated schema
82
+ breaks tracking the other way round. Back up the database first: the route identity
83
+ migration is irreversible.
84
+ </p>
85
+ </div>
86
+
87
+ <%= link_to "View upgrade docs →",
88
+ "https://railspulse.com/documentation/upgrade/",
89
+ class: "setup-screen__docs",
90
+ target: "_blank",
91
+ rel: "noopener" %>
92
+ </div>
93
+ </div>
94
+ </main>
95
+ </body>
96
+ </html>
@@ -21,5 +21,14 @@ unless args.any? { |arg| arg.start_with?("-p") || arg == "--port" }
21
21
  args.unshift("-p", port)
22
22
  end
23
23
 
24
+ # rackup's own environment defaults to "development" regardless of RAILS_ENV,
25
+ # and in that mode it wraps the app in Rack::Lint and Rack::ShowExceptions.
26
+ # Follow RACK_ENV, then RAILS_ENV, so a production dashboard is served without
27
+ # the development middleware (an explicit -E/--env still wins).
28
+ rack_env = ENV["RACK_ENV"].to_s.empty? ? ENV["RAILS_ENV"] : ENV["RACK_ENV"]
29
+ unless rack_env.to_s.empty? || args.any? { |arg| arg.start_with?("-E") || arg == "--env" }
30
+ args.unshift("-E", rack_env)
31
+ end
32
+
24
33
  # Execute rackup with the server file
25
34
  exec("rackup", rackup_file, *args)
@@ -80,6 +80,8 @@ module RailsPulse
80
80
  migrations_paths: db/rails_pulse_migrate
81
81
  schema_dump: false
82
82
 
83
+ Repeat this entry for each environment (development, test, production).
84
+
83
85
  schema_dump: false is required so Rails does not dump or load
84
86
  db/rails_pulse_structure.sql. Keep database_tasks enabled so
85
87
  rails db:migrate:rails_pulse still runs upgrades.