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.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -1
  3. data/README.md +15 -4
  4. data/app/assets/stylesheets/rails_pulse/components/chart.css +11 -0
  5. data/app/assets/stylesheets/rails_pulse/components/descriptive_list.css +19 -1
  6. data/app/assets/stylesheets/rails_pulse/components/row.css +3 -17
  7. data/app/controllers/concerns/chart_table_concern.rb +28 -69
  8. data/app/controllers/concerns/deployment_markers_concern.rb +3 -1
  9. data/app/controllers/concerns/metric_card_concern.rb +6 -7
  10. data/app/controllers/rails_pulse/api/v1/base_controller.rb +130 -0
  11. data/app/controllers/rails_pulse/api/v1/capabilities_controller.rb +27 -0
  12. data/app/controllers/rails_pulse/api/v1/coverage_controller.rb +158 -0
  13. data/app/controllers/rails_pulse/api/v1/deployments_controller.rb +24 -0
  14. data/app/controllers/rails_pulse/api/v1/exceptions_controller.rb +76 -0
  15. data/app/controllers/rails_pulse/api/v1/job_runs_controller.rb +34 -0
  16. data/app/controllers/rails_pulse/api/v1/jobs_controller.rb +172 -0
  17. data/app/controllers/rails_pulse/api/v1/queries_controller.rb +114 -0
  18. data/app/controllers/rails_pulse/api/v1/requests_controller.rb +58 -0
  19. data/app/controllers/rails_pulse/api/v1/routes_controller.rb +98 -0
  20. data/app/controllers/rails_pulse/application_controller.rb +1 -1
  21. data/app/controllers/rails_pulse/dashboard_controller.rb +20 -20
  22. data/app/controllers/rails_pulse/deployments_controller.rb +23 -13
  23. data/app/controllers/rails_pulse/exceptions_controller.rb +10 -12
  24. data/app/controllers/rails_pulse/jobs_controller.rb +1 -2
  25. data/app/controllers/rails_pulse/queries_controller.rb +6 -5
  26. data/app/controllers/rails_pulse/requests_controller.rb +15 -14
  27. data/app/controllers/rails_pulse/routes_controller.rb +4 -8
  28. data/app/helpers/rails_pulse/application_helper.rb +15 -0
  29. data/app/helpers/rails_pulse/chart_helper.rb +9 -2
  30. data/app/helpers/rails_pulse/formatting_helper.rb +11 -12
  31. data/app/javascript/rails_pulse/controllers/chart_controller.js +80 -43
  32. data/app/jobs/rails_pulse/backfill_summaries_job.rb +4 -2
  33. data/app/models/concerns/rails_pulse/has_metadata.rb +15 -0
  34. data/app/models/concerns/rails_pulse/taggable.rb +5 -12
  35. data/app/models/rails_pulse/cards/base.rb +20 -11
  36. data/app/models/rails_pulse/charts/base.rb +2 -6
  37. data/app/models/rails_pulse/charts/percentile_chart_base.rb +3 -4
  38. data/app/models/rails_pulse/dashboard/charts/response_time_percentiles.rb +4 -5
  39. data/app/models/rails_pulse/dashboard/charts/throughput_and_errors.rb +4 -5
  40. data/app/models/rails_pulse/dashboard/concerns/time_range_helper.rb +1 -2
  41. data/app/models/rails_pulse/dashboard/health_summary.rb +30 -3
  42. data/app/models/rails_pulse/dashboard/needs_attention.rb +2 -3
  43. data/app/models/rails_pulse/dashboard/storage_pressure.rb +34 -4
  44. data/app/models/rails_pulse/dashboard/storage_status.rb +36 -0
  45. data/app/models/rails_pulse/deployment.rb +2 -7
  46. data/app/models/rails_pulse/event.rb +40 -0
  47. data/app/models/rails_pulse/exception_occurrence.rb +1 -1
  48. data/app/models/rails_pulse/exceptions/cards/exception_rate.rb +3 -6
  49. data/app/models/rails_pulse/exceptions/cards/open_groups.rb +1 -4
  50. data/app/models/rails_pulse/exceptions/cards/total_occurrences.rb +3 -6
  51. data/app/models/rails_pulse/exceptions/charts/occurrence_volume.rb +4 -5
  52. data/app/models/rails_pulse/job_run.rb +1 -1
  53. data/app/models/rails_pulse/jobs/cards/failure_rate.rb +8 -14
  54. data/app/models/rails_pulse/jobs/cards/p95_duration.rb +8 -14
  55. data/app/models/rails_pulse/jobs/cards/total_runs.rb +5 -11
  56. data/app/models/rails_pulse/jobs/charts/duration.rb +1 -1
  57. data/app/models/rails_pulse/jobs/charts/execution_volume.rb +1 -1
  58. data/app/models/rails_pulse/jobs/charts/failure_rate.rb +1 -1
  59. data/app/models/rails_pulse/like_pattern.rb +29 -0
  60. data/app/models/rails_pulse/queries/cards/average_query_times.rb +6 -12
  61. data/app/models/rails_pulse/queries/cards/database_load.rb +0 -9
  62. data/app/models/rails_pulse/queries/cards/execution_rate.rb +5 -11
  63. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +6 -12
  64. data/app/models/rails_pulse/queries/charts/database_load.rb +5 -6
  65. data/app/models/rails_pulse/queries/charts/execution_volume.rb +1 -1
  66. data/app/models/rails_pulse/request.rb +1 -1
  67. data/app/models/rails_pulse/routes/cards/error_rates.rb +5 -15
  68. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +5 -17
  69. data/app/models/rails_pulse/routes/cards/request_count_totals.rb +5 -17
  70. data/app/models/rails_pulse/routes/charts/error_rate.rb +1 -1
  71. data/app/models/rails_pulse/routes/charts/request_volume.rb +1 -1
  72. data/app/models/rails_pulse/summary.rb +5 -0
  73. data/app/models/rails_pulse/tables/base.rb +5 -6
  74. data/app/models/rails_pulse/time_range.rb +309 -0
  75. data/app/models/rails_pulse/writer_heartbeat.rb +86 -0
  76. data/app/serializers/rails_pulse/api/v1/deployment_serializer.rb +20 -0
  77. data/app/serializers/rails_pulse/api/v1/exception_group_serializer.rb +23 -0
  78. data/app/serializers/rails_pulse/api/v1/exception_occurrence_serializer.rb +22 -0
  79. data/app/serializers/rails_pulse/api/v1/job_run_serializer.rb +28 -0
  80. data/app/serializers/rails_pulse/api/v1/job_serializer.rb +26 -0
  81. data/app/serializers/rails_pulse/api/v1/query_serializer.rb +25 -0
  82. data/app/serializers/rails_pulse/api/v1/request_serializer.rb +21 -0
  83. data/app/serializers/rails_pulse/api/v1/route_serializer.rb +19 -0
  84. data/app/services/rails_pulse/exception_capture_service.rb +1 -1
  85. data/app/services/rails_pulse/operations/comparison.rb +1 -1
  86. data/app/services/rails_pulse/summary_service/from_child_periods.rb +161 -0
  87. data/app/services/rails_pulse/summary_service/from_raw_rows.rb +115 -0
  88. data/app/services/rails_pulse/summary_service/metrics.rb +38 -0
  89. data/app/services/rails_pulse/summary_service.rb +121 -133
  90. data/app/services/rails_pulse/tag_filter_service.rb +5 -5
  91. data/app/views/layouts/rails_pulse/_time_range_selector.html.erb +4 -1
  92. data/app/views/rails_pulse/components/_operation_details_popover.html.erb +1 -1
  93. data/app/views/rails_pulse/components/_table_head.html.erb +2 -2
  94. data/app/views/rails_pulse/dashboard/_chart_panel.html.erb +6 -1
  95. data/app/views/rails_pulse/dashboard/_deployments_panel.html.erb +2 -2
  96. data/app/views/rails_pulse/dashboard/_health_badge.html.erb +29 -20
  97. data/app/views/rails_pulse/dashboard/index.html.erb +16 -2
  98. data/app/views/rails_pulse/jobs/_chart_tabs.html.erb +1 -0
  99. data/app/views/rails_pulse/jobs/index.html.erb +1 -1
  100. data/app/views/rails_pulse/queries/_chart_tabs.html.erb +1 -0
  101. data/app/views/rails_pulse/queries/index.html.erb +1 -1
  102. data/app/views/rails_pulse/queries/show.html.erb +1 -1
  103. data/app/views/rails_pulse/requests/index.html.erb +1 -1
  104. data/app/views/rails_pulse/routes/_chart_tabs.html.erb +1 -0
  105. data/app/views/rails_pulse/routes/index.html.erb +1 -1
  106. data/app/views/rails_pulse/routes/show.html.erb +1 -1
  107. data/app/views/rails_pulse/shared/_aggregation_zone_badge.html.erb +4 -0
  108. data/app/views/rails_pulse/storage/show.html.erb +37 -0
  109. data/config/routes.rb +19 -0
  110. data/db/rails_pulse_migrate/20260506000001_create_rails_pulse_exceptions.rb +1 -1
  111. data/db/rails_pulse_migrate/20260921000001_create_rails_pulse_events.rb +21 -0
  112. data/db/rails_pulse_schema.rb +20 -2
  113. data/exe/rails-pulse +7 -0
  114. data/lib/generators/rails_pulse/base_methods.rb +1 -0
  115. data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +20 -2
  116. data/lib/generators/rails_pulse/templates/rails_pulse.rb +36 -13
  117. data/lib/rails_pulse/cleanup_service.rb +26 -20
  118. data/lib/rails_pulse/cli/agent_files/agents.md +87 -0
  119. data/lib/rails_pulse/cli/agent_files/claude_skill.md +165 -0
  120. data/lib/rails_pulse/cli/base_command.rb +37 -0
  121. data/lib/rails_pulse/cli/client.rb +64 -0
  122. data/lib/rails_pulse/cli/config.rb +87 -0
  123. data/lib/rails_pulse/cli/configure.rb +75 -0
  124. data/lib/rails_pulse/cli/coverage.rb +91 -0
  125. data/lib/rails_pulse/cli/deployments.rb +40 -0
  126. data/lib/rails_pulse/cli/exceptions.rb +111 -0
  127. data/lib/rails_pulse/cli/formatter.rb +31 -0
  128. data/lib/rails_pulse/cli/install.rb +145 -0
  129. data/lib/rails_pulse/cli/job_runs.rb +53 -0
  130. data/lib/rails_pulse/cli/jobs.rb +71 -0
  131. data/lib/rails_pulse/cli/main.rb +72 -0
  132. data/lib/rails_pulse/cli/mcp.rb +47 -0
  133. data/lib/rails_pulse/cli/queries.rb +62 -0
  134. data/lib/rails_pulse/cli/requests.rb +49 -0
  135. data/lib/rails_pulse/cli/routes.rb +66 -0
  136. data/lib/rails_pulse/configuration.rb +23 -1
  137. data/lib/rails_pulse/current.rb +11 -0
  138. data/lib/rails_pulse/engine.rb +12 -3
  139. data/lib/rails_pulse/job_run_collector.rb +15 -15
  140. data/lib/rails_pulse/mcp/server.rb +76 -0
  141. data/lib/rails_pulse/mcp/tools/coverage.rb +96 -0
  142. data/lib/rails_pulse/mcp/tools/deployments.rb +80 -0
  143. data/lib/rails_pulse/mcp/tools/endpoint.rb +167 -0
  144. data/lib/rails_pulse/mcp/tools/errors.rb +95 -0
  145. data/lib/rails_pulse/mcp/tools/exception_detail.rb +118 -0
  146. data/lib/rails_pulse/mcp/tools/exceptions.rb +114 -0
  147. data/lib/rails_pulse/mcp/tools/helpers.rb +162 -0
  148. data/lib/rails_pulse/mcp/tools/jobs.rb +155 -0
  149. data/lib/rails_pulse/mcp/tools/queries.rb +136 -0
  150. data/lib/rails_pulse/mcp/tools/routes.rb +97 -0
  151. data/lib/rails_pulse/mcp/tools/slow_requests.rb +118 -0
  152. data/lib/rails_pulse/middleware/request_collector.rb +9 -9
  153. data/lib/rails_pulse/scoped_inflector.rb +35 -0
  154. data/lib/rails_pulse/statistics.rb +27 -0
  155. data/lib/rails_pulse/subscribers/exception_subscriber.rb +2 -2
  156. data/lib/rails_pulse/subscribers/operation_subscriber.rb +15 -29
  157. data/lib/rails_pulse/tasks/status_reporter.rb +26 -0
  158. data/lib/rails_pulse/tracker.rb +75 -14
  159. data/lib/rails_pulse/version.rb +1 -1
  160. data/lib/rails_pulse.rb +0 -4
  161. data/public/rails-pulse-assets/rails-pulse.css +1 -1
  162. data/public/rails-pulse-assets/rails-pulse.js +13 -13
  163. metadata +96 -24
  164. data/app/controllers/concerns/page_timings.rb +0 -10
  165. data/app/controllers/concerns/response_range_concern.rb +0 -45
  166. data/app/controllers/concerns/time_range_concern.rb +0 -152
  167. data/app/controllers/concerns/zoom_range_concern.rb +0 -86
data/config/routes.rb CHANGED
@@ -37,6 +37,25 @@ RailsPulse::Engine.routes.draw do
37
37
  end
38
38
  end
39
39
 
40
+ # Read-only JSON API for the rails-pulse CLI, the MCP server and CI, all
41
+ # authenticated by config.api_token (app/controllers/rails_pulse/api/v1).
42
+ scope path: "api/v1", module: "api/v1", as: :api_v1 do
43
+ resources :routes, only: :index
44
+ resources :requests, only: :index
45
+ resources :queries, only: :index
46
+ resources :jobs, only: :index
47
+ resources :job_runs, only: :index
48
+ resources :deployments, only: :index
49
+ resources :exceptions, only: %i[index show]
50
+
51
+ # Whether the data is there, rather than a question about the data.
52
+ get "coverage", to: "coverage#show", as: :coverage
53
+
54
+ # Which application, environment and version answered, so results gathered
55
+ # against staging are not read as production.
56
+ get "capabilities", to: "capabilities#show", as: :capabilities
57
+ end
58
+
40
59
  # CSP compliance testing (development/test only)
41
60
  if Rails.env.local?
42
61
  get "csp_test", to: "csp_test#show", as: :csp_test
@@ -32,7 +32,7 @@ class CreateRailsPulseExceptions < ActiveRecord::Migration[7.0]
32
32
  t.string :request_url, comment: "Nullable — web requests only"
33
33
  t.string :request_method, comment: "GET, POST, etc."
34
34
  t.string :environment, comment: "production, staging, etc."
35
- t.string :deploy_sha, comment: "Captured now even though Pro uses it — cannot backfill later"
35
+ t.string :deploy_sha, comment: "Captured at occurrence time — cannot backfill later"
36
36
  t.text :request_params, comment: "JSON hash of filtered request params — web requests only"
37
37
  t.datetime :occurred_at, null: false
38
38
  t.timestamps
@@ -0,0 +1,21 @@
1
+ class CreateRailsPulseEvents < ActiveRecord::Migration[7.0]
2
+ def change
3
+ return if table_exists?(:rails_pulse_events)
4
+
5
+ create_table :rails_pulse_events do |t|
6
+ t.string :kind, null: false, comment: "What noticed it, e.g. writer_heartbeat"
7
+ t.string :subject, comment: "Who it is about: host:pid, rule name, job name, exception class"
8
+ t.string :outcome, null: false, comment: "sampled, triggered, clean, insufficient_data, ran"
9
+ t.decimal :value, precision: 15, scale: 6, comment: "The number behind it: requests dropped since the last sample, or the triggered metric value"
10
+ t.datetime :occurred_at, null: false
11
+ t.text :message
12
+ t.text :metadata, comment: "JSON with the kind-specific detail"
13
+ t.timestamps
14
+ end
15
+
16
+ add_index :rails_pulse_events, [ :kind, :occurred_at ],
17
+ name: "index_rp_events_on_kind_and_occurred_at"
18
+ add_index :rails_pulse_events, [ :kind, :subject, :occurred_at ],
19
+ name: "index_rp_events_on_kind_subject_and_occurred_at"
20
+ end
21
+ end
@@ -7,7 +7,7 @@ require "rails_pulse/route_indexes" unless defined?(RailsPulse::RouteIndexes)
7
7
  RailsPulse::Schema = lambda do |connection|
8
8
  adapter = connection.adapter_name.downcase
9
9
  # Skip if all tables already exist to prevent conflicts
10
- required_tables = [ :rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_jobs, :rails_pulse_job_runs, :rails_pulse_summaries, :rails_pulse_deployments, :rails_pulse_exception_groups, :rails_pulse_exception_occurrences ]
10
+ required_tables = [ :rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_jobs, :rails_pulse_job_runs, :rails_pulse_summaries, :rails_pulse_deployments, :rails_pulse_exception_groups, :rails_pulse_exception_occurrences, :rails_pulse_events ]
11
11
 
12
12
  # Check which tables already exist
13
13
  existing_tables = required_tables.select { |table| connection.table_exists?(table) }
@@ -249,7 +249,7 @@ RailsPulse::Schema = lambda do |connection|
249
249
  t.string :request_url, comment: "Nullable — web requests only"
250
250
  t.string :request_method, comment: "GET, POST, etc."
251
251
  t.string :environment, comment: "production, staging, etc."
252
- t.string :deploy_sha, comment: "Captured now even though Pro uses it — cannot backfill later"
252
+ t.string :deploy_sha, comment: "Captured at occurrence time — cannot backfill later"
253
253
  t.text :request_params, comment: "JSON hash of filtered request params — web requests only"
254
254
  t.datetime :occurred_at, null: false
255
255
  t.timestamps
@@ -259,6 +259,24 @@ RailsPulse::Schema = lambda do |connection|
259
259
  connection.add_index :rails_pulse_exception_occurrences, :exception_group_id, name: "index_rp_exception_occurrences_on_group_id"
260
260
  end
261
261
 
262
+ unless connection.table_exists?(:rails_pulse_events)
263
+ connection.create_table :rails_pulse_events do |t|
264
+ t.string :kind, null: false, comment: "What noticed it, e.g. writer_heartbeat"
265
+ t.string :subject, comment: "Who it is about: host:pid, rule name, job name, exception class"
266
+ t.string :outcome, null: false, comment: "sampled, triggered, clean, insufficient_data, ran"
267
+ t.decimal :value, precision: 15, scale: 6, comment: "The number behind it: requests dropped since the last sample, or the triggered metric value"
268
+ t.datetime :occurred_at, null: false
269
+ t.text :message
270
+ t.text :metadata, comment: "JSON with the kind-specific detail"
271
+ t.timestamps
272
+ end
273
+
274
+ connection.add_index :rails_pulse_events, [ :kind, :occurred_at ],
275
+ name: "index_rp_events_on_kind_and_occurred_at"
276
+ connection.add_index :rails_pulse_events, [ :kind, :subject, :occurred_at ],
277
+ name: "index_rp_events_on_kind_subject_and_occurred_at"
278
+ end
279
+
262
280
  # Add indexes to existing tables for efficient aggregation
263
281
  unless connection.index_exists?(:rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation")
264
282
  connection.add_index :rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation"
data/exe/rails-pulse ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Runs outside the Rails app: talks to it over the JSON API, so only the CLI
5
+ # is loaded here, never the engine.
6
+ require "rails_pulse/cli/main"
7
+ RailsPulse::CLI::Main.start(ARGV)
@@ -15,6 +15,7 @@ module RailsPulse
15
15
  rails_pulse_deployments
16
16
  rails_pulse_exception_groups
17
17
  rails_pulse_exception_occurrences
18
+ rails_pulse_events
18
19
  ].freeze
19
20
 
20
21
  # Generate next migration number for timestamped migrations
@@ -7,7 +7,7 @@ require "rails_pulse/route_indexes" unless defined?(RailsPulse::RouteIndexes)
7
7
  RailsPulse::Schema = lambda do |connection|
8
8
  adapter = connection.adapter_name.downcase
9
9
  # Skip if all tables already exist to prevent conflicts
10
- required_tables = [ :rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_jobs, :rails_pulse_job_runs, :rails_pulse_summaries, :rails_pulse_deployments, :rails_pulse_exception_groups, :rails_pulse_exception_occurrences ]
10
+ required_tables = [ :rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_jobs, :rails_pulse_job_runs, :rails_pulse_summaries, :rails_pulse_deployments, :rails_pulse_exception_groups, :rails_pulse_exception_occurrences, :rails_pulse_events ]
11
11
 
12
12
  # Check which tables already exist
13
13
  existing_tables = required_tables.select { |table| connection.table_exists?(table) }
@@ -249,7 +249,7 @@ RailsPulse::Schema = lambda do |connection|
249
249
  t.string :request_url, comment: "Nullable — web requests only"
250
250
  t.string :request_method, comment: "GET, POST, etc."
251
251
  t.string :environment, comment: "production, staging, etc."
252
- t.string :deploy_sha, comment: "Captured now even though Pro uses it — cannot backfill later"
252
+ t.string :deploy_sha, comment: "Captured at occurrence time — cannot backfill later"
253
253
  t.text :request_params, comment: "JSON hash of filtered request params — web requests only"
254
254
  t.datetime :occurred_at, null: false
255
255
  t.timestamps
@@ -259,6 +259,24 @@ RailsPulse::Schema = lambda do |connection|
259
259
  connection.add_index :rails_pulse_exception_occurrences, :exception_group_id, name: "index_rp_exception_occurrences_on_group_id"
260
260
  end
261
261
 
262
+ unless connection.table_exists?(:rails_pulse_events)
263
+ connection.create_table :rails_pulse_events do |t|
264
+ t.string :kind, null: false, comment: "What noticed it, e.g. writer_heartbeat"
265
+ t.string :subject, comment: "Who it is about: host:pid, rule name, job name, exception class"
266
+ t.string :outcome, null: false, comment: "sampled, triggered, clean, insufficient_data, ran"
267
+ t.decimal :value, precision: 15, scale: 6, comment: "The number behind it: requests dropped since the last sample, or the triggered metric value"
268
+ t.datetime :occurred_at, null: false
269
+ t.text :message
270
+ t.text :metadata, comment: "JSON with the kind-specific detail"
271
+ t.timestamps
272
+ end
273
+
274
+ connection.add_index :rails_pulse_events, [ :kind, :occurred_at ],
275
+ name: "index_rp_events_on_kind_and_occurred_at"
276
+ connection.add_index :rails_pulse_events, [ :kind, :subject, :occurred_at ],
277
+ name: "index_rp_events_on_kind_subject_and_occurred_at"
278
+ end
279
+
262
280
  # Add indexes to existing tables for efficient aggregation
263
281
  unless connection.index_exists?(:rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation")
264
282
  connection.add_index :rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation"
@@ -327,22 +327,36 @@ RailsPulse.configure do |config|
327
327
  # }
328
328
 
329
329
  # ====================================================================================================
330
- # DEPLOYMENT TRACKING
330
+ # API TOKENS AND DEPLOYMENT TRACKING
331
331
  # ====================================================================================================
332
- # Record deployments to display vertical marker lines on performance charts, making it easy
333
- # to correlate performance changes with specific releases.
332
+ # Two tokens authenticate what is not the dashboard, so a credential given to a coding agent
333
+ # cannot write anything. Both are sent as an `X-Rails-Pulse-Token` header.
334
334
  #
335
- # API token for the POST /rails_pulse/deployments endpoint.
336
- # When set, requests must include an `X-Rails-Pulse-Token` header matching this value.
337
- # When nil, the endpoint falls back to the standard dashboard authentication.
335
+ # api_token reads the JSON API under /rails_pulse/api/v1 — what the `rails-pulse` CLI and the
336
+ # MCP server your coding agent uses talk to. Every endpoint under it is read-only.
337
+ # config.api_token = Rails.application.credentials.dig(:rails_pulse, :api_token)
338
+ # config.api_token = ENV["RAILS_PULSE_API_TOKEN"]
338
339
  #
339
- # Set this in your CI/CD pipeline and store the value in credentials or an environment variable:
340
- # config.deployment_api_token = Rails.application.credentials.dig(:rails_pulse, :deployment_api_token)
341
- # config.deployment_api_token = ENV["RAILS_PULSE_DEPLOYMENT_TOKEN"]
340
+ # deployment_token records a release through POST /rails_pulse/deployments, which your CI calls.
341
+ # It is the only credential that endpoint accepts: api_token is refused there.
342
+ # config.deployment_token = Rails.application.credentials.dig(:rails_pulse, :deployment_token)
343
+ # config.deployment_token = ENV["RAILS_PULSE_DEPLOYMENT_TOKEN"]
342
344
  #
343
- # Limits: revision ≤ 255 characters, metadata ≤ 4 KB serialized, started_at at most
344
- # one hour in the future. Rows beyond max_table_records[:rails_pulse_deployments]
345
- # are pruned oldest-first by the cleanup task.
345
+ # Set both to the same value if you would rather run one credential for both.
346
+ #
347
+ # With no api_token the JSON API refuses every request. With no deployment_token the deployments
348
+ # endpoint refuses every request too; a dashboard login is never accepted in its place. The
349
+ # rails_pulse:record_deployment and finish_deployment rake tasks need no token. (deployment_api_token
350
+ # is the pre-0.5 name for deployment_token and still works.)
351
+ #
352
+ # Point the CLI and MCP server at this app with `rails-pulse configure`, or set RAILS_PULSE_URL
353
+ # and RAILS_PULSE_TOKEN in the agent's environment. The MCP server needs `gem "mcp"` in this
354
+ # app's Gemfile (a development group is enough); Rails Pulse does not pull it into production.
355
+ #
356
+ # Deployments draw vertical markers on the charts so a change in performance can be lined up
357
+ # with the release that caused it. Limits: revision ≤ 255 characters, metadata ≤ 4 KB
358
+ # serialized, started_at at most one hour in the future. Rows beyond
359
+ # max_table_records[:rails_pulse_deployments] are pruned oldest-first by the cleanup task.
346
360
  #
347
361
  # Record a deployment from your CI/CD pipeline:
348
362
  # curl -X POST https://yourapp.com/rails_pulse/deployments \
@@ -356,7 +370,8 @@ RailsPulse.configure do |config|
356
370
  # Metadata for the rake task comes from an environment variable, as a JSON object:
357
371
  # RAILS_PULSE_DEPLOYMENT_METADATA='{"environment":"production"}' rake rails_pulse:record_deployment[abc1234]
358
372
 
359
- # config.deployment_api_token = ENV["RAILS_PULSE_DEPLOYMENT_TOKEN"]
373
+ # config.api_token = ENV["RAILS_PULSE_API_TOKEN"]
374
+ # config.deployment_token = ENV["RAILS_PULSE_DEPLOYMENT_TOKEN"]
360
375
 
361
376
  # ====================================================================================================
362
377
  # DATA CLEANUP
@@ -376,6 +391,14 @@ RailsPulse.configure do |config|
376
391
  # Time-based retention - delete records older than this period
377
392
  config.full_retention_period = 30.days
378
393
 
394
+ # How long rows in rails_pulse_events are kept. The background writer's
395
+ # heartbeats are pruned after a day regardless.
396
+ config.event_retention_period = 90.days
397
+
398
+ # Event kinds exempt from event_retention_period — for rows a writer updates
399
+ # in place rather than appends.
400
+ config.event_retention_exempt_kinds = []
401
+
379
402
  # Count-based retention - maximum records to keep per table
380
403
  # After time-based cleanup, if tables still exceed these limits,
381
404
  # the oldest remaining records will be deleted to stay under the limit
@@ -29,6 +29,7 @@ module RailsPulse
29
29
 
30
30
  perform_time_based_cleanup
31
31
  perform_count_based_cleanup
32
+ perform_event_cleanup
32
33
  perform_summary_cleanup
33
34
 
34
35
  log_cleanup_summary
@@ -116,6 +117,22 @@ module RailsPulse
116
117
  end
117
118
  end
118
119
 
120
+ # event_retention_exempt_kinds are rows a writer updates in place, which
121
+ # must survive so "last ran" never becomes "never ran"
122
+ def perform_event_cleanup
123
+ return unless @config.event_retention_period
124
+ return unless RailsPulse::Event.table_available?
125
+
126
+ cutoff_time = [ @config.event_retention_period.ago, 1.hour.ago ].min
127
+ run_stage(:time_based, :events) do
128
+ delete_in_batches(
129
+ RailsPulse::Event
130
+ .where.not(kind: @config.event_retention_exempt_kinds)
131
+ .where(occurred_at: ...cutoff_time)
132
+ )
133
+ end
134
+ end
135
+
119
136
  def perform_count_based_cleanup
120
137
  return unless @config.max_table_records&.any?
121
138
 
@@ -313,37 +330,26 @@ module RailsPulse
313
330
  RailsPulse::ExceptionOccurrence.where.not(exception_group_id: preserved_ids)
314
331
  end
315
332
 
316
- # Prune oldest non-preserved, non-ignored groups when over the configured cap.
317
- # Child occurrences must be deleted first — there is no ON DELETE CASCADE on the FK.
333
+ # Prune the oldest deletable groups when over the configured cap. Preserved
334
+ # and ignored groups are outside the cap: counting them would make a cap
335
+ # that they alone fill impossible to satisfy, so every run would trim the
336
+ # deletable groups for no gain (decision 0017). Child occurrences must be
337
+ # deleted first — there is no ON DELETE CASCADE on the FK.
318
338
  def cleanup_exception_groups_by_count
319
339
  max_records = @config.max_table_records[:rails_pulse_exception_groups]
320
340
  return 0 unless max_records
321
341
 
322
- current_count = RailsPulse::ExceptionGroup.count
323
- return 0 if current_count <= max_records
324
-
325
- overage = current_count - max_records
326
342
  scope = RailsPulse::ExceptionGroup.where(preserve: false).where.not(status: "ignored")
327
- # Only delete as many as the deletable population allows — if ignored/preserved
328
- # groups dominate, we delete what we can and log when the cap cannot be met.
329
- deletable_count = scope.count
330
- records_to_delete = [ overage, deletable_count ].min
331
- return 0 if records_to_delete <= 0
343
+ overage = scope.count - max_records
344
+ return 0 if overage <= 0
332
345
 
333
- deleted = delete_parents_in_batches(
346
+ delete_parents_in_batches(
334
347
  scope,
335
348
  order_column: :last_seen_at,
336
349
  children: RailsPulse::ExceptionOccurrence,
337
350
  foreign_key: :exception_group_id,
338
- limit: records_to_delete
351
+ limit: overage
339
352
  )
340
-
341
- if overage > deletable_count
342
- RailsPulse.logger.warn("[RailsPulse] Exception group cap #{max_records} cannot be met: " \
343
- "#{current_count - deleted} remain (#{current_count - deletable_count} are preserved/ignored)")
344
- end
345
-
346
- deleted
347
353
  end
348
354
 
349
355
  # Delete groups whose last occurrence was removed — the orphan check and
@@ -0,0 +1,87 @@
1
+ # Rails Pulse — Agent Integration
2
+
3
+ Rails Pulse gives AI coding agents read access to a Rails application's performance data: requests, SQL queries, background jobs, exceptions and deployments, all from the application's own database. Two interfaces, one token.
4
+
5
+ ## MCP server
6
+
7
+ ```
8
+ rails-pulse mcp
9
+ ```
10
+
11
+ The server needs the `mcp` gem, which Rails Pulse does not depend on: add `gem "mcp", "~> 1.0"` to the application's Gemfile (a development group is enough), or `gem install mcp` when running `rails-pulse` outside Bundler.
12
+
13
+ **Claude Code** (`~/.claude.json`):
14
+ ```json
15
+ {
16
+ "mcpServers": {
17
+ "rails-pulse": {
18
+ "command": "rails-pulse",
19
+ "args": ["mcp"],
20
+ "env": {
21
+ "RAILS_PULSE_URL": "https://myapp.com",
22
+ "RAILS_PULSE_TOKEN": "your-token"
23
+ }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ **Cursor** (`.cursor/mcp.json`):
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "rails-pulse": { "command": "rails-pulse", "args": ["mcp"] }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ### Tools
39
+
40
+ All tools are read-only. Each returns a `summary` and `next_steps`.
41
+
42
+ | Tool | Purpose |
43
+ |------|---------|
44
+ | `rails_pulse_routes` | Discover endpoints with request volume, latency and errors |
45
+ | `rails_pulse_slow_requests` | Slowest endpoints for a period |
46
+ | `rails_pulse_coverage` | What has been recorded, how recently, and any collection gaps |
47
+ | `rails_pulse_errors` | Recent errors grouped by endpoint |
48
+ | `rails_pulse_exceptions` | Exception groups with status, count, location and latest message |
49
+ | `rails_pulse_exception` | One group's recent occurrences with backtraces, request and params |
50
+ | `rails_pulse_endpoint` | Deep performance profile of one endpoint |
51
+ | `rails_pulse_queries` | Most expensive SQL queries with N+1 detection |
52
+ | `rails_pulse_jobs` | Background job health and recent failures |
53
+ | `rails_pulse_deployments` | Recent deployments with revision, timing and metadata |
54
+
55
+ Every tool that takes a `period` also takes `since` and `until` as ISO 8601 timestamps, read as UTC when no zone is given, and echoes the bounds it measured back as `window`. `period` itself is only `last_hour`, `last_24_hours` or `last_7_days` (plus `all` on `rails_pulse_exceptions`); any other window is `since`/`until`. Use them to compare two fixed windows — the 24 hours before a release against the 24 hours after — rather than a relative period that moves between calls.
56
+
57
+ ## CLI
58
+
59
+ The `rails-pulse` CLI works without MCP. Append `--json` for structured output.
60
+
61
+ | Command | Description |
62
+ |---------|-------------|
63
+ | `rails-pulse routes list` | Tracked routes (`--since` adds request stats) |
64
+ | `rails-pulse requests list` | Recorded requests with status and time filters |
65
+ | `rails-pulse queries list` | SQL queries (`--since` adds timing stats) |
66
+ | `rails-pulse jobs list` | Background jobs: lifetime stats, or one window's with `--since` / `--until` |
67
+ | `rails-pulse job_runs list` | Individual job runs with errors |
68
+ | `rails-pulse exceptions list` | Exception groups with status and occurrence counts |
69
+ | `rails-pulse exceptions show ID` | One group with backtraces and recent occurrences |
70
+ | `rails-pulse deployments list` | Recorded deployments, most recent first |
71
+ | `rails-pulse coverage show` | What has been recorded, how recently, and any collection gaps |
72
+
73
+ Common flags on every list command: `--limit N` (1 to 500, default 25), `--offset N`, `--json`, `--since TIME` / `--until TIME` (ISO 8601; no zone means UTC).
74
+
75
+ ## Authentication
76
+
77
+ Set `RAILS_PULSE_URL` and `RAILS_PULSE_TOKEN` (the application's `config.api_token`), or run `rails-pulse configure` to write `~/.rails-pulse`. That token is read-only; recording a deployment needs `config.deployment_token`, which these tools do not carry.
78
+
79
+ ## Investigation pattern
80
+
81
+ 1. Pin the time window to a deployment (`rails_pulse_deployments`)
82
+ 2. Identify slow or failing endpoints (`rails_pulse_slow_requests`); resolve names with `rails_pulse_routes`
83
+ 3. Profile the suspect endpoint (`rails_pulse_endpoint`)
84
+ 4. Check errors (`rails_pulse_errors`)
85
+ 5. Inspect SQL and jobs (`rails_pulse_queries`, `rails_pulse_jobs`)
86
+ 6. Correlate with source code: pass the endpoint's `route_id` to `rails_pulse_queries` for the SQL inside it and the file and line each query came from
87
+ 7. Fix, deploy, and re-check the same endpoint
@@ -0,0 +1,165 @@
1
+ ---
2
+ name: rails-pulse
3
+ description: Investigate Rails application performance with Rails Pulse. Use when asked about slow endpoints, regressions after a deploy, expensive or N+1 SQL, error rates, failing background jobs, or whether a performance fix worked. Prefers the rails_pulse_* MCP tools; falls back to the rails-pulse CLI.
4
+ ---
5
+
6
+ # Rails Pulse
7
+
8
+ Rails Pulse records every request, SQL query, background job and exception of a Rails application in the application's own database. This skill gives you read access to that data so you can find what is slow, why, and whether a change fixed it. Nothing here can modify the application.
9
+
10
+ ## When to use it
11
+
12
+ - Investigating a performance complaint or a slow endpoint
13
+ - Diagnosing a regression after a deployment
14
+ - Finding expensive or N+1 SQL queries
15
+ - Investigating an increased error rate
16
+ - Analysing background job failures or slowness
17
+ - Validating whether a performance fix worked
18
+
19
+ ## Interfaces
20
+
21
+ ### MCP tools (preferred when available)
22
+
23
+ All tools are read-only. Each returns a `summary` and `next_steps`.
24
+
25
+ | Tool | Purpose |
26
+ |------|---------|
27
+ | `rails_pulse_routes` | Discover endpoints: request volume, latency and errors per route |
28
+ | `rails_pulse_slow_requests` | The slowest endpoints for a period |
29
+ | `rails_pulse_coverage` | What has been recorded, how recently, and any collection gaps |
30
+ | `rails_pulse_errors` | Recent 4xx/5xx responses grouped by endpoint |
31
+ | `rails_pulse_exceptions` | Exception groups with status, occurrence count, location and latest message |
32
+ | `rails_pulse_exception` | One group's recent occurrences with full backtraces, request, params and deploy SHA |
33
+ | `rails_pulse_endpoint` | Deep profile of one endpoint |
34
+ | `rails_pulse_queries` | Most expensive SQL queries, with N+1 detection |
35
+ | `rails_pulse_jobs` | Background job health and recent failures with error classes |
36
+ | `rails_pulse_deployments` | Recent deployments with revision, start and finish time, and metadata |
37
+
38
+ ### CLI
39
+
40
+ The `rails-pulse` executable ships with the gem. Add `--json` for structured output.
41
+
42
+ | Command | Purpose |
43
+ |---------|---------|
44
+ | `rails-pulse routes list --json` | Tracked routes (add `--since` for stats) |
45
+ | `rails-pulse requests list --json` | Recorded requests with status and time filters |
46
+ | `rails-pulse queries list --json` | SQL queries (add `--since` for timing stats) |
47
+ | `rails-pulse jobs list --json` | Background jobs: lifetime stats, or one window's with `--since` / `--until` |
48
+ | `rails-pulse job_runs list --json` | Individual job runs with error class and message |
49
+ | `rails-pulse exceptions list --json` | Exception groups with status, count, location and message |
50
+ | `rails-pulse exceptions show ID --json` | One group with backtraces, request, params and deploy SHA |
51
+ | `rails-pulse deployments list --json` | Recorded deployments, most recent first |
52
+ | `rails-pulse coverage show --json` | What has been recorded, how recently, and any collection gaps |
53
+
54
+ ## Investigation workflow
55
+
56
+ ### 1. Establish the time period
57
+
58
+ Check recent deployments first; a regression usually lines up with one.
59
+
60
+ ```
61
+ rails_pulse_deployments(period: "last_7_days")
62
+ rails-pulse deployments list --json
63
+ ```
64
+
65
+ Use a deployment's `started_at` as `since` for the tools below, then compare against the same length of time before it.
66
+
67
+ ### 2. Identify affected endpoints
68
+
69
+ ```
70
+ rails_pulse_slow_requests(period: "last_24_hours", limit: 10)
71
+ rails_pulse_routes(search: "checkout", period: "last_7_days")
72
+ rails-pulse requests list --limit 50 --json
73
+ ```
74
+
75
+ ### 3. Profile the suspect endpoint
76
+
77
+ ```
78
+ rails_pulse_endpoint(endpoint: "CheckoutController#create", period: "last_7_days")
79
+ rails-pulse requests list --since 2026-06-01T00:00:00Z --json
80
+ ```
81
+
82
+ ### 4. Check errors
83
+
84
+ ```
85
+ rails_pulse_errors(period: "last_24_hours", status: "5xx")
86
+ rails-pulse requests list --status 5xx --since 2026-06-01T00:00:00Z --json
87
+ ```
88
+
89
+ ### 5. Inspect SQL and background jobs
90
+
91
+ ```
92
+ rails_pulse_queries(period: "last_24_hours", sort: "total_duration")
93
+ rails_pulse_queries(n_plus_one_only: true)
94
+ rails_pulse_jobs(period: "last_24_hours")
95
+ rails-pulse queries list --since 2026-06-01T00:00:00Z --json
96
+ rails-pulse job_runs list --status failed --json
97
+ ```
98
+
99
+ ### 6. Correlate with source code
100
+
101
+ Pass the endpoint's `route_id` to `rails_pulse_queries` to see only the SQL that ran inside it,
102
+ with `source_locations` naming the file and line each query was issued from.
103
+
104
+ ```
105
+ rails_pulse_queries(route: 42, period: "last_24_hours")
106
+ ```
107
+
108
+ Start at those locations rather than searching for the SQL. Look for N+1 queries, missing
109
+ indexes, expensive work in the request path, unnecessary serialization and missing caching.
110
+
111
+ ### 7. Fix, then validate
112
+
113
+ After the fix is deployed, re-check the same endpoint over the period since the deploy.
114
+
115
+ ```
116
+ rails_pulse_deployments(period: "last_24_hours")
117
+ rails_pulse_endpoint(endpoint: "CheckoutController#create", period: "last_hour")
118
+ ```
119
+
120
+ To show a change rather than a snapshot, measure the same endpoint over two fixed windows
121
+ either side of the release. Every windowed tool takes `since` and `until` as ISO 8601
122
+ timestamps, read as UTC when no zone is given, and echoes the bounds it used back as `window`.
123
+
124
+ ```
125
+ rails_pulse_endpoint(endpoint: "CheckoutController#create", since: "2026-09-24T12:00:00Z", until: "2026-09-25T12:00:00Z")
126
+ rails_pulse_endpoint(endpoint: "CheckoutController#create", since: "2026-09-25T12:00:00Z", until: "2026-09-26T12:00:00Z")
127
+ ```
128
+
129
+ Use the deploy time as the boundary, and exclude the minutes around it when a rolling deploy
130
+ makes them a mix of both versions.
131
+
132
+ ## Guidelines
133
+
134
+ - **SQL is not always the cause.** External API calls, serialization, view rendering and application logic slow requests too.
135
+ - **Use more than one tool.** Cross-reference latency with error rates, SQL timing and job performance before concluding.
136
+ - **Orient once before investigating.** `rails_pulse_coverage` names the application, environment and version you are querying. Call it first, so findings are attributed to the installation that actually produced them.
137
+ - **Confirm the data exists before reporting an all-clear.** An empty result means nothing was recorded, which only means nothing happened when collection was healthy over that window. Call `rails_pulse_coverage` before concluding a period was clean, and say so when it flags a gap, stale summaries or a kind that is not tracked.
138
+ - **Check the error rate.** A fast endpoint with a high error rate may be failing early rather than performing well.
139
+ - **Read percentiles, not just averages.** A low average with a high p95 or p99 means intermittent trouble.
140
+ - **Weigh request volume.** A slow endpoint nobody calls may not be worth the work.
141
+ - **Job figures cover the window.** `rails_pulse_jobs` reads them from summaries plus the runs not yet summarized. p95 and p99 are given only when one summary period holds the whole window; otherwise they are null and `note` says why, so compare averages and failure rates instead.
142
+ - **Check what the percentiles covered.** `rails_pulse_endpoint` computes them over the most recent requests it sampled, not the whole window. When `sampled_requests` is below `request_count` it says so in `latency.computed_over`; narrow the window until the two match before comparing percentiles across windows.
143
+
144
+ ## Authentication
145
+
146
+ The CLI and MCP server read credentials from environment variables or `~/.rails-pulse`. The token is `config.api_token` in the application's Rails Pulse initializer. It is read-only: recording a deployment needs `config.deployment_token`, which these tools do not carry.
147
+
148
+ ```
149
+ RAILS_PULSE_URL=https://myapp.com
150
+ RAILS_PULSE_TOKEN=my-secret-token
151
+ RAILS_PULSE_MOUNT_PATH=/rails_pulse # optional, default /rails_pulse
152
+ ```
153
+
154
+ ```yaml
155
+ # ~/.rails-pulse
156
+ url: https://myapp.com
157
+ token: my-secret-token
158
+ mount_path: /rails_pulse
159
+ ```
160
+
161
+ Interactive setup: `rails-pulse configure`.
162
+
163
+ ## CLI reference
164
+
165
+ List commands accept `--limit N` (1 to 500, default 25), `--offset N` and `--json`. Every list command also accepts `--since TIME` and `--until TIME` as ISO 8601; a time with no zone is read as UTC. JSON output is `{ "data": [...], "meta": { "total", "limit", "offset" } }`; page with `--offset`.
@@ -0,0 +1,37 @@
1
+ require "thor"
2
+ require_relative "config"
3
+ require_relative "client"
4
+
5
+ module RailsPulse
6
+ module CLI
7
+ class BaseCommand < Thor
8
+ # A usage error (unknown command, missing argument, bad flag) exits 1,
9
+ # so a script or CI step running the CLI can tell it failed.
10
+ def self.exit_on_failure?
11
+ true
12
+ end
13
+
14
+ # A mistyped flag is an error rather than silently ignored, so
15
+ # `--limt 5` does not quietly return the default page.
16
+ check_unknown_options!
17
+
18
+ no_commands do
19
+ def with_error_handling
20
+ yield
21
+ rescue RailsPulse::CLI::Config::ConfigError => e
22
+ say "Error: #{e.message}.", :red
23
+ exit 1
24
+ rescue RailsPulse::CLI::Client::ApiError => e
25
+ say "API error: #{e.message}", :red
26
+ exit 1
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def client
33
+ @client ||= Client.new
34
+ end
35
+ end
36
+ end
37
+ end