rails_pulse 0.4.2 → 0.5.0.pre.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 +4 -4
- data/CHANGELOG.md +33 -1
- data/README.md +15 -4
- data/app/assets/stylesheets/rails_pulse/components/chart.css +11 -0
- data/app/assets/stylesheets/rails_pulse/components/descriptive_list.css +19 -1
- data/app/assets/stylesheets/rails_pulse/components/row.css +3 -17
- data/app/controllers/concerns/chart_table_concern.rb +28 -69
- data/app/controllers/concerns/deployment_markers_concern.rb +3 -1
- data/app/controllers/concerns/metric_card_concern.rb +6 -7
- data/app/controllers/rails_pulse/api/v1/base_controller.rb +130 -0
- data/app/controllers/rails_pulse/api/v1/capabilities_controller.rb +27 -0
- data/app/controllers/rails_pulse/api/v1/coverage_controller.rb +158 -0
- data/app/controllers/rails_pulse/api/v1/deployments_controller.rb +24 -0
- data/app/controllers/rails_pulse/api/v1/exceptions_controller.rb +76 -0
- data/app/controllers/rails_pulse/api/v1/job_runs_controller.rb +34 -0
- data/app/controllers/rails_pulse/api/v1/jobs_controller.rb +172 -0
- data/app/controllers/rails_pulse/api/v1/queries_controller.rb +114 -0
- data/app/controllers/rails_pulse/api/v1/requests_controller.rb +58 -0
- data/app/controllers/rails_pulse/api/v1/routes_controller.rb +98 -0
- data/app/controllers/rails_pulse/application_controller.rb +1 -1
- data/app/controllers/rails_pulse/dashboard_controller.rb +20 -20
- data/app/controllers/rails_pulse/deployments_controller.rb +23 -13
- data/app/controllers/rails_pulse/exceptions_controller.rb +10 -12
- data/app/controllers/rails_pulse/jobs_controller.rb +1 -2
- data/app/controllers/rails_pulse/queries_controller.rb +6 -5
- data/app/controllers/rails_pulse/requests_controller.rb +15 -14
- data/app/controllers/rails_pulse/routes_controller.rb +4 -8
- data/app/helpers/rails_pulse/application_helper.rb +15 -0
- data/app/helpers/rails_pulse/chart_helper.rb +9 -2
- data/app/helpers/rails_pulse/formatting_helper.rb +11 -12
- data/app/javascript/rails_pulse/controllers/chart_controller.js +80 -43
- data/app/jobs/rails_pulse/backfill_summaries_job.rb +4 -2
- data/app/models/concerns/rails_pulse/has_metadata.rb +15 -0
- data/app/models/concerns/rails_pulse/taggable.rb +5 -12
- data/app/models/rails_pulse/cards/base.rb +20 -11
- data/app/models/rails_pulse/charts/base.rb +2 -6
- data/app/models/rails_pulse/charts/percentile_chart_base.rb +3 -4
- data/app/models/rails_pulse/dashboard/charts/response_time_percentiles.rb +4 -5
- data/app/models/rails_pulse/dashboard/charts/throughput_and_errors.rb +4 -5
- data/app/models/rails_pulse/dashboard/concerns/time_range_helper.rb +1 -2
- data/app/models/rails_pulse/dashboard/health_summary.rb +30 -3
- data/app/models/rails_pulse/dashboard/needs_attention.rb +2 -3
- data/app/models/rails_pulse/dashboard/storage_pressure.rb +34 -4
- data/app/models/rails_pulse/dashboard/storage_status.rb +36 -0
- data/app/models/rails_pulse/deployment.rb +2 -7
- data/app/models/rails_pulse/event.rb +40 -0
- data/app/models/rails_pulse/exception_occurrence.rb +1 -1
- data/app/models/rails_pulse/exceptions/cards/exception_rate.rb +3 -6
- data/app/models/rails_pulse/exceptions/cards/open_groups.rb +1 -4
- data/app/models/rails_pulse/exceptions/cards/total_occurrences.rb +3 -6
- data/app/models/rails_pulse/exceptions/charts/occurrence_volume.rb +4 -5
- data/app/models/rails_pulse/job_run.rb +1 -1
- data/app/models/rails_pulse/jobs/cards/failure_rate.rb +8 -14
- data/app/models/rails_pulse/jobs/cards/p95_duration.rb +8 -14
- data/app/models/rails_pulse/jobs/cards/total_runs.rb +5 -11
- data/app/models/rails_pulse/jobs/charts/duration.rb +1 -1
- data/app/models/rails_pulse/jobs/charts/execution_volume.rb +1 -1
- data/app/models/rails_pulse/jobs/charts/failure_rate.rb +1 -1
- data/app/models/rails_pulse/like_pattern.rb +29 -0
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +6 -12
- data/app/models/rails_pulse/queries/cards/database_load.rb +0 -9
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +5 -11
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +6 -12
- data/app/models/rails_pulse/queries/charts/database_load.rb +5 -6
- data/app/models/rails_pulse/queries/charts/execution_volume.rb +1 -1
- data/app/models/rails_pulse/request.rb +1 -1
- data/app/models/rails_pulse/routes/cards/error_rates.rb +5 -15
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +5 -17
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +5 -17
- data/app/models/rails_pulse/routes/charts/error_rate.rb +1 -1
- data/app/models/rails_pulse/routes/charts/request_volume.rb +1 -1
- data/app/models/rails_pulse/summary.rb +5 -0
- data/app/models/rails_pulse/tables/base.rb +5 -6
- data/app/models/rails_pulse/time_range.rb +309 -0
- data/app/models/rails_pulse/writer_heartbeat.rb +86 -0
- data/app/serializers/rails_pulse/api/v1/deployment_serializer.rb +20 -0
- data/app/serializers/rails_pulse/api/v1/exception_group_serializer.rb +23 -0
- data/app/serializers/rails_pulse/api/v1/exception_occurrence_serializer.rb +22 -0
- data/app/serializers/rails_pulse/api/v1/job_run_serializer.rb +28 -0
- data/app/serializers/rails_pulse/api/v1/job_serializer.rb +26 -0
- data/app/serializers/rails_pulse/api/v1/query_serializer.rb +25 -0
- data/app/serializers/rails_pulse/api/v1/request_serializer.rb +21 -0
- data/app/serializers/rails_pulse/api/v1/route_serializer.rb +19 -0
- data/app/services/rails_pulse/exception_capture_service.rb +1 -1
- data/app/services/rails_pulse/operations/comparison.rb +1 -1
- data/app/services/rails_pulse/summary_service/from_child_periods.rb +161 -0
- data/app/services/rails_pulse/summary_service/from_raw_rows.rb +115 -0
- data/app/services/rails_pulse/summary_service/metrics.rb +38 -0
- data/app/services/rails_pulse/summary_service.rb +121 -133
- data/app/services/rails_pulse/tag_filter_service.rb +5 -5
- data/app/views/layouts/rails_pulse/_time_range_selector.html.erb +4 -1
- data/app/views/rails_pulse/components/_operation_details_popover.html.erb +1 -1
- data/app/views/rails_pulse/components/_table_head.html.erb +2 -2
- data/app/views/rails_pulse/dashboard/_chart_panel.html.erb +6 -1
- data/app/views/rails_pulse/dashboard/_deployments_panel.html.erb +2 -2
- data/app/views/rails_pulse/dashboard/_health_badge.html.erb +29 -20
- data/app/views/rails_pulse/dashboard/index.html.erb +16 -2
- data/app/views/rails_pulse/jobs/_chart_tabs.html.erb +1 -0
- data/app/views/rails_pulse/jobs/index.html.erb +1 -1
- data/app/views/rails_pulse/queries/_chart_tabs.html.erb +1 -0
- data/app/views/rails_pulse/queries/index.html.erb +1 -1
- data/app/views/rails_pulse/queries/show.html.erb +1 -1
- data/app/views/rails_pulse/requests/index.html.erb +1 -1
- data/app/views/rails_pulse/routes/_chart_tabs.html.erb +1 -0
- data/app/views/rails_pulse/routes/index.html.erb +1 -1
- data/app/views/rails_pulse/routes/show.html.erb +1 -1
- data/app/views/rails_pulse/shared/_aggregation_zone_badge.html.erb +4 -0
- data/app/views/rails_pulse/storage/show.html.erb +37 -0
- data/config/routes.rb +19 -0
- data/db/rails_pulse_migrate/20260506000001_create_rails_pulse_exceptions.rb +1 -1
- data/db/rails_pulse_migrate/20260921000001_create_rails_pulse_events.rb +21 -0
- data/db/rails_pulse_schema.rb +20 -2
- data/exe/rails-pulse +7 -0
- data/lib/generators/rails_pulse/base_methods.rb +1 -0
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +20 -2
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +36 -13
- data/lib/rails_pulse/cleanup_service.rb +26 -20
- data/lib/rails_pulse/cli/agent_files/agents.md +87 -0
- data/lib/rails_pulse/cli/agent_files/claude_skill.md +165 -0
- data/lib/rails_pulse/cli/base_command.rb +37 -0
- data/lib/rails_pulse/cli/client.rb +64 -0
- data/lib/rails_pulse/cli/config.rb +87 -0
- data/lib/rails_pulse/cli/configure.rb +75 -0
- data/lib/rails_pulse/cli/coverage.rb +91 -0
- data/lib/rails_pulse/cli/deployments.rb +40 -0
- data/lib/rails_pulse/cli/exceptions.rb +111 -0
- data/lib/rails_pulse/cli/formatter.rb +31 -0
- data/lib/rails_pulse/cli/install.rb +145 -0
- data/lib/rails_pulse/cli/job_runs.rb +53 -0
- data/lib/rails_pulse/cli/jobs.rb +71 -0
- data/lib/rails_pulse/cli/main.rb +72 -0
- data/lib/rails_pulse/cli/mcp.rb +47 -0
- data/lib/rails_pulse/cli/queries.rb +62 -0
- data/lib/rails_pulse/cli/requests.rb +49 -0
- data/lib/rails_pulse/cli/routes.rb +66 -0
- data/lib/rails_pulse/configuration.rb +23 -1
- data/lib/rails_pulse/current.rb +11 -0
- data/lib/rails_pulse/engine.rb +12 -3
- data/lib/rails_pulse/job_run_collector.rb +15 -15
- data/lib/rails_pulse/mcp/server.rb +76 -0
- data/lib/rails_pulse/mcp/tools/coverage.rb +96 -0
- data/lib/rails_pulse/mcp/tools/deployments.rb +80 -0
- data/lib/rails_pulse/mcp/tools/endpoint.rb +167 -0
- data/lib/rails_pulse/mcp/tools/errors.rb +95 -0
- data/lib/rails_pulse/mcp/tools/exception_detail.rb +118 -0
- data/lib/rails_pulse/mcp/tools/exceptions.rb +114 -0
- data/lib/rails_pulse/mcp/tools/helpers.rb +162 -0
- data/lib/rails_pulse/mcp/tools/jobs.rb +155 -0
- data/lib/rails_pulse/mcp/tools/queries.rb +136 -0
- data/lib/rails_pulse/mcp/tools/routes.rb +97 -0
- data/lib/rails_pulse/mcp/tools/slow_requests.rb +118 -0
- data/lib/rails_pulse/middleware/request_collector.rb +9 -9
- data/lib/rails_pulse/scoped_inflector.rb +35 -0
- data/lib/rails_pulse/statistics.rb +27 -0
- data/lib/rails_pulse/subscribers/exception_subscriber.rb +2 -2
- data/lib/rails_pulse/subscribers/operation_subscriber.rb +15 -29
- data/lib/rails_pulse/tasks/status_reporter.rb +26 -0
- data/lib/rails_pulse/tracker.rb +75 -14
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/rails_pulse.rb +0 -4
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +13 -13
- metadata +96 -24
- data/app/controllers/concerns/page_timings.rb +0 -10
- data/app/controllers/concerns/response_range_concern.rb +0 -45
- data/app/controllers/concerns/time_range_concern.rb +0 -152
- data/app/controllers/concerns/zoom_range_concern.rb +0 -86
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3a494a9f4ff4c1f388ed3f87f6911cfae02b9efb329673f1b697b8af5c589f4a
|
|
4
|
+
data.tar.gz: bd9fc701eccf980bca1c8aa1269dc5549fa3882f905c8691cb9fb70cc842b399
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 642d5edc8eb02e9b7d4b5e393995ebd84782f2068eef36e075820ce9fed47db44ec3ce8bce4fec076e9a4d2b055a95b7c390309ab83dc51a3c13f2c4babc8608
|
|
7
|
+
data.tar.gz: adcc2d2f7ce7176cbadd781abe14cc13df14002a4ba5e48f178c2853074118a0996d0c09e752077b65cf5c4eab77387f49a5ffff5b45b08422a460fab7a4f95a
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,39 @@ All notable changes to Rails Pulse will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.5.0.pre.1] - 2026-10-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **JSON API, `rails-pulse` CLI and MCP server.** A read-only API under `/rails_pulse/api/v1`, a `rails-pulse` executable and an MCP server (`rails-pulse mcp`; add `gem "mcp"` to your Gemfile) give scripts, CI and coding agents what the dashboard shows: routes, requests, queries, jobs, job runs, exceptions and deployments. (#309)
|
|
15
|
+
- **Coverage reporting for agents.** `rails-pulse coverage show` and the `rails_pulse_coverage` tool report what has been recorded, how recently, any collection gaps, and which application and environment answered, so missing data is not mistaken for an all-clear. (#309)
|
|
16
|
+
- **Fixed windows and drilldown.** Every windowed command and tool takes ISO 8601 `since`/`until` and reports the window it measured, and `rails_pulse_queries` takes an endpoint's `route_id` to show the SQL inside it with the file and line each query came from. (#309)
|
|
17
|
+
- **Agent instructions.** `rails-pulse install claude` installs a Claude Code skill, and `rails-pulse install agents` writes a Rails Pulse section into `AGENTS.md`, alongside a project's own instructions with `--append`. (#309)
|
|
18
|
+
- `rails rails_pulse:status` reports whether the API and deployment tokens are set. (#309)
|
|
19
|
+
- **Dropped requests are now visible.** Each background writer records a heartbeat once a minute (queue depth and requests dropped since the last one), pruned after a day. The dashboard's health bar gains a Tracking badge, shown only when a writer is backlogged or dropping requests; the Storage page lists every live writer with its queue and drops; and `rails rails_pulse:status` reports the totals and exits 1 when anything was dropped in the last hour. (#281)
|
|
20
|
+
- **A `rails_pulse_events` table** for what Rails Pulse notices rather than measures, starting with the writer heartbeats above. `config.event_retention_period` (default 90 days) prunes it. Run `rails generate rails_pulse:upgrade` and migrate; tracking pauses until the table exists.
|
|
21
|
+
- CI now exercises the separate-database upgrade path (`bin/test_separate_database_upgrade`, SQLite and PostgreSQL), and the migration regression suite gains a 0.3.2 baseline. (#284)
|
|
22
|
+
- **Charts say which time zone they are in.** Every chart carries a zone badge (hover for the full zone name and the exact window shown), chart tooltips end with the zone, and the custom date range picker states the zone its inputs are read in. (#303)
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **Daily, weekly and monthly summaries are built from shorter periods instead of raw rows.** The summary job's memory no longer grows with the length of the period or with raised retention caps, and longer periods stay complete after raw data is pruned; their P50/P95/P99 are now traffic-weighted from the hours or days below them, as the dashboard already shows for multi-period ranges. A period is summarized only once it ends, so the backfill task no longer writes a partial row for the current day. (#279)
|
|
27
|
+
- **`config.deployment_api_token` is now `config.deployment_token`.** The old name still works and still only records deployments; the JSON API reads a separate `config.api_token`, so a token given to a coding agent cannot record a release. (#309)
|
|
28
|
+
- **Recording a deployment over HTTP requires `config.deployment_token`.** The deployments endpoint no longer accepts a dashboard login in place of the token, which let any page a signed-in admin visited record a release; set the token for CI, or use the `rails_pulse:record_deployment` task, which needs none. (#309)
|
|
29
|
+
- **All timestamps display in the app's `config.time_zone`.** Chart axes and tooltips are formatted in that zone rather than the browser's, so a daily point no longer lands on the wrong calendar day for viewers in another zone. Request, job, exception and operation timestamps also use it instead of the server's OS zone; on a host whose server runs in UTC with a different `config.time_zone`, those pages now show the configured zone. (#303)
|
|
30
|
+
- **Dashboard health bar badges omit zero counts.** "26 healthy · 0 slow · 0 critical" now reads "26 healthy"; the Storage badge is shown only under warning or critical pressure.
|
|
31
|
+
- **Dropped the `request_store` runtime dependency.** Per-request tracking state now goes through `RailsPulse::Current`, built on Rails' own `ActiveSupport::CurrentAttributes`. No configuration or behavior change; a host that read `RequestStore.store[:rails_pulse_request_id]` directly (undocumented, but reachable) needs to switch to `RailsPulse::Current.rails_pulse_request_id`. (#277)
|
|
32
|
+
- **Requires Ruby 3.2+ and Rails 7.2+.** The gemspec advertised Ruby 3.1 and Rails 7.1 but CI never ran them; the floors now match what is tested, and the untested Rails 7.1 and Ruby 3.1 code paths are gone. (#270)
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Weekly and monthly summaries written by `SummaryJob` now include the period's last day. (#279)
|
|
37
|
+
- Backfill date arguments are read in the app's time zone, so backfilled summaries land on the same period boundaries the scheduled job writes instead of UTC midnights. (#279)
|
|
38
|
+
- **The exception-group row cap no longer counts preserved and ignored groups.** Once those exempt groups approached the cap, every cleanup run deleted the oldest deletable groups without ever getting under it. The cap now applies to deletable groups only. (#285)
|
|
39
|
+
|
|
40
|
+
## [0.4.2] - 2026-10-01
|
|
9
41
|
|
|
10
42
|
### Changed
|
|
11
43
|
|
data/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|

|
|
11
|
-

|
|
12
12
|

|
|
13
13
|
|
|
14
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.
|
|
@@ -27,6 +27,7 @@ Rails Pulse is a Rails engine. It hooks into the instrumentation Rails already e
|
|
|
27
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
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
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
|
+
- **Your coding agent can read it.** A read-only JSON API, a `rails-pulse` CLI and an MCP server give Claude Code, Codex, Cursor or a CI script the same data: the slowest endpoints since the last deploy, the queries behind them, the jobs that failed. Ask the agent why checkout got slow and it can go and look.
|
|
30
31
|
|
|
31
32
|
<table>
|
|
32
33
|
<tr>
|
|
@@ -89,7 +90,7 @@ production:
|
|
|
89
90
|
|
|
90
91
|
Open `http://localhost:3000/rails_pulse`. That's the whole setup.
|
|
91
92
|
|
|
92
|
-
Requirements: Ruby 3.
|
|
93
|
+
Requirements: Ruby 3.2+, Rails 7.2+ (tested on 7.2, 8.0 and 8.1), SQLite, PostgreSQL or MySQL.
|
|
93
94
|
|
|
94
95
|
Full install guide, including a separate database and plain cron: [railspulse.com/documentation/installation](https://railspulse.com/documentation/installation)
|
|
95
96
|
|
|
@@ -109,7 +110,17 @@ With nothing configured it falls back to HTTP Basic against `RAILS_PULSE_USERNAM
|
|
|
109
110
|
|
|
110
111
|
**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)
|
|
111
112
|
|
|
112
|
-
**Mark your deploys.** `rails rails_pulse:record_deployment[sha]` from a release script, or `POST /rails_pulse/deployments` with
|
|
113
|
+
**Mark your deploys.** `rails rails_pulse:record_deployment[sha]` from a release script, or `POST /rails_pulse/deployments` with the API token from CI, and every chart draws a line at that moment.
|
|
114
|
+
|
|
115
|
+
**Brief your agent.** Set `config.api_token`, then on your machine:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
rails-pulse configure # URL and token, saved to ~/.rails-pulse
|
|
119
|
+
rails-pulse routes list --since 2026-06-01T00:00:00Z
|
|
120
|
+
rails-pulse install claude # Claude Code skill: when and how to use the tools
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Add `gem "mcp"` to your Gemfile (a development group is enough), register `rails-pulse mcp` as an MCP server, and the agent gets ten read-only tools: routes, slow requests, errors, exception groups and their backtraces, one endpoint in depth, expensive and N+1 queries, job health, deployments, and what has actually been recorded. Nothing the agent can call changes production. [Agent tooling](https://railspulse.com/documentation/mcp)
|
|
113
124
|
|
|
114
125
|
**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)
|
|
115
126
|
|
|
@@ -126,7 +137,7 @@ Upgrading from 0.3.x to 0.4? **Back up first**, run `rails rails_pulse:migrate_r
|
|
|
126
137
|
|
|
127
138
|
## Contributing
|
|
128
139
|
|
|
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.
|
|
140
|
+
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. Building on top of Rails Pulse (a plugin, scripting the CLI)? [`docs/api.md`](docs/api.md) states what's public and stable across minor releases.
|
|
130
141
|
|
|
131
142
|
```bash
|
|
132
143
|
git config core.hooksPath .githooks # once, after cloning
|
|
@@ -85,6 +85,17 @@
|
|
|
85
85
|
border-left: none;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/* Aggregation-zone badge beside a chart title or tab strip. Its title
|
|
89
|
+
attribute carries the full zone name and the resolved window, hence the
|
|
90
|
+
help cursor. */
|
|
91
|
+
.zone-badge {
|
|
92
|
+
font-size: var(--text-xs);
|
|
93
|
+
font-weight: var(--font-normal);
|
|
94
|
+
color: var(--color-text-subtle);
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
cursor: help;
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
.panel-tab {
|
|
89
100
|
display: inline-flex;
|
|
90
101
|
align-items: center;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/* @container rules match descendants of the container, not the container
|
|
2
|
+
itself, so the containment context has to live on the parent. */
|
|
3
|
+
:has(> .descriptive-list) {
|
|
4
|
+
container-type: inline-size;
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
.descriptive-list {
|
|
2
8
|
display: grid;
|
|
3
9
|
grid-template-columns: 200px 1fr;
|
|
@@ -6,9 +12,21 @@
|
|
|
6
12
|
|
|
7
13
|
.descriptive-list dt, .descriptive-list dd {
|
|
8
14
|
font-size: var(--text-sm);
|
|
15
|
+
min-width: 0;
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
.descriptive-list dd {
|
|
12
19
|
overflow-wrap: break-word;
|
|
13
|
-
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Below this the fixed 200px label column crowds out the value column */
|
|
23
|
+
@container (max-width: 340px) {
|
|
24
|
+
.descriptive-list {
|
|
25
|
+
grid-template-columns: 1fr;
|
|
26
|
+
gap: 0.125rem 0.5rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.descriptive-list dd {
|
|
30
|
+
margin-block-end: 0.5rem;
|
|
31
|
+
}
|
|
14
32
|
}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
flex: 1;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/*
|
|
26
|
+
/* Single column below 768px */
|
|
27
27
|
@media (max-width: 768px) {
|
|
28
28
|
.row {
|
|
29
29
|
display: flex;
|
|
@@ -34,35 +34,21 @@
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
.row > * {
|
|
37
|
-
flex: 0 0
|
|
37
|
+
flex: 0 0 100%;
|
|
38
38
|
min-width: 0;
|
|
39
39
|
height: auto;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
.row > .grid-item {
|
|
43
43
|
height: auto;
|
|
44
|
+
min-height: auto;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
.row > .grid-item > * {
|
|
47
48
|
flex: none;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
/* Tables and chart panels should stack in single column on tablet */
|
|
51
|
-
.row:has(.table-container) > *,
|
|
52
|
-
.row:has(.chart-container) > * {
|
|
53
|
-
flex: 0 0 100%;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/* Single column for very small screens */
|
|
57
51
|
@media (max-width: 480px) {
|
|
58
|
-
.row > * {
|
|
59
|
-
flex: 0 0 100%;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.row > .grid-item {
|
|
63
|
-
min-height: auto;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
52
|
/* Make metric cards more compact on mobile */
|
|
67
53
|
.row > .grid-item .card {
|
|
68
54
|
padding: var(--size-3);
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Core concern for controllers that display both charts and tables (routes, requests, queries, jobs).
|
|
4
4
|
# Orchestrates time range setup, chart generation, table queries, and zoom/filter handling.
|
|
5
|
-
# Includes TimeRangeConcern, ResponseRangeConcern, and ZoomRangeConcern for filter management.
|
|
6
5
|
#
|
|
7
6
|
# Controllers including this concern must implement:
|
|
8
7
|
# - chart_model, table_model, chart_definitions, default_table_sort, build_table_results
|
|
@@ -13,9 +12,7 @@ module ChartTableConcern
|
|
|
13
12
|
VALID_PERIOD_TYPES = %w[hour day].freeze
|
|
14
13
|
|
|
15
14
|
included do
|
|
16
|
-
include
|
|
17
|
-
include ResponseRangeConcern
|
|
18
|
-
include ZoomRangeConcern
|
|
15
|
+
include RansackParamsConcern
|
|
19
16
|
include DeploymentMarkersConcern
|
|
20
17
|
|
|
21
18
|
before_action :setup_page_timings
|
|
@@ -24,32 +21,16 @@ module ChartTableConcern
|
|
|
24
21
|
|
|
25
22
|
private
|
|
26
23
|
|
|
24
|
+
# Hook: override to change the default time range (e.g. :last_7_days).
|
|
25
|
+
def default_time_range_key = :last_24_hours
|
|
26
|
+
|
|
27
27
|
def setup_page_timings
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@page_timings = PageTimings.new(
|
|
34
|
-
start_time: start_time, end_time: end_time,
|
|
35
|
-
table_start_time: table_start_time, table_end_time: table_end_time,
|
|
36
|
-
zoom_start: zoom_start, zoom_end: zoom_end,
|
|
37
|
-
time_diff_hours: time_diff_hours, start_duration: start_duration,
|
|
38
|
-
selected_time_range: selected_time_range, selected_response_range: selected_response_range
|
|
28
|
+
@time_range = RailsPulse::TimeRange.resolve(
|
|
29
|
+
params: params,
|
|
30
|
+
session: session,
|
|
31
|
+
default_key: default_time_range_key,
|
|
32
|
+
duration_range_type: duration_range_type
|
|
39
33
|
)
|
|
40
|
-
|
|
41
|
-
# Keep individual @vars for backward compat with views, helpers, and other
|
|
42
|
-
# concerns (DeploymentMarkersConcern, MetricCardConcern, ChartHelper).
|
|
43
|
-
@start_time = start_time
|
|
44
|
-
@end_time = end_time
|
|
45
|
-
@time_diff_hours = time_diff_hours
|
|
46
|
-
@start_duration = start_duration
|
|
47
|
-
@selected_time_range = selected_time_range
|
|
48
|
-
@selected_response_range = selected_response_range
|
|
49
|
-
@zoom_start = zoom_start
|
|
50
|
-
@zoom_end = zoom_end
|
|
51
|
-
@table_start_time = table_start_time
|
|
52
|
-
@table_end_time = table_end_time
|
|
53
34
|
end
|
|
54
35
|
|
|
55
36
|
def setup_chart_and_table_data
|
|
@@ -78,9 +59,8 @@ module ChartTableConcern
|
|
|
78
59
|
common_options = {
|
|
79
60
|
ransack_query: chart_ransack_query,
|
|
80
61
|
period_type: period_type,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
start_duration: @page_timings.start_duration,
|
|
62
|
+
window: @time_range&.window,
|
|
63
|
+
start_duration: @time_range&.start_duration,
|
|
84
64
|
disabled_tags: session_disabled_tags,
|
|
85
65
|
show_non_tagged: session[:show_non_tagged] != false,
|
|
86
66
|
**chart_options
|
|
@@ -110,22 +90,7 @@ module ChartTableConcern
|
|
|
110
90
|
end
|
|
111
91
|
|
|
112
92
|
def period_type
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
type = if time_diff.nil?
|
|
116
|
-
"day" # Default to day for "recent" mode or when time_diff isn't set
|
|
117
|
-
elsif time_diff <= 25
|
|
118
|
-
"hour"
|
|
119
|
-
else
|
|
120
|
-
"day"
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Validate period type to prevent SQL injection via string interpolation
|
|
124
|
-
unless VALID_PERIOD_TYPES.include?(type)
|
|
125
|
-
raise ArgumentError, "Invalid period_type: #{type}. Must be one of: #{VALID_PERIOD_TYPES.join(", ")}"
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
type
|
|
93
|
+
@time_range&.period_type || "day"
|
|
129
94
|
end
|
|
130
95
|
|
|
131
96
|
def meaningful_chart_data?
|
|
@@ -181,15 +146,13 @@ module ChartTableConcern
|
|
|
181
146
|
|
|
182
147
|
# Builds ransack parameters for chart queries
|
|
183
148
|
# Common pattern: time range + optional duration filter + resource scope
|
|
184
|
-
# Handles "recent" mode where @page_timings.start_time/@end_time may be nil
|
|
185
149
|
def build_chart_ransack_params(ransack_params)
|
|
186
150
|
base_params = ransack_params.except(:s, *chart_filter_exclusions)
|
|
187
151
|
|
|
188
|
-
|
|
189
|
-
if @page_timings&.start_time && @page_timings&.end_time
|
|
152
|
+
if @time_range&.window
|
|
190
153
|
base_params.merge!(
|
|
191
|
-
period_start_gteq:
|
|
192
|
-
period_start_lt:
|
|
154
|
+
period_start_gteq: @time_range.window.start_time,
|
|
155
|
+
period_start_lt: @time_range.window.end_time
|
|
193
156
|
)
|
|
194
157
|
end
|
|
195
158
|
|
|
@@ -197,8 +160,8 @@ module ChartTableConcern
|
|
|
197
160
|
base_params.merge!(summarizable_type_eq: summarizable_type) if summarizable_type
|
|
198
161
|
|
|
199
162
|
# Only add duration filter if we have a meaningful threshold
|
|
200
|
-
if @
|
|
201
|
-
base_params[:avg_duration_gteq] = @
|
|
163
|
+
if @time_range&.start_duration && @time_range.start_duration > 0
|
|
164
|
+
base_params[:avg_duration_gteq] = @time_range.start_duration
|
|
202
165
|
end
|
|
203
166
|
|
|
204
167
|
# Scope to specific resource on show pages
|
|
@@ -220,41 +183,37 @@ module ChartTableConcern
|
|
|
220
183
|
end
|
|
221
184
|
|
|
222
185
|
# Builds table params for show pages (individual records like Request, JobRun)
|
|
223
|
-
# Handles "recent" mode where time boundaries may be nil
|
|
224
186
|
def build_show_table_ransack_params(ransack_params)
|
|
225
187
|
params = ransack_params.dup
|
|
226
188
|
|
|
227
|
-
|
|
228
|
-
if @page_timings&.table_start_time && @page_timings&.table_end_time
|
|
189
|
+
if @time_range&.table_window
|
|
229
190
|
params.merge!(
|
|
230
|
-
occurred_at_gteq:
|
|
231
|
-
occurred_at_lt:
|
|
191
|
+
occurred_at_gteq: @time_range.table_window.start_time,
|
|
192
|
+
occurred_at_lt: @time_range.table_window.end_time
|
|
232
193
|
)
|
|
233
194
|
end
|
|
234
195
|
|
|
235
196
|
params.merge!(show_resource_filter)
|
|
236
|
-
if @
|
|
237
|
-
params[:duration_gteq] = @
|
|
197
|
+
if @time_range&.start_duration && @time_range.start_duration > 0
|
|
198
|
+
params[:duration_gteq] = @time_range.start_duration
|
|
238
199
|
end
|
|
239
200
|
params
|
|
240
201
|
end
|
|
241
202
|
|
|
242
203
|
# Builds table params for index pages (summary records)
|
|
243
|
-
# Handles "recent" mode where time boundaries may be nil
|
|
244
204
|
def build_index_table_ransack_params(ransack_params)
|
|
245
205
|
params = ransack_params.dup
|
|
246
206
|
|
|
247
|
-
|
|
248
|
-
if @page_timings&.table_start_time && @page_timings&.table_end_time
|
|
207
|
+
if @time_range&.table_window
|
|
249
208
|
params.merge!(
|
|
250
|
-
period_start_gteq:
|
|
251
|
-
period_start_lt:
|
|
209
|
+
period_start_gteq: @time_range.table_window.start_time,
|
|
210
|
+
period_start_lt: @time_range.table_window.end_time
|
|
252
211
|
)
|
|
253
212
|
end
|
|
254
213
|
|
|
255
214
|
params.merge!(summarizable_type_eq: summarizable_type) if summarizable_type
|
|
256
|
-
if @
|
|
257
|
-
params[:avg_duration_gteq] = @
|
|
215
|
+
if @time_range&.start_duration && @time_range.start_duration > 0
|
|
216
|
+
params[:avg_duration_gteq] = @time_range.start_duration
|
|
258
217
|
end
|
|
259
218
|
params
|
|
260
219
|
end
|
|
@@ -285,7 +244,7 @@ module ChartTableConcern
|
|
|
285
244
|
raise NotImplementedError, "#{self.class} must implement #chart_definitions"
|
|
286
245
|
end
|
|
287
246
|
|
|
288
|
-
# Returns options passed to chart classes (e.g., {
|
|
247
|
+
# Returns options passed to chart classes (e.g., { subject: @route })
|
|
289
248
|
def chart_options
|
|
290
249
|
{}
|
|
291
250
|
end
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
module DeploymentMarkersConcern
|
|
2
2
|
def populate_deployment_markers
|
|
3
|
+
return unless @time_range&.window
|
|
4
|
+
|
|
3
5
|
@deployment_markers = RailsPulse::Deployment
|
|
4
|
-
.for_range(
|
|
6
|
+
.for_range(@time_range.window.start_time, @time_range.window.end_time)
|
|
5
7
|
.map(&:to_chart_marker)
|
|
6
8
|
end
|
|
7
9
|
end
|
|
@@ -33,14 +33,13 @@ module MetricCardConcern
|
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
# Common parameters passed to all metric card classes
|
|
36
|
+
# Common parameters passed to all metric card classes. Deliberately does
|
|
37
|
+
# NOT pass window: — these cards show a "trailing period" figure, not the
|
|
38
|
+
# exact selected range (ChartTableConcern's charts get the exact window;
|
|
39
|
+
# see Cards::Base#window_days).
|
|
37
40
|
def metric_card_params
|
|
38
|
-
|
|
39
|
-
period_days =
|
|
40
|
-
7
|
|
41
|
-
else
|
|
42
|
-
((@end_time - @start_time) / 1.day).round
|
|
43
|
-
end
|
|
41
|
+
window = @time_range&.window
|
|
42
|
+
period_days = window ? ((window.end_time - window.start_time) / 1.day).round : 7
|
|
44
43
|
|
|
45
44
|
{
|
|
46
45
|
resource_key => current_resource,
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
# Read-only JSON API for the rails-pulse CLI, the MCP server, CI scripts
|
|
5
|
+
# and coding agents. Authenticated by config.api_token alone: the
|
|
6
|
+
# dashboard session is never consulted, and with no token configured
|
|
7
|
+
# every request is refused.
|
|
8
|
+
class BaseController < RailsPulse::ApplicationController
|
|
9
|
+
skip_before_action :authenticate_rails_pulse_user!
|
|
10
|
+
skip_before_action :set_show_non_tagged_default
|
|
11
|
+
skip_before_action :set_onboarding_state
|
|
12
|
+
skip_before_action :load_deployment_markers
|
|
13
|
+
|
|
14
|
+
# Prepended so it runs ahead of the inherited require_current_schema!:
|
|
15
|
+
# the schema report lists missing tables and columns and must not be
|
|
16
|
+
# served to anonymous callers.
|
|
17
|
+
prepend_before_action :authenticate_api_token!
|
|
18
|
+
|
|
19
|
+
# After authentication, so an anonymous caller learns nothing from
|
|
20
|
+
# which of its parameters were refused.
|
|
21
|
+
before_action :validate_params!
|
|
22
|
+
|
|
23
|
+
# Every action is a GET read from the query string. Rails would
|
|
24
|
+
# otherwise copy a JSON request's parameters into a hash named after
|
|
25
|
+
# the controller (`route`, `job`), which collides with the filters of
|
|
26
|
+
# the same name whenever a client sends Content-Type: application/json.
|
|
27
|
+
wrap_parameters false
|
|
28
|
+
|
|
29
|
+
# Parameters every endpoint reads as one string. A repeated or nested
|
|
30
|
+
# one (`search[]=x`) is refused rather than reaching a String method.
|
|
31
|
+
SCALAR_PARAMS = %i[limit offset min_requests occurrences since until search route status sort job].freeze
|
|
32
|
+
INTEGER_PARAMS = %i[limit offset min_requests occurrences].freeze
|
|
33
|
+
|
|
34
|
+
# Past any real table, and inside a 64-bit integer on every adapter.
|
|
35
|
+
MAX_INTEGER = 1_000_000_000
|
|
36
|
+
|
|
37
|
+
# A date, optionally a time, optionally a zone. Time.parse would also
|
|
38
|
+
# take "10" as the 10th of this month, which is not a window anyone
|
|
39
|
+
# asked for.
|
|
40
|
+
ISO8601 = /\A\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)?(?:Z|[+-]\d{2}:?\d{2})?\z/i
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def authenticate_api_token!
|
|
45
|
+
token = RailsPulse.configuration.api_token.to_s
|
|
46
|
+
provided = request.headers["X-Rails-Pulse-Token"].to_s
|
|
47
|
+
return if token.present? && ActiveSupport::SecurityUtils.secure_compare(provided, token)
|
|
48
|
+
|
|
49
|
+
render json: { error: "Unauthorized" }, status: :unauthorized
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The schema report picks its format from the request; API callers
|
|
53
|
+
# rarely send an Accept header, so answer JSON regardless.
|
|
54
|
+
def require_current_schema!
|
|
55
|
+
request.format = :json
|
|
56
|
+
super
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def validate_params!
|
|
60
|
+
SCALAR_PARAMS.each do |name|
|
|
61
|
+
value = params[name]
|
|
62
|
+
next if value.nil? || value.is_a?(String)
|
|
63
|
+
|
|
64
|
+
return render_bad_request("'#{name}' must be a single value")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
INTEGER_PARAMS.each do |name|
|
|
68
|
+
value = params[name]
|
|
69
|
+
next if value.blank? || value.match?(/\A\d+\z/)
|
|
70
|
+
|
|
71
|
+
return render_bad_request("'#{name}' must be a whole number")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@since_time = parse_time_param(:since)
|
|
75
|
+
return if performed?
|
|
76
|
+
@until_time = parse_time_param(:until)
|
|
77
|
+
return if performed?
|
|
78
|
+
|
|
79
|
+
if @since_time && @until_time && @until_time <= @since_time
|
|
80
|
+
render_bad_request("'until' must be later than 'since'")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render_bad_request(message)
|
|
85
|
+
render json: { error: message }, status: :bad_request
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def limit
|
|
89
|
+
integer_param(:limit, 25, 1..500)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def offset
|
|
93
|
+
integer_param(:offset, 0, 0..MAX_INTEGER)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def integer_param(name, default, range)
|
|
97
|
+
params[name].blank? ? default : params[name].to_i.clamp(range)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A time with no zone is read as UTC, so a window means the same thing
|
|
101
|
+
# wherever the caller and the server are.
|
|
102
|
+
def parse_time_param(name)
|
|
103
|
+
value = params[name]
|
|
104
|
+
return if value.blank?
|
|
105
|
+
|
|
106
|
+
string = value.strip
|
|
107
|
+
raise ArgumentError unless string.match?(ISO8601)
|
|
108
|
+
|
|
109
|
+
ActiveSupport::TimeZone["UTC"].parse(string)
|
|
110
|
+
rescue ArgumentError
|
|
111
|
+
render_bad_request(
|
|
112
|
+
"Invalid time format for '#{name}'. Use ISO 8601, such as 2026-09-24T12:00:00Z; " \
|
|
113
|
+
"a time with no zone is read as UTC."
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Parsed and checked by validate_params! before the action runs.
|
|
118
|
+
def time_range
|
|
119
|
+
[ @since_time, @until_time ]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def paginated(collection)
|
|
123
|
+
total = collection.count
|
|
124
|
+
data = collection.limit(limit).offset(offset)
|
|
125
|
+
[ data, { total: total, limit: limit, offset: offset } ]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
# Which installation answered, so a caller can tell one deployment's
|
|
5
|
+
# numbers from another's before it starts reading them.
|
|
6
|
+
class CapabilitiesController < BaseController
|
|
7
|
+
def show
|
|
8
|
+
render json: {
|
|
9
|
+
rails_pulse_version: RailsPulse::VERSION,
|
|
10
|
+
environment: Rails.env.to_s,
|
|
11
|
+
# Names the installation a response came from, so two profiles
|
|
12
|
+
# pointed at staging and production cannot be confused.
|
|
13
|
+
application: application_name
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def application_name
|
|
20
|
+
Rails.application.class.module_parent_name
|
|
21
|
+
rescue StandardError
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|