rails_pulse 0.4.2 → 0.5.0.pre.2

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 (203) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -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 +133 -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 +40 -0
  14. data/app/controllers/rails_pulse/api/v1/exceptions_controller.rb +77 -0
  15. data/app/controllers/rails_pulse/api/v1/insights_controller.rb +62 -0
  16. data/app/controllers/rails_pulse/api/v1/job_runs_controller.rb +34 -0
  17. data/app/controllers/rails_pulse/api/v1/jobs_controller.rb +172 -0
  18. data/app/controllers/rails_pulse/api/v1/queries_controller.rb +123 -0
  19. data/app/controllers/rails_pulse/api/v1/requests_controller.rb +74 -0
  20. data/app/controllers/rails_pulse/api/v1/routes_controller.rb +103 -0
  21. data/app/controllers/rails_pulse/application_controller.rb +1 -1
  22. data/app/controllers/rails_pulse/dashboard_controller.rb +25 -21
  23. data/app/controllers/rails_pulse/deployments_controller.rb +23 -13
  24. data/app/controllers/rails_pulse/exceptions_controller.rb +10 -12
  25. data/app/controllers/rails_pulse/jobs_controller.rb +1 -2
  26. data/app/controllers/rails_pulse/queries_controller.rb +6 -5
  27. data/app/controllers/rails_pulse/requests_controller.rb +15 -14
  28. data/app/controllers/rails_pulse/routes_controller.rb +4 -8
  29. data/app/helpers/rails_pulse/application_helper.rb +15 -0
  30. data/app/helpers/rails_pulse/chart_helper.rb +9 -2
  31. data/app/helpers/rails_pulse/formatting_helper.rb +11 -12
  32. data/app/javascript/rails_pulse/controllers/chart_controller.js +80 -43
  33. data/app/jobs/rails_pulse/backfill_summaries_job.rb +4 -2
  34. data/app/jobs/rails_pulse/cloud_health_job.rb +11 -0
  35. data/app/jobs/rails_pulse/cloud_sync_job.rb +11 -0
  36. data/app/jobs/rails_pulse/summary_job.rb +13 -0
  37. data/app/models/concerns/rails_pulse/has_metadata.rb +15 -0
  38. data/app/models/concerns/rails_pulse/taggable.rb +5 -12
  39. data/app/models/rails_pulse/cards/base.rb +20 -11
  40. data/app/models/rails_pulse/charts/base.rb +5 -18
  41. data/app/models/rails_pulse/charts/percentile_chart_base.rb +3 -5
  42. data/app/models/rails_pulse/cleanup_run.rb +32 -0
  43. data/app/models/rails_pulse/cloud/buffered_batch.rb +55 -0
  44. data/app/models/rails_pulse/cloud/installation.rb +55 -0
  45. data/app/models/rails_pulse/dashboard/charts/response_time_percentiles.rb +4 -5
  46. data/app/models/rails_pulse/dashboard/charts/throughput_and_errors.rb +4 -5
  47. data/app/models/rails_pulse/dashboard/concerns/attention_classification.rb +104 -0
  48. data/app/models/rails_pulse/dashboard/concerns/time_range_helper.rb +25 -2
  49. data/app/models/rails_pulse/dashboard/health_summary.rb +38 -13
  50. data/app/models/rails_pulse/dashboard/needs_attention.rb +31 -98
  51. data/app/models/rails_pulse/dashboard/storage_pressure.rb +39 -63
  52. data/app/models/rails_pulse/dashboard/storage_status.rb +130 -39
  53. data/app/models/rails_pulse/deployment.rb +2 -7
  54. data/app/models/rails_pulse/event.rb +41 -0
  55. data/app/models/rails_pulse/exception_occurrence.rb +1 -1
  56. data/app/models/rails_pulse/exceptions/cards/exception_rate.rb +3 -6
  57. data/app/models/rails_pulse/exceptions/cards/open_groups.rb +1 -4
  58. data/app/models/rails_pulse/exceptions/cards/total_occurrences.rb +3 -6
  59. data/app/models/rails_pulse/exceptions/charts/occurrence_volume.rb +4 -7
  60. data/app/models/rails_pulse/job_run.rb +1 -1
  61. data/app/models/rails_pulse/jobs/cards/failure_rate.rb +8 -14
  62. data/app/models/rails_pulse/jobs/cards/p95_duration.rb +8 -14
  63. data/app/models/rails_pulse/jobs/cards/total_runs.rb +5 -11
  64. data/app/models/rails_pulse/jobs/charts/duration.rb +1 -2
  65. data/app/models/rails_pulse/jobs/charts/execution_volume.rb +1 -1
  66. data/app/models/rails_pulse/jobs/charts/failure_rate.rb +1 -2
  67. data/app/models/rails_pulse/like_pattern.rb +35 -0
  68. data/app/models/rails_pulse/queries/cards/average_query_times.rb +6 -12
  69. data/app/models/rails_pulse/queries/cards/database_load.rb +0 -9
  70. data/app/models/rails_pulse/queries/cards/execution_rate.rb +5 -11
  71. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +6 -12
  72. data/app/models/rails_pulse/queries/charts/database_load.rb +5 -8
  73. data/app/models/rails_pulse/queries/charts/execution_volume.rb +1 -1
  74. data/app/models/rails_pulse/request.rb +1 -1
  75. data/app/models/rails_pulse/routes/cards/error_rates.rb +5 -15
  76. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +5 -17
  77. data/app/models/rails_pulse/routes/cards/request_count_totals.rb +5 -17
  78. data/app/models/rails_pulse/routes/charts/error_rate.rb +1 -1
  79. data/app/models/rails_pulse/routes/charts/request_volume.rb +1 -1
  80. data/app/models/rails_pulse/summary.rb +5 -0
  81. data/app/models/rails_pulse/tables/base.rb +5 -6
  82. data/app/models/rails_pulse/time_range.rb +309 -0
  83. data/app/models/rails_pulse/time_window.rb +15 -0
  84. data/app/models/rails_pulse/writer_heartbeat.rb +90 -0
  85. data/app/serializers/rails_pulse/api/v1/deployment_serializer.rb +23 -0
  86. data/app/serializers/rails_pulse/api/v1/exception_group_serializer.rb +23 -0
  87. data/app/serializers/rails_pulse/api/v1/exception_occurrence_serializer.rb +22 -0
  88. data/app/serializers/rails_pulse/api/v1/job_run_serializer.rb +28 -0
  89. data/app/serializers/rails_pulse/api/v1/job_serializer.rb +26 -0
  90. data/app/serializers/rails_pulse/api/v1/query_serializer.rb +25 -0
  91. data/app/serializers/rails_pulse/api/v1/request_serializer.rb +22 -0
  92. data/app/serializers/rails_pulse/api/v1/route_serializer.rb +19 -0
  93. data/app/services/rails_pulse/cloud/batch.rb +83 -0
  94. data/app/services/rails_pulse/cloud/client.rb +68 -0
  95. data/app/services/rails_pulse/cloud/combined_metrics.rb +49 -0
  96. data/app/services/rails_pulse/cloud/health_item.rb +77 -0
  97. data/app/services/rails_pulse/cloud/path_prefix.rb +50 -0
  98. data/app/services/rails_pulse/cloud/preview.rb +83 -0
  99. data/app/services/rails_pulse/cloud/query_shape.rb +195 -0
  100. data/app/services/rails_pulse/cloud/record_items.rb +60 -0
  101. data/app/services/rails_pulse/cloud/route_pattern.rb +111 -0
  102. data/app/services/rails_pulse/cloud/summary_items.rb +188 -0
  103. data/app/services/rails_pulse/cloud/sync.rb +275 -0
  104. data/app/services/rails_pulse/config_recommendations.rb +132 -0
  105. data/app/services/rails_pulse/deployment_comparison.rb +177 -0
  106. data/app/services/rails_pulse/exception_capture_service.rb +1 -1
  107. data/app/services/rails_pulse/operations/change_point.rb +1 -1
  108. data/app/services/rails_pulse/operations/comparison.rb +1 -1
  109. data/app/services/rails_pulse/period_insights.rb +131 -0
  110. data/app/services/rails_pulse/sql_query_normalizer.rb +222 -122
  111. data/app/services/rails_pulse/summary_service/from_child_periods.rb +161 -0
  112. data/app/services/rails_pulse/summary_service/from_raw_rows.rb +115 -0
  113. data/app/services/rails_pulse/summary_service/metrics.rb +38 -0
  114. data/app/services/rails_pulse/summary_service.rb +121 -133
  115. data/app/services/rails_pulse/tag_filter_service.rb +5 -5
  116. data/app/views/layouts/rails_pulse/_time_range_selector.html.erb +4 -1
  117. data/app/views/rails_pulse/components/_operation_details_popover.html.erb +1 -1
  118. data/app/views/rails_pulse/components/_table_head.html.erb +2 -2
  119. data/app/views/rails_pulse/dashboard/_chart_panel.html.erb +6 -1
  120. data/app/views/rails_pulse/dashboard/_deployments_panel.html.erb +2 -2
  121. data/app/views/rails_pulse/dashboard/_health_badge.html.erb +29 -20
  122. data/app/views/rails_pulse/dashboard/_storage_panel.html.erb +13 -3
  123. data/app/views/rails_pulse/dashboard/index.html.erb +16 -2
  124. data/app/views/rails_pulse/jobs/_chart_tabs.html.erb +1 -0
  125. data/app/views/rails_pulse/jobs/index.html.erb +1 -1
  126. data/app/views/rails_pulse/queries/_chart_tabs.html.erb +1 -0
  127. data/app/views/rails_pulse/queries/index.html.erb +1 -1
  128. data/app/views/rails_pulse/queries/show.html.erb +1 -1
  129. data/app/views/rails_pulse/requests/index.html.erb +1 -1
  130. data/app/views/rails_pulse/routes/_chart_tabs.html.erb +1 -0
  131. data/app/views/rails_pulse/routes/index.html.erb +1 -1
  132. data/app/views/rails_pulse/routes/show.html.erb +1 -1
  133. data/app/views/rails_pulse/shared/_aggregation_zone_badge.html.erb +4 -0
  134. data/app/views/rails_pulse/storage/show.html.erb +39 -2
  135. data/config/routes.rb +23 -0
  136. data/db/rails_pulse_cloud_schema.rb +58 -0
  137. data/db/rails_pulse_migrate/20260506000001_create_rails_pulse_exceptions.rb +1 -1
  138. data/db/rails_pulse_migrate/20260921000001_create_rails_pulse_events.rb +21 -0
  139. data/db/rails_pulse_schema.rb +20 -2
  140. data/exe/rails-pulse +7 -0
  141. data/lib/generators/rails_pulse/base_methods.rb +19 -0
  142. data/lib/generators/rails_pulse/install_cloud_generator.rb +84 -0
  143. data/lib/generators/rails_pulse/templates/db/rails_pulse_cloud_schema.rb +58 -0
  144. data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +20 -2
  145. data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_cloud_tables.rb +30 -0
  146. data/lib/generators/rails_pulse/templates/rails_pulse.rb +71 -19
  147. data/lib/generators/rails_pulse/upgrade_generator.rb +9 -16
  148. data/lib/rails_pulse/cleanup_service.rb +37 -20
  149. data/lib/rails_pulse/cli/agent_files/agents.md +89 -0
  150. data/lib/rails_pulse/cli/agent_files/claude_skill.md +182 -0
  151. data/lib/rails_pulse/cli/base_command.rb +37 -0
  152. data/lib/rails_pulse/cli/client.rb +64 -0
  153. data/lib/rails_pulse/cli/config.rb +87 -0
  154. data/lib/rails_pulse/cli/configure.rb +75 -0
  155. data/lib/rails_pulse/cli/coverage.rb +91 -0
  156. data/lib/rails_pulse/cli/deployments.rb +54 -0
  157. data/lib/rails_pulse/cli/exceptions.rb +116 -0
  158. data/lib/rails_pulse/cli/formatter.rb +31 -0
  159. data/lib/rails_pulse/cli/insights.rb +70 -0
  160. data/lib/rails_pulse/cli/install.rb +145 -0
  161. data/lib/rails_pulse/cli/job_runs.rb +53 -0
  162. data/lib/rails_pulse/cli/jobs.rb +71 -0
  163. data/lib/rails_pulse/cli/main.rb +76 -0
  164. data/lib/rails_pulse/cli/mcp.rb +47 -0
  165. data/lib/rails_pulse/cli/queries.rb +67 -0
  166. data/lib/rails_pulse/cli/requests.rb +57 -0
  167. data/lib/rails_pulse/cli/routes.rb +73 -0
  168. data/lib/rails_pulse/configuration.rb +100 -2
  169. data/lib/rails_pulse/current.rb +11 -0
  170. data/lib/rails_pulse/engine.rb +12 -3
  171. data/lib/rails_pulse/installers/config_updater.rb +9 -2
  172. data/lib/rails_pulse/job_run_collector.rb +15 -15
  173. data/lib/rails_pulse/mcp/server.rb +78 -0
  174. data/lib/rails_pulse/mcp/tools/coverage.rb +96 -0
  175. data/lib/rails_pulse/mcp/tools/deployments.rb +117 -0
  176. data/lib/rails_pulse/mcp/tools/endpoint.rb +167 -0
  177. data/lib/rails_pulse/mcp/tools/errors.rb +104 -0
  178. data/lib/rails_pulse/mcp/tools/exception_detail.rb +118 -0
  179. data/lib/rails_pulse/mcp/tools/exceptions.rb +128 -0
  180. data/lib/rails_pulse/mcp/tools/helpers.rb +162 -0
  181. data/lib/rails_pulse/mcp/tools/insights.rb +99 -0
  182. data/lib/rails_pulse/mcp/tools/jobs.rb +155 -0
  183. data/lib/rails_pulse/mcp/tools/queries.rb +142 -0
  184. data/lib/rails_pulse/mcp/tools/routes.rb +108 -0
  185. data/lib/rails_pulse/mcp/tools/slow_requests.rb +118 -0
  186. data/lib/rails_pulse/middleware/request_collector.rb +9 -9
  187. data/lib/rails_pulse/scoped_inflector.rb +35 -0
  188. data/lib/rails_pulse/statistics.rb +27 -0
  189. data/lib/rails_pulse/subscribers/exception_subscriber.rb +2 -2
  190. data/lib/rails_pulse/subscribers/operation_subscriber.rb +15 -29
  191. data/lib/rails_pulse/tasks/status_reporter.rb +201 -0
  192. data/lib/rails_pulse/tracker.rb +76 -14
  193. data/lib/rails_pulse/version.rb +1 -1
  194. data/lib/rails_pulse.rb +0 -4
  195. data/lib/tasks/rails_pulse.rake +6 -0
  196. data/lib/tasks/rails_pulse_tasks.rake +7 -0
  197. data/public/rails-pulse-assets/rails-pulse.css +1 -1
  198. data/public/rails-pulse-assets/rails-pulse.js +13 -13
  199. metadata +123 -24
  200. data/app/controllers/concerns/page_timings.rb +0 -10
  201. data/app/controllers/concerns/response_range_concern.rb +0 -45
  202. data/app/controllers/concerns/time_range_concern.rb +0 -152
  203. data/app/controllers/concerns/zoom_range_concern.rb +0 -86
@@ -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"
@@ -0,0 +1,30 @@
1
+ # Generated by rails_pulse:install_cloud - loads the Rails Pulse Cloud schema definition
2
+ class InstallRailsPulseCloudTables < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
3
+ def up
4
+ if rails_pulse_cloud_installed?
5
+ say "Rails Pulse Cloud tables already exist. Skipping installation.", :yellow
6
+ return
7
+ end
8
+
9
+ # Load and execute the Cloud schema directly, so this migration always
10
+ # creates the tables as db/rails_pulse_cloud_schema.rb describes them.
11
+ schema_file = File.join(::Rails.root.to_s, "db/rails_pulse_cloud_schema.rb")
12
+ raise "Rails Pulse Cloud schema file not found at db/rails_pulse_cloud_schema.rb" unless File.exist?(schema_file)
13
+
14
+ say "Loading Rails Pulse Cloud schema from db/rails_pulse_cloud_schema.rb"
15
+ load schema_file
16
+ RailsPulse::CloudSchema.call(connection)
17
+ say "Rails Pulse Cloud tables created successfully"
18
+ end
19
+
20
+ def down
21
+ drop_table :rails_pulse_cloud_batches if table_exists?(:rails_pulse_cloud_batches)
22
+ drop_table :rails_pulse_cloud_installations if table_exists?(:rails_pulse_cloud_installations)
23
+ end
24
+
25
+ private
26
+
27
+ def rails_pulse_cloud_installed?
28
+ table_exists?(:rails_pulse_cloud_installations) && table_exists?(:rails_pulse_cloud_batches)
29
+ end
30
+ end
@@ -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,37 @@ 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"]
375
+
376
+ # ====================================================================================================
377
+ # RAILS PULSE CLOUD
378
+ # ====================================================================================================
379
+ # Off unless both api_key and application are set; until then Rails Pulse makes no network calls.
380
+ # When on, it sends Rails Pulse Cloud an hourly summary of each route, query, job and exception,
381
+ # exception group and deployment records, and a health update every minute. It never sends request
382
+ # URLs or parameters, values in SQL, exception messages, backtraces or job arguments; the detail stays
383
+ # in this database. Everything in deployment metadata is sent as recorded.
384
+ # Run `rails rails_pulse:cloud:preview` to print exactly what would be sent.
385
+ #
386
+ # Cloud keeps its state in two tables of its own. Add them with
387
+ # `rails generate rails_pulse:install_cloud`, then migrate.
388
+ #
389
+ # The hourly sync runs after RailsPulse::SummaryJob. Schedule the health update every minute:
390
+ # # config/recurring.yml
391
+ # rails_pulse_cloud_health:
392
+ # class: RailsPulse::CloudHealthJob
393
+ # schedule: every minute
394
+ #
395
+ # The key comes from your Application's settings in Rails Pulse Cloud; application is its slug.
396
+ # config.cloud.api_key = ENV["RAILS_PULSE_CLOUD_API_KEY"]
397
+ # config.cloud.application = "your-app"
398
+ # Defaults to Rails.env.
399
+ # config.cloud.environment = Rails.env
400
+ # config.cloud.url = "https://ingest.railspulse.com"
401
+ # Hosts are named by hostname, which can show internal addresses and changes with every container.
402
+ # Set a stable label instead (defaults to RAILS_PULSE_HOST_LABEL).
403
+ # config.cloud.host_label = ENV["RAILS_PULSE_HOST_LABEL"]
360
404
 
361
405
  # ====================================================================================================
362
406
  # DATA CLEANUP
@@ -376,6 +420,14 @@ RailsPulse.configure do |config|
376
420
  # Time-based retention - delete records older than this period
377
421
  config.full_retention_period = 30.days
378
422
 
423
+ # How long rows in rails_pulse_events are kept. The background writer's
424
+ # heartbeats are pruned after a day regardless.
425
+ config.event_retention_period = 90.days
426
+
427
+ # Event kinds exempt from event_retention_period — for rows a writer updates
428
+ # in place rather than appends.
429
+ config.event_retention_exempt_kinds = []
430
+
379
431
  # Count-based retention - maximum records to keep per table
380
432
  # After time-based cleanup, if tables still exceed these limits,
381
433
  # the oldest remaining records will be deleted to stay under the limit
@@ -404,12 +456,12 @@ RailsPulse.configure do |config|
404
456
  # config.baseline_window = 28.days
405
457
  # config.comparison_window = 1.day
406
458
 
407
- # Hourly summaries decide how precisely a change point can be placed. They are
408
- # pruned at this age because the 1-day view is otherwise the only thing that
409
- # reads them. Inside this window Rails Pulse can say a route slowed down at
410
- # 14:00; beyond it, the finest answer is the day. Raising this buys precision
411
- # at the cost of summary table growth.
412
- # config.hourly_summary_retention = 2.days
459
+ # Hourly summaries decide how precisely a change point can be placed, and
460
+ # deployment comparisons read them. Inside this window Rails Pulse can say a
461
+ # route slowed down at 14:00 and compare a deploy's hour before and after;
462
+ # beyond it, the finest answer is the day and a deploy can no longer be
463
+ # compared. Raising this buys precision at the cost of summary table growth.
464
+ # config.hourly_summary_retention = 7.days
413
465
 
414
466
  # A change is reported as a regression only when it clears both the ratio and
415
467
  # the absolute floor for its unit. The ratio alone flags trivial millisecond
@@ -60,22 +60,6 @@ module RailsPulse
60
60
  end
61
61
  end
62
62
 
63
- def has_separate_database_config?
64
- config_path = File.join(root_path, "config/database.yml")
65
- return false unless File.exist?(config_path)
66
-
67
- require "yaml"
68
- require "erb"
69
- # Process ERB before YAML parsing — database.yml files commonly use ERB
70
- # for environment-specific values. YAML.safe_load alone raises SyntaxError
71
- # on ERB tags.
72
- yaml_content = ERB.new(File.read(config_path)).result
73
- db_config = YAML.safe_load(yaml_content, aliases: true)
74
- db_config.values.any? { |env| env.is_a?(Hash) && env.key?("rails_pulse") }
75
- rescue
76
- false
77
- end
78
-
79
63
  def rails_pulse_tables_exist?
80
64
  return false unless defined?(ActiveRecord::Base)
81
65
 
@@ -129,6 +113,7 @@ module RailsPulse
129
113
  # Refresh the schema file so fresh databases (test, CI) built from
130
114
  # db/rails_pulse_schema.rb include all current columns and tables.
131
115
  copy_file "db/rails_pulse_schema.rb", "db/rails_pulse_schema.rb", force: true
116
+ refresh_cloud_schema
132
117
  sync_initializer
133
118
 
134
119
  gem_migrations = get_gem_migrations
@@ -152,6 +137,14 @@ module RailsPulse
152
137
  end
153
138
  end
154
139
 
140
+ # Only an app that ran rails_pulse:install_cloud has the Cloud schema
141
+ # file; it is refreshed like the main one, never added.
142
+ def refresh_cloud_schema
143
+ return unless File.exist?(File.join(root_path, "db/rails_pulse_cloud_schema.rb"))
144
+
145
+ copy_file "db/rails_pulse_cloud_schema.rb", "db/rails_pulse_cloud_schema.rb", force: true
146
+ end
147
+
155
148
  def announce_new_features(new_migrations)
156
149
  notices = FEATURE_NOTICES.filter_map do |migration_key, notice|
157
150
  notice if new_migrations.any? { |filename| filename.include?(migration_key) }
@@ -29,9 +29,11 @@ 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
36
+ record_run
35
37
  raise @failures.first[:error] if @failures.any?
36
38
 
37
39
  @stats
@@ -116,6 +118,22 @@ module RailsPulse
116
118
  end
117
119
  end
118
120
 
121
+ # event_retention_exempt_kinds are rows a writer updates in place, which
122
+ # must survive so "last ran" never becomes "never ran"
123
+ def perform_event_cleanup
124
+ return unless @config.event_retention_period
125
+ return unless RailsPulse::Event.table_available?
126
+
127
+ cutoff_time = [ @config.event_retention_period.ago, 1.hour.ago ].min
128
+ run_stage(:time_based, :events) do
129
+ delete_in_batches(
130
+ RailsPulse::Event
131
+ .where.not(kind: @config.event_retention_exempt_kinds)
132
+ .where(occurred_at: ...cutoff_time)
133
+ )
134
+ end
135
+ end
136
+
119
137
  def perform_count_based_cleanup
120
138
  return unless @config.max_table_records&.any?
121
139
 
@@ -313,37 +331,26 @@ module RailsPulse
313
331
  RailsPulse::ExceptionOccurrence.where.not(exception_group_id: preserved_ids)
314
332
  end
315
333
 
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.
334
+ # Prune the oldest deletable groups when over the configured cap. Preserved
335
+ # and ignored groups are outside the cap: counting them would make a cap
336
+ # that they alone fill impossible to satisfy, so every run would trim the
337
+ # deletable groups for no gain (decision 0017). Child occurrences must be
338
+ # deleted first — there is no ON DELETE CASCADE on the FK.
318
339
  def cleanup_exception_groups_by_count
319
340
  max_records = @config.max_table_records[:rails_pulse_exception_groups]
320
341
  return 0 unless max_records
321
342
 
322
- current_count = RailsPulse::ExceptionGroup.count
323
- return 0 if current_count <= max_records
324
-
325
- overage = current_count - max_records
326
343
  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
344
+ overage = scope.count - max_records
345
+ return 0 if overage <= 0
332
346
 
333
- deleted = delete_parents_in_batches(
347
+ delete_parents_in_batches(
334
348
  scope,
335
349
  order_column: :last_seen_at,
336
350
  children: RailsPulse::ExceptionOccurrence,
337
351
  foreign_key: :exception_group_id,
338
- limit: records_to_delete
352
+ limit: overage
339
353
  )
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
354
  end
348
355
 
349
356
  # Delete groups whose last occurrence was removed — the orphan check and
@@ -374,6 +381,16 @@ module RailsPulse
374
381
  end
375
382
  end
376
383
 
384
+ # rails_pulse:status reads this to tell whether cleanup is scheduled. A
385
+ # failure to record must not turn a successful cleanup into a failed one.
386
+ def record_run
387
+ return unless RailsPulse::Event.table_available?
388
+
389
+ RailsPulse::CleanupRun.record!(stats: @stats, failed_stages: @failures.map { |failure| failure[:stage] })
390
+ rescue StandardError => e
391
+ RailsPulse.logger.error "Could not record the cleanup run: #{e.class}: #{e.message}"
392
+ end
393
+
377
394
  # Returns the period_end of the most recent completed hourly overall-request
378
395
  # summary, or nil if the summary job has never run. Records with occurred_at
379
396
  # before this timestamp have been fully aggregated and are safe to delete.
@@ -0,0 +1,89 @@
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 timing, metadata and a before/after comparison |
54
+ | `rails_pulse_insights` | What needs attention over one hour, day, week or month, and threshold suggestions |
55
+
56
+ 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`. `rails_pulse_insights` instead reads one whole summary period (`period`: `hour`, `day`, `week` or `month`, and `at` for a time inside it), so its P95s are exact. 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.
57
+
58
+ ## CLI
59
+
60
+ The `rails-pulse` CLI works without MCP. Append `--json` for structured output.
61
+
62
+ | Command | Description |
63
+ |---------|-------------|
64
+ | `rails-pulse routes list` | Tracked routes (`--since` adds request stats) |
65
+ | `rails-pulse requests list` | Recorded requests with status and time filters |
66
+ | `rails-pulse queries list` | SQL queries (`--since` adds timing stats) |
67
+ | `rails-pulse jobs list` | Background jobs: lifetime stats, or one window's with `--since` / `--until` |
68
+ | `rails-pulse job_runs list` | Individual job runs with errors |
69
+ | `rails-pulse exceptions list` | Exception groups with status and occurrence counts |
70
+ | `rails-pulse exceptions show ID` | One group with backtraces and recent occurrences |
71
+ | `rails-pulse deployments list` | Recorded deployments, most recent first, with each one's comparison |
72
+ | `rails-pulse coverage show` | What has been recorded, how recently, and any collection gaps |
73
+ | `rails-pulse insights show` | What needs attention over one period, and threshold suggestions |
74
+
75
+ 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).
76
+
77
+ ## Authentication
78
+
79
+ 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.
80
+
81
+ ## Investigation pattern
82
+
83
+ 1. Pin the time window to a deployment (`rails_pulse_deployments`); its `comparison` says whether response time or error rate got worse in the hour after it
84
+ 2. Identify slow or failing endpoints (`rails_pulse_insights` for the last day or week, `rails_pulse_slow_requests` for any window); resolve names with `rails_pulse_routes`
85
+ 3. Profile the suspect endpoint (`rails_pulse_endpoint`)
86
+ 4. Check errors (`rails_pulse_errors`)
87
+ 5. Inspect SQL and jobs (`rails_pulse_queries`, `rails_pulse_jobs`)
88
+ 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
89
+ 7. Fix, deploy, and re-check the same endpoint
@@ -0,0 +1,182 @@
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 timing, metadata, and whether each one degraded response time or error rate |
37
+ | `rails_pulse_insights` | What needs attention over one hour, day, week or month, and whether the thresholds fit it |
38
+
39
+ ### CLI
40
+
41
+ The `rails-pulse` executable ships with the gem. Add `--json` for structured output.
42
+
43
+ | Command | Purpose |
44
+ |---------|---------|
45
+ | `rails-pulse routes list --json` | Tracked routes (add `--since` for stats) |
46
+ | `rails-pulse requests list --json` | Recorded requests with status and time filters |
47
+ | `rails-pulse queries list --json` | SQL queries (add `--since` for timing stats) |
48
+ | `rails-pulse jobs list --json` | Background jobs: lifetime stats, or one window's with `--since` / `--until` |
49
+ | `rails-pulse job_runs list --json` | Individual job runs with error class and message |
50
+ | `rails-pulse exceptions list --json` | Exception groups with status, count, location and message |
51
+ | `rails-pulse exceptions show ID --json` | One group with backtraces, request, params and deploy SHA |
52
+ | `rails-pulse deployments list --json` | Recorded deployments, most recent first, each with its before/after comparison |
53
+ | `rails-pulse coverage show --json` | What has been recorded, how recently, and any collection gaps |
54
+ | `rails-pulse insights show --period week --json` | What needs attention over one period, and suggested threshold changes |
55
+
56
+ ## Investigation workflow
57
+
58
+ ### 1. Establish the time period
59
+
60
+ Check recent deployments first; a regression usually lines up with one.
61
+
62
+ ```
63
+ rails_pulse_deployments(period: "last_7_days")
64
+ rails-pulse deployments list --json
65
+ ```
66
+
67
+ Each deployment carries a `comparison` of the hour before the deploy started against the hour
68
+ after it finished. `degraded` means average or p95 response time got more than 1.5x and at
69
+ least 50ms worse, or the error rate more than 1.25x and at least a percentage point worse; the
70
+ `metrics` say which. `insufficient_data` means fewer than 10 requests in either hour, so it is
71
+ not an all-clear; `pending` means the hour after has not been summarized yet; `unavailable`
72
+ means it never will be because SummaryJob is not running. Use the `before` and `after` bounds
73
+ as `since`/`until` for the tools below to see which endpoints changed.
74
+
75
+ ### 2. Identify affected endpoints
76
+
77
+ For "what should I look at", start with the needs-attention list for the last complete day or
78
+ week; each item carries the route, query or job `id` to drill into.
79
+
80
+ ```
81
+ rails_pulse_insights(period: "day")
82
+ rails-pulse insights show --period week --json
83
+ ```
84
+
85
+ ```
86
+ rails_pulse_slow_requests(period: "last_24_hours", limit: 10)
87
+ rails_pulse_routes(search: "checkout", period: "last_7_days")
88
+ rails-pulse requests list --limit 50 --json
89
+ ```
90
+
91
+ ### 3. Profile the suspect endpoint
92
+
93
+ ```
94
+ rails_pulse_endpoint(endpoint: "CheckoutController#create", period: "last_7_days")
95
+ rails-pulse requests list --since 2026-06-01T00:00:00Z --json
96
+ ```
97
+
98
+ ### 4. Check errors
99
+
100
+ ```
101
+ rails_pulse_errors(period: "last_24_hours", status: "5xx")
102
+ rails-pulse requests list --status 5xx --since 2026-06-01T00:00:00Z --json
103
+ ```
104
+
105
+ ### 5. Inspect SQL and background jobs
106
+
107
+ ```
108
+ rails_pulse_queries(period: "last_24_hours", sort: "total_duration")
109
+ rails_pulse_queries(n_plus_one_only: true)
110
+ rails_pulse_jobs(period: "last_24_hours")
111
+ rails-pulse queries list --since 2026-06-01T00:00:00Z --json
112
+ rails-pulse job_runs list --status failed --json
113
+ ```
114
+
115
+ ### 6. Correlate with source code
116
+
117
+ Pass the endpoint's `route_id` to `rails_pulse_queries` to see only the SQL that ran inside it,
118
+ with `source_locations` naming the file and line each query was issued from.
119
+
120
+ ```
121
+ rails_pulse_queries(route: 42, period: "last_24_hours")
122
+ ```
123
+
124
+ Start at those locations rather than searching for the SQL. Look for N+1 queries, missing
125
+ indexes, expensive work in the request path, unnecessary serialization and missing caching.
126
+
127
+ ### 7. Fix, then validate
128
+
129
+ After the fix is deployed, re-check the same endpoint over the period since the deploy.
130
+
131
+ ```
132
+ rails_pulse_deployments(period: "last_24_hours")
133
+ rails_pulse_endpoint(endpoint: "CheckoutController#create", period: "last_hour")
134
+ ```
135
+
136
+ To show a change rather than a snapshot, measure the same endpoint over two fixed windows
137
+ either side of the release. Every windowed tool takes `since` and `until` as ISO 8601
138
+ timestamps, read as UTC when no zone is given, and echoes the bounds it used back as `window`.
139
+
140
+ ```
141
+ rails_pulse_endpoint(endpoint: "CheckoutController#create", since: "2026-09-24T12:00:00Z", until: "2026-09-25T12:00:00Z")
142
+ rails_pulse_endpoint(endpoint: "CheckoutController#create", since: "2026-09-25T12:00:00Z", until: "2026-09-26T12:00:00Z")
143
+ ```
144
+
145
+ Use the deploy time as the boundary, and exclude the minutes around it when a rolling deploy
146
+ makes them a mix of both versions.
147
+
148
+ ## Guidelines
149
+
150
+ - **SQL is not always the cause.** External API calls, serialization, view rendering and application logic slow requests too.
151
+ - **Use more than one tool.** Cross-reference latency with error rates, SQL timing and job performance before concluding.
152
+ - **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.
153
+ - **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.
154
+ - **Check the error rate.** A fast endpoint with a high error rate may be failing early rather than performing well.
155
+ - **Read percentiles, not just averages.** A low average with a high p95 or p99 means intermittent trouble.
156
+ - **Weigh request volume.** A slow endpoint nobody calls may not be worth the work.
157
+ - **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.
158
+ - **Threshold suggestions are a starting point.** `rails_pulse_insights` suggests a `config_snippet` from one period's data. Check the period was typical (not a deploy day or an incident) before proposing the change, and say which period it came from.
159
+ - **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.
160
+
161
+ ## Authentication
162
+
163
+ 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.
164
+
165
+ ```
166
+ RAILS_PULSE_URL=https://myapp.com
167
+ RAILS_PULSE_TOKEN=my-secret-token
168
+ RAILS_PULSE_MOUNT_PATH=/rails_pulse # optional, default /rails_pulse
169
+ ```
170
+
171
+ ```yaml
172
+ # ~/.rails-pulse
173
+ url: https://myapp.com
174
+ token: my-secret-token
175
+ mount_path: /rails_pulse
176
+ ```
177
+
178
+ Interactive setup: `rails-pulse configure`.
179
+
180
+ ## CLI reference
181
+
182
+ 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