rails_pulse 0.3.3 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +299 -0
- data/README.md +44 -3
- data/app/assets/stylesheets/rails_pulse/application.css +21 -32
- data/app/assets/stylesheets/rails_pulse/components/backtrace.css +238 -0
- data/app/assets/stylesheets/rails_pulse/components/badge.css +32 -0
- data/app/assets/stylesheets/rails_pulse/components/button.css +2 -36
- data/app/assets/stylesheets/rails_pulse/components/card.css +8 -11
- data/app/assets/stylesheets/rails_pulse/components/chart.css +16 -3
- data/app/assets/stylesheets/rails_pulse/components/csp_safe_positioning.css +5 -39
- data/app/assets/stylesheets/rails_pulse/components/dashboard.css +0 -52
- data/app/assets/stylesheets/rails_pulse/components/deployments.css +39 -0
- data/app/assets/stylesheets/rails_pulse/components/dialog.css +0 -5
- data/app/assets/stylesheets/rails_pulse/components/input.css +0 -14
- data/app/assets/stylesheets/rails_pulse/components/layouts.css +0 -20
- data/app/assets/stylesheets/rails_pulse/components/menu.css +0 -16
- data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +0 -16
- data/app/assets/stylesheets/rails_pulse/components/storage.css +187 -0
- data/app/assets/stylesheets/rails_pulse/components/tags.css +0 -14
- data/app/assets/stylesheets/rails_pulse/components/time_range_selector.css +5 -5
- data/app/assets/stylesheets/rails_pulse/components/utilities.css +0 -17
- data/app/controllers/concerns/chart_table_concern.rb +15 -3
- data/app/controllers/concerns/ransack_params_concern.rb +18 -0
- data/app/controllers/concerns/response_range_concern.rb +1 -1
- data/app/controllers/concerns/time_range_concern.rb +28 -14
- data/app/controllers/rails_pulse/application_controller.rb +104 -31
- data/app/controllers/rails_pulse/dashboard_controller.rb +9 -0
- data/app/controllers/rails_pulse/deployments_controller.rb +26 -5
- data/app/controllers/rails_pulse/exception_occurrences_controller.rb +19 -0
- data/app/controllers/rails_pulse/exceptions_controller.rb +119 -0
- data/app/controllers/rails_pulse/job_runs_controller.rb +1 -1
- data/app/controllers/rails_pulse/requests_controller.rb +1 -1
- data/app/controllers/rails_pulse/routes_controller.rb +7 -1
- data/app/controllers/rails_pulse/storage_controller.rb +7 -0
- data/app/controllers/rails_pulse/tags_controller.rb +2 -2
- data/app/helpers/rails_pulse/application_helper.rb +1 -0
- data/app/helpers/rails_pulse/backtrace_helper.rb +85 -0
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +4 -1
- data/app/helpers/rails_pulse/chart_helper.rb +0 -3
- data/app/helpers/rails_pulse/route_helper.rb +8 -16
- data/app/javascript/rails_pulse/application.js +29 -8
- data/app/javascript/rails_pulse/controllers/clipboard_controller.js +58 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +16 -10
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +6 -2
- data/app/javascript/rails_pulse/controllers/popover_controller.js +4 -0
- data/app/models/concerns/rails_pulse/taggable.rb +11 -6
- data/app/models/rails_pulse/cards/base.rb +53 -0
- data/app/models/rails_pulse/dashboard/health_summary.rb +52 -0
- data/app/models/rails_pulse/dashboard/needs_attention.rb +73 -1
- data/app/models/rails_pulse/dashboard/storage_status.rb +490 -0
- data/app/models/rails_pulse/deployment.rb +47 -1
- data/app/models/rails_pulse/exception_group.rb +37 -0
- data/app/models/rails_pulse/exception_occurrence.rb +27 -0
- data/app/models/rails_pulse/exceptions/cards/exception_rate.rb +116 -0
- data/app/models/rails_pulse/exceptions/cards/open_groups.rb +49 -0
- data/app/models/rails_pulse/exceptions/cards/total_occurrences.rb +62 -0
- data/app/models/rails_pulse/exceptions/charts/occurrence_volume.rb +56 -0
- data/app/models/rails_pulse/exceptions/raw_data.rb +216 -0
- data/app/models/rails_pulse/jobs/cards/p95_duration.rb +8 -2
- data/app/models/rails_pulse/operation.rb +13 -4
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +8 -3
- data/app/models/rails_pulse/request.rb +5 -1
- data/app/models/rails_pulse/route.rb +71 -21
- data/app/models/rails_pulse/routes/cards/error_rates.rb +6 -3
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +8 -3
- data/app/models/rails_pulse/routes/tables/index.rb +8 -6
- data/app/models/rails_pulse/summary.rb +21 -10
- data/app/models/rails_pulse/tables/base.rb +4 -2
- data/app/services/rails_pulse/analysis/explain_plan_analyzer.rb +47 -12
- data/app/services/rails_pulse/exception_capture_service.rb +228 -0
- data/app/services/rails_pulse/exception_message_sanitizer.rb +152 -0
- data/app/services/rails_pulse/operations/change_point.rb +144 -0
- data/app/services/rails_pulse/operations/compare.rb +92 -0
- data/app/services/rails_pulse/operations/comparison.rb +126 -0
- data/app/services/rails_pulse/operations/metric.rb +63 -0
- data/app/services/rails_pulse/operations/series.rb +84 -0
- data/app/services/rails_pulse/operations/subject.rb +77 -0
- data/app/services/rails_pulse/route_controller_action_backfiller.rb +110 -0
- data/app/services/rails_pulse/route_merger.rb +99 -0
- data/app/services/rails_pulse/route_migrator.rb +139 -0
- data/app/services/rails_pulse/route_path_normalizer.rb +79 -0
- data/app/services/rails_pulse/route_recognizer.rb +47 -0
- data/app/services/rails_pulse/summary_service.rb +45 -0
- data/app/services/rails_pulse/tag_filter_service.rb +5 -3
- data/app/views/layouts/rails_pulse/_menu_items.html.erb +7 -0
- data/app/views/layouts/rails_pulse/_onboarding_banner.html.erb +8 -0
- data/app/views/layouts/rails_pulse/_setup_screen.html.erb +5 -5
- data/app/views/layouts/rails_pulse/application.html.erb +4 -4
- data/app/views/rails_pulse/components/_empty_state.html.erb +1 -1
- data/app/views/rails_pulse/components/_panel.html.erb +16 -6
- data/app/views/rails_pulse/components/_raw_data.html.erb +34 -0
- data/app/views/rails_pulse/dashboard/_chart_panel.html.erb +6 -8
- data/app/views/rails_pulse/dashboard/_deployments_panel.html.erb +64 -0
- data/app/views/rails_pulse/dashboard/_health_badge.html.erb +21 -10
- data/app/views/rails_pulse/dashboard/_storage_panel.html.erb +67 -0
- data/app/views/rails_pulse/dashboard/index.html.erb +22 -1
- data/app/views/rails_pulse/deployments/_status_badge.html.erb +5 -0
- data/app/views/rails_pulse/deployments/_table.html.erb +29 -0
- data/app/views/rails_pulse/deployments/index.html.erb +15 -0
- data/app/views/rails_pulse/deployments/show.html.erb +62 -0
- data/app/views/rails_pulse/exception_occurrences/show.html.erb +45 -0
- data/app/views/rails_pulse/exceptions/_backtrace.html.erb +33 -0
- data/app/views/rails_pulse/exceptions/_metric_cards.html.erb +6 -0
- data/app/views/rails_pulse/exceptions/_occurrences_table.html.erb +23 -0
- data/app/views/rails_pulse/exceptions/_status_badge.html.erb +6 -0
- data/app/views/rails_pulse/exceptions/_table.html.erb +33 -0
- data/app/views/rails_pulse/exceptions/index.html.erb +66 -0
- data/app/views/rails_pulse/exceptions/show.html.erb +52 -0
- data/app/views/rails_pulse/queries/_raw_data.html.erb +1 -23
- data/app/views/rails_pulse/requests/_table.html.erb +1 -1
- data/app/views/rails_pulse/routes/_requests_table.html.erb +5 -0
- data/app/views/rails_pulse/routes/_table.html.erb +22 -8
- data/app/views/rails_pulse/routes/index.html.erb +5 -1
- data/app/views/rails_pulse/routes/show.html.erb +5 -0
- data/app/views/rails_pulse/storage/show.html.erb +123 -0
- data/config/routes.rb +13 -4
- data/db/rails_pulse_migrate/20260506000001_create_rails_pulse_exceptions.rb +50 -0
- data/db/rails_pulse_migrate/20260507000001_add_actual_query_to_operations.rb +10 -5
- data/db/rails_pulse_migrate/20260610000001_add_controller_action_to_rails_pulse_routes.rb +8 -0
- data/db/rails_pulse_migrate/20260610000002_change_rails_pulse_routes_to_multi_verb_model.rb +185 -0
- data/db/rails_pulse_migrate/20260610000003_add_null_action_unique_index_to_routes.rb +32 -0
- data/db/rails_pulse_migrate/20260823000001_add_location_to_exception_groups.rb +12 -0
- data/db/rails_pulse_schema.rb +54 -6
- data/lib/generators/rails_pulse/base_methods.rb +2 -0
- data/lib/generators/rails_pulse/install_generator.rb +5 -0
- data/lib/generators/rails_pulse/schema_parser.rb +22 -2
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +54 -6
- data/lib/generators/rails_pulse/templates/migrations/upgrade_rails_pulse_tables.rb +1 -1
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +157 -16
- data/lib/generators/rails_pulse/upgrade_generator.rb +166 -38
- data/lib/rails_pulse/cleanup_service.rb +110 -15
- data/lib/rails_pulse/configuration.rb +125 -6
- data/lib/rails_pulse/engine.rb +32 -17
- data/lib/rails_pulse/installers/config_updater.rb +202 -0
- data/lib/rails_pulse/job_run_collector.rb +43 -16
- data/lib/rails_pulse/middleware/asset_server.rb +11 -0
- data/lib/rails_pulse/middleware/request_collector.rb +9 -4
- data/lib/rails_pulse/packaged_assets.rb +147 -0
- data/lib/rails_pulse/route_indexes.rb +66 -0
- data/lib/rails_pulse/subscribers/exception_subscriber.rb +36 -0
- data/lib/rails_pulse/tracker.rb +29 -10
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/rails_pulse.rb +1 -0
- data/lib/rails_pulse_server.ru +16 -47
- data/lib/tasks/rails_pulse.rake +83 -4
- data/lib/tasks/rails_pulse_assets.rake +15 -0
- data/lib/tasks/rails_pulse_tasks.rake +29 -0
- data/public/rails-pulse-assets/rails-pulse-icons.js +6 -2
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +18 -117
- metadata +78 -37
- data/Rakefile +0 -554
- data/app/javascript/rails_pulse/controllers/form_controller.js +0 -39
- data/app/javascript/rails_pulse/controllers/period_selector_controller.js +0 -19
- data/app/javascript/rails_pulse/controllers/timezone_controller.js +0 -89
- data/app/javascript/rails_pulse/theme.js +0 -416
- data/config/brakeman.ignore +0 -190
- data/config/brakeman.yml +0 -68
- data/config/initializers/rails_pulse.rb +0 -287
- data/public/rails-pulse-assets/csp-test.js +0 -120
- data/public/rails-pulse-assets/rails-pulse-icons.js.map +0 -13
- data/public/rails-pulse-assets/rails-pulse.css.map +0 -1
- data/public/rails-pulse-assets/rails-pulse.js.map +0 -7
- data/vendor/assets/javascripts/rails-pulse-icons.js +0 -93
- data/vendor/assets/javascripts/rails-pulse-icons.js.map +0 -13
- data/vendor/assets/javascripts/rails-pulse.js +0 -157
- data/vendor/assets/javascripts/rails-pulse.js.map +0 -7
- data/vendor/assets/stylesheets/rails-pulse.css +0 -1
- data/vendor/assets/stylesheets/rails-pulse.css.map +0 -1
|
@@ -6,6 +6,11 @@ RailsPulse.configure do |config|
|
|
|
6
6
|
# Enable or disable Rails Pulse
|
|
7
7
|
config.enabled = true
|
|
8
8
|
|
|
9
|
+
# Tracking writes happen on a background thread by default (see `config.async`
|
|
10
|
+
# under ADVANCED). Transactional tests share one database connection across
|
|
11
|
+
# threads, so write inline there to keep the writer off the test's connection.
|
|
12
|
+
config.async = false if Rails.env.test?
|
|
13
|
+
|
|
9
14
|
# ====================================================================================================
|
|
10
15
|
# THRESHOLDS
|
|
11
16
|
# ====================================================================================================
|
|
@@ -116,6 +121,58 @@ RailsPulse.configure do |config|
|
|
|
116
121
|
|
|
117
122
|
config.tags = [ "ignored", "critical", "experimental" ]
|
|
118
123
|
|
|
124
|
+
# ====================================================================================================
|
|
125
|
+
# EXCEPTION TRACKING
|
|
126
|
+
# ====================================================================================================
|
|
127
|
+
# When enabled, Rails Pulse captures unhandled exceptions raised during web
|
|
128
|
+
# requests and failed background jobs, groups them by class and location, and
|
|
129
|
+
# displays them in the Exceptions tab. Rake tasks are not captured automatically —
|
|
130
|
+
# call ExceptionCaptureService.capture yourself if needed.
|
|
131
|
+
#
|
|
132
|
+
# Capture runs synchronously on the calling thread (upsert + insert). That keeps
|
|
133
|
+
# the implementation simple for v1; under an error storm it adds DB latency to
|
|
134
|
+
# failing requests and jobs. Set track_exceptions = false to disable.
|
|
135
|
+
#
|
|
136
|
+
# The gem default is false so existing installs do not start capturing on
|
|
137
|
+
# upgrade. New apps get true from this template; the upgrade generator
|
|
138
|
+
# inserts false into existing initializers.
|
|
139
|
+
#
|
|
140
|
+
# Backtraces store the first 50 frames. Request params are filtered via Rails'
|
|
141
|
+
# filter_parameters. Exception messages are filtered too: the SQL and echoed
|
|
142
|
+
# values in ActiveRecord::StatementInvalid messages are stripped, and any
|
|
143
|
+
# `key=value` / `key: value` fragment whose key matches filter_parameters
|
|
144
|
+
# (password=…, token: …) is masked. Messages are truncated to 500 characters.
|
|
145
|
+
|
|
146
|
+
# Enable or disable exception tracking
|
|
147
|
+
config.track_exceptions = true
|
|
148
|
+
|
|
149
|
+
# Capture request params with each exception occurrence.
|
|
150
|
+
# Params are filtered using Rails' filter_parameters config (passwords, tokens, etc. are redacted).
|
|
151
|
+
# Occurrences with params larger than 10KB after filtering are stored without params.
|
|
152
|
+
# Set to false to disable entirely, e.g. for strict data-minimization requirements.
|
|
153
|
+
config.capture_exception_params = true
|
|
154
|
+
|
|
155
|
+
# Extra redaction for exception messages, applied after the built-in
|
|
156
|
+
# filter_parameters pass. Receives the message and the exception; return the
|
|
157
|
+
# message to store. If the hook raises, the message is stored as "[FILTERED]".
|
|
158
|
+
# config.exception_message_filter = ->(message, exception) {
|
|
159
|
+
# message.gsub(/\b\d{13,16}\b/, "[FILTERED]") # card-number-shaped digits
|
|
160
|
+
# }
|
|
161
|
+
|
|
162
|
+
# How many times an exception group has to fire over the dashboard period
|
|
163
|
+
# before it is called out. These are occurrence counts, not durations.
|
|
164
|
+
# config.exception_thresholds = {
|
|
165
|
+
# warning: 10,
|
|
166
|
+
# critical: 100
|
|
167
|
+
# }
|
|
168
|
+
|
|
169
|
+
# Capture the raw (unparameterized) SQL for each operation.
|
|
170
|
+
# WARNING: mysql2 defaults to prepared_statements: false, so every literal
|
|
171
|
+
# value (emails, passwords, tokens) is inlined and stored in plaintext.
|
|
172
|
+
# Same applies to PostgreSQL behind PgBouncer in transaction-pool mode.
|
|
173
|
+
# Default: false (upgrade-safe). New installs may opt in after review.
|
|
174
|
+
# config.capture_actual_sql = true
|
|
175
|
+
|
|
119
176
|
# ====================================================================================================
|
|
120
177
|
# BACKGROUND JOBS
|
|
121
178
|
# ====================================================================================================
|
|
@@ -144,8 +201,21 @@ RailsPulse.configure do |config|
|
|
|
144
201
|
config.ignored_queues = []
|
|
145
202
|
|
|
146
203
|
# Capture job arguments for debugging (may contain sensitive data)
|
|
147
|
-
#
|
|
148
|
-
|
|
204
|
+
# WARNING: job arguments may contain user credentials, PII, or API keys.
|
|
205
|
+
# Default: false. Set to true only after reviewing your job argument contents.
|
|
206
|
+
config.capture_job_arguments = false
|
|
207
|
+
|
|
208
|
+
# Job tracking mode: :universal (all jobs) or :opt_in (only explicitly tracked jobs)
|
|
209
|
+
# config.job_tracking_mode = :universal
|
|
210
|
+
|
|
211
|
+
# Per-adapter settings. Disable adapters or opt into queue-depth tracking:
|
|
212
|
+
# config.job_adapters = {
|
|
213
|
+
# sidekiq: { enabled: true, track_queue_depth: false },
|
|
214
|
+
# solid_queue: { enabled: true, track_recurring: false },
|
|
215
|
+
# good_job: { enabled: true, track_cron: false },
|
|
216
|
+
# delayed_job: { enabled: true },
|
|
217
|
+
# resque: { enabled: true }
|
|
218
|
+
# }
|
|
149
219
|
|
|
150
220
|
# ====================================================================================================
|
|
151
221
|
# DATABASE CONFIGURATION
|
|
@@ -177,26 +247,37 @@ RailsPulse.configure do |config|
|
|
|
177
247
|
# password: <%= Rails.application.credentials.dig(:rails_pulse, :database_password) %>
|
|
178
248
|
# host: localhost
|
|
179
249
|
# pool: 5
|
|
250
|
+
# migrations_paths: db/rails_pulse_migrate
|
|
251
|
+
# schema_dump: false
|
|
180
252
|
|
|
181
253
|
# ====================================================================================================
|
|
182
254
|
# AUTHENTICATION
|
|
183
255
|
# ====================================================================================================
|
|
184
256
|
# Configure authentication to secure access to the Rails Pulse dashboard.
|
|
185
|
-
# Authentication is ENABLED BY DEFAULT
|
|
257
|
+
# Authentication is ENABLED BY DEFAULT outside development and test.
|
|
186
258
|
#
|
|
187
|
-
# If
|
|
259
|
+
# If nothing below is configured, Rails Pulse will use HTTP Basic Auth
|
|
188
260
|
# with credentials from RAILS_PULSE_USERNAME (default: 'admin') and RAILS_PULSE_PASSWORD
|
|
189
261
|
# environment variables. Set RAILS_PULSE_PASSWORD to enable this fallback.
|
|
190
|
-
#
|
|
191
|
-
# Uncomment and configure one of the following patterns based on your authentication system:
|
|
192
262
|
|
|
193
|
-
# Enable/disable authentication (enabled by default
|
|
194
|
-
# config.authentication_enabled = Rails.env.
|
|
263
|
+
# Enable/disable authentication (enabled by default outside development/test)
|
|
264
|
+
# config.authentication_enabled = !Rails.env.local?
|
|
195
265
|
|
|
196
|
-
# Where to redirect
|
|
266
|
+
# Where to redirect users when an authentication hook raises
|
|
197
267
|
# config.authentication_redirect_path = "/"
|
|
198
268
|
|
|
199
|
-
#
|
|
269
|
+
# RECOMMENDED: a fail-closed predicate. Receives the controller; return
|
|
270
|
+
# true to allow. Anything else (false, nil, no user) is a 403 Forbidden.
|
|
271
|
+
# config.authorize = ->(controller) { controller.current_user&.admin? }
|
|
272
|
+
#
|
|
273
|
+
# A zero-argument proc runs in the controller's context instead:
|
|
274
|
+
# config.authorize = proc { user_signed_in? && current_user.admin? }
|
|
275
|
+
|
|
276
|
+
# Alternatively, a hook that runs in the controller and DENIES BY RENDERING
|
|
277
|
+
# OR REDIRECTING. Returning false without responding is also a denial;
|
|
278
|
+
# returning nil (what `unless … end` returns on success) allows the request.
|
|
279
|
+
# Do not write predicate-style checks here — use config.authorize for those.
|
|
280
|
+
# If both are set, authentication_method runs first, then authorize.
|
|
200
281
|
|
|
201
282
|
# Example 1: Devise with admin role check
|
|
202
283
|
# config.authentication_method = proc {
|
|
@@ -220,7 +301,8 @@ RailsPulse.configure do |config|
|
|
|
220
301
|
# Example 4: Basic HTTP authentication
|
|
221
302
|
# config.authentication_method = proc {
|
|
222
303
|
# authenticate_or_request_with_http_basic do |username, password|
|
|
223
|
-
# username
|
|
304
|
+
# ActiveSupport::SecurityUtils.secure_compare(username, ENV['RAILS_PULSE_USERNAME']) &&
|
|
305
|
+
# ActiveSupport::SecurityUtils.secure_compare(password, ENV['RAILS_PULSE_PASSWORD'])
|
|
224
306
|
# end
|
|
225
307
|
# }
|
|
226
308
|
|
|
@@ -246,6 +328,10 @@ RailsPulse.configure do |config|
|
|
|
246
328
|
# config.deployment_api_token = Rails.application.credentials.dig(:rails_pulse, :deployment_api_token)
|
|
247
329
|
# config.deployment_api_token = ENV["RAILS_PULSE_DEPLOYMENT_TOKEN"]
|
|
248
330
|
#
|
|
331
|
+
# Limits: revision ≤ 255 characters, metadata ≤ 4 KB serialized, started_at at most
|
|
332
|
+
# one hour in the future. Rows beyond max_table_records[:rails_pulse_deployments]
|
|
333
|
+
# are pruned oldest-first by the cleanup task.
|
|
334
|
+
#
|
|
249
335
|
# Record a deployment from your CI/CD pipeline:
|
|
250
336
|
# curl -X POST https://yourapp.com/rails_pulse/deployments \
|
|
251
337
|
# -H "X-Rails-Pulse-Token: $RAILS_PULSE_DEPLOYMENT_TOKEN" \
|
|
@@ -273,15 +359,70 @@ RailsPulse.configure do |config|
|
|
|
273
359
|
config.archiving_enabled = true
|
|
274
360
|
|
|
275
361
|
# Time-based retention - delete records older than this period
|
|
276
|
-
config.full_retention_period =
|
|
362
|
+
config.full_retention_period = 30.days
|
|
277
363
|
|
|
278
364
|
# Count-based retention - maximum records to keep per table
|
|
279
365
|
# After time-based cleanup, if tables still exceed these limits,
|
|
280
366
|
# the oldest remaining records will be deleted to stay under the limit
|
|
281
367
|
config.max_table_records = {
|
|
282
|
-
rails_pulse_requests:
|
|
283
|
-
rails_pulse_operations:
|
|
284
|
-
rails_pulse_routes:
|
|
285
|
-
rails_pulse_queries:
|
|
368
|
+
rails_pulse_requests: 50_000, # HTTP requests (moderate volume)
|
|
369
|
+
rails_pulse_operations: 100_000, # Operations within requests (high volume)
|
|
370
|
+
rails_pulse_routes: 1_000, # Unique routes (low volume)
|
|
371
|
+
rails_pulse_queries: 10_000, # Normalized SQL queries (low volume)
|
|
372
|
+
rails_pulse_job_runs: 50_000, # Individual job executions (high volume)
|
|
373
|
+
rails_pulse_jobs: 1_000, # Unique job classes (low volume)
|
|
374
|
+
rails_pulse_exception_occurrences: 50_000, # Individual exception raises (high volume)
|
|
375
|
+
rails_pulse_exception_groups: 10_000, # Distinct exception sites (moderate volume)
|
|
376
|
+
rails_pulse_deployments: 1_000 # Deploy markers (low volume; oldest pruned first)
|
|
286
377
|
}
|
|
378
|
+
|
|
379
|
+
# ====================================================================================================
|
|
380
|
+
# HISTORICAL COMPARISON
|
|
381
|
+
# ====================================================================================================
|
|
382
|
+
|
|
383
|
+
# Rails Pulse compares recent behaviour against a route, query or job's own
|
|
384
|
+
# history to answer "what changed?" rather than only "what is slow?".
|
|
385
|
+
#
|
|
386
|
+
# The baseline is the traffic-weighted metric across `baseline_window` of day
|
|
387
|
+
# summaries; `comparison_window` is the recent slice measured against it. The
|
|
388
|
+
# two never overlap.
|
|
389
|
+
# config.baseline_window = 28.days
|
|
390
|
+
# config.comparison_window = 1.day
|
|
391
|
+
|
|
392
|
+
# Hourly summaries decide how precisely a change point can be placed. They are
|
|
393
|
+
# pruned at this age because the 1-day view is otherwise the only thing that
|
|
394
|
+
# reads them. Inside this window Rails Pulse can say a route slowed down at
|
|
395
|
+
# 14:00; beyond it, the finest answer is the day. Raising this buys precision
|
|
396
|
+
# at the cost of summary table growth.
|
|
397
|
+
# config.hourly_summary_retention = 2.days
|
|
398
|
+
|
|
399
|
+
# A change is reported as a regression only when it clears both the ratio and
|
|
400
|
+
# the absolute floor for its unit. The ratio alone flags trivial millisecond
|
|
401
|
+
# noise on fast endpoints; the floor alone flags slow endpoints that never
|
|
402
|
+
# actually changed. min_samples keeps low-traffic subjects quiet.
|
|
403
|
+
# config.regression_thresholds = {
|
|
404
|
+
# ratio: 1.5, # 50% worse than baseline
|
|
405
|
+
# min_delta_ms: 50.0, # ...and at least 50ms worse
|
|
406
|
+
# min_delta_rate: 1.0, # ...or 1 percentage point, for error rates
|
|
407
|
+
# min_samples: 100, # minimum observations on each side
|
|
408
|
+
# min_baseline_periods: 3 # minimum days of history before comparing
|
|
409
|
+
# }
|
|
410
|
+
|
|
411
|
+
# ====================================================================================================
|
|
412
|
+
# ADVANCED
|
|
413
|
+
# ====================================================================================================
|
|
414
|
+
|
|
415
|
+
# Use a custom logger (default: Rails.logger)
|
|
416
|
+
# config.logger = Logger.new("log/rails_pulse.log")
|
|
417
|
+
|
|
418
|
+
# Perform tracking writes in a background thread (default: true).
|
|
419
|
+
# When false, writes happen inline before the response is sent.
|
|
420
|
+
# config.async = true
|
|
421
|
+
|
|
422
|
+
# Show a dashboard banner when summary data is stale (default: true)
|
|
423
|
+
# config.warn_on_stale_summaries = true
|
|
424
|
+
|
|
425
|
+
# Set to false to skip dashboard middleware and asset serving entirely.
|
|
426
|
+
# Useful for standalone/API-only deployments that use a separate dashboard app.
|
|
427
|
+
# config.mount_dashboard = true
|
|
287
428
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require_relative "base_methods"
|
|
2
2
|
require_relative "schema_parser"
|
|
3
|
+
require_relative "../../rails_pulse/installers/config_updater"
|
|
3
4
|
|
|
4
5
|
module RailsPulse
|
|
5
6
|
module Generators
|
|
@@ -21,9 +22,10 @@ module RailsPulse
|
|
|
21
22
|
|
|
22
23
|
case @database_type
|
|
23
24
|
when :single
|
|
24
|
-
upgrade_installation(migration_dir: "db/migrate",
|
|
25
|
+
upgrade_installation(migration_dir: "db/migrate", migrate_command: "rails db:migrate")
|
|
25
26
|
when :separate
|
|
26
|
-
|
|
27
|
+
warn_if_missing_schema_dump_false
|
|
28
|
+
upgrade_installation(migration_dir: "db/rails_pulse_migrate", migrate_command: "rails db:migrate:rails_pulse")
|
|
27
29
|
when :schema_only
|
|
28
30
|
offer_conversion_to_migrations
|
|
29
31
|
when :not_installed
|
|
@@ -38,16 +40,20 @@ module RailsPulse
|
|
|
38
40
|
# Override with command line option if provided
|
|
39
41
|
return options[:database].to_sym if options[:database] != "detect"
|
|
40
42
|
|
|
41
|
-
#
|
|
43
|
+
# Determine database type before checking tables so the table lookup
|
|
44
|
+
# uses the correct connection pool for separate-database setups.
|
|
45
|
+
@is_separate_db = has_separate_database_config?
|
|
42
46
|
tables_exist = rails_pulse_tables_exist?
|
|
43
|
-
|
|
44
47
|
schema_path = File.join(root_path, "db/rails_pulse_schema.rb")
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
# :schema_only only applies to single-database users who have the schema file
|
|
50
|
+
# but haven't run the install migration yet. Separate-database users legitimately
|
|
51
|
+
# have no Rails Pulse tables on the primary connection — that is not an error.
|
|
52
|
+
if !tables_exist && !@is_separate_db && File.exist?(schema_path)
|
|
47
53
|
:schema_only
|
|
48
54
|
elsif !tables_exist
|
|
49
55
|
:not_installed
|
|
50
|
-
elsif
|
|
56
|
+
elsif @is_separate_db
|
|
51
57
|
:separate
|
|
52
58
|
else
|
|
53
59
|
:single
|
|
@@ -56,25 +62,30 @@ module RailsPulse
|
|
|
56
62
|
|
|
57
63
|
def has_separate_database_config?
|
|
58
64
|
config_path = File.join(root_path, "config/database.yml")
|
|
59
|
-
|
|
60
65
|
return false unless File.exist?(config_path)
|
|
61
66
|
|
|
62
67
|
require "yaml"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
#
|
|
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)
|
|
66
74
|
db_config.values.any? { |env| env.is_a?(Hash) && env.key?("rails_pulse") }
|
|
67
|
-
rescue
|
|
68
|
-
# If we can't read or parse the file, assume single database
|
|
75
|
+
rescue
|
|
69
76
|
false
|
|
70
77
|
end
|
|
71
78
|
|
|
72
79
|
def rails_pulse_tables_exist?
|
|
73
80
|
return false unless defined?(ActiveRecord::Base)
|
|
74
81
|
|
|
75
|
-
connection =
|
|
76
|
-
|
|
82
|
+
connection = if @is_separate_db && defined?(RailsPulse::ApplicationRecord)
|
|
83
|
+
RailsPulse::ApplicationRecord.connection
|
|
84
|
+
else
|
|
85
|
+
ActiveRecord::Base.connection
|
|
86
|
+
end
|
|
77
87
|
|
|
88
|
+
required_tables = get_rails_pulse_table_names
|
|
78
89
|
required_tables.all? { |table| connection.table_exists?(table) }
|
|
79
90
|
rescue
|
|
80
91
|
false
|
|
@@ -85,8 +96,41 @@ module RailsPulse
|
|
|
85
96
|
SchemaParser.new(schema_file).extract_table_names
|
|
86
97
|
end
|
|
87
98
|
|
|
99
|
+
# Notices printed when a feature's migration is newly copied into the host app.
|
|
100
|
+
# Key is a substring of the migration filename (without timestamp).
|
|
101
|
+
FEATURE_NOTICES = {
|
|
102
|
+
"create_rails_pulse_exceptions" => <<~NOTICE.rstrip
|
|
103
|
+
Exception tracking (opt-in)
|
|
104
|
+
|
|
105
|
+
This upgrade adds exception tables. Capture stays off for existing
|
|
106
|
+
installs until you opt in. After migrating, set the following in
|
|
107
|
+
config/initializers/rails_pulse.rb (the upgrade generator inserts
|
|
108
|
+
it as false — review with git diff) and restart:
|
|
109
|
+
|
|
110
|
+
config.track_exceptions = true
|
|
111
|
+
|
|
112
|
+
New installs enable this in the generated initializer. Messages and
|
|
113
|
+
request params are both redacted using Rails filter_parameters; set
|
|
114
|
+
config.exception_message_filter for anything app-specific.
|
|
115
|
+
|
|
116
|
+
Two new tables will be created:
|
|
117
|
+
- rails_pulse_exception_groups
|
|
118
|
+
- rails_pulse_exception_occurrences
|
|
119
|
+
|
|
120
|
+
To skip the tables entirely, delete the copied
|
|
121
|
+
*_create_rails_pulse_exceptions.rb migration before db:migrate.
|
|
122
|
+
(A later upgrade will copy it again unless that file remains in
|
|
123
|
+
your migrate folder.)
|
|
124
|
+
NOTICE
|
|
125
|
+
}.freeze
|
|
126
|
+
|
|
88
127
|
# Shared upgrade logic for both single and separate database setups
|
|
89
|
-
def upgrade_installation(migration_dir:,
|
|
128
|
+
def upgrade_installation(migration_dir:, migrate_command:)
|
|
129
|
+
# Refresh the schema file so fresh databases (test, CI) built from
|
|
130
|
+
# db/rails_pulse_schema.rb include all current columns and tables.
|
|
131
|
+
copy_file "db/rails_pulse_schema.rb", "db/rails_pulse_schema.rb", force: true
|
|
132
|
+
sync_initializer
|
|
133
|
+
|
|
90
134
|
gem_migrations = get_gem_migrations
|
|
91
135
|
existing_migrations = get_user_migrations(migration_dir)
|
|
92
136
|
new_migrations = gem_migrations - existing_migrations
|
|
@@ -99,18 +143,38 @@ module RailsPulse
|
|
|
99
143
|
end
|
|
100
144
|
|
|
101
145
|
say "\nMigrations copied successfully!", :green
|
|
102
|
-
|
|
103
|
-
|
|
146
|
+
announce_new_features(new_migrations)
|
|
147
|
+
include_route_backfill = requires_route_backfill?(new_migrations)
|
|
148
|
+
say_route_backfill_warning if include_route_backfill
|
|
149
|
+
say_next_steps(migrate_command, include_route_backfill: include_route_backfill)
|
|
104
150
|
else
|
|
105
|
-
upgrade_with_missing_columns(migration_dir: migration_dir,
|
|
151
|
+
upgrade_with_missing_columns(migration_dir: migration_dir, migrate_command: migrate_command)
|
|
106
152
|
end
|
|
107
153
|
end
|
|
108
154
|
|
|
109
|
-
def
|
|
155
|
+
def announce_new_features(new_migrations)
|
|
156
|
+
notices = FEATURE_NOTICES.filter_map do |migration_key, notice|
|
|
157
|
+
notice if new_migrations.any? { |filename| filename.include?(migration_key) }
|
|
158
|
+
end
|
|
159
|
+
return if notices.empty?
|
|
160
|
+
|
|
161
|
+
say "\n" + ("=" * 72), :yellow
|
|
162
|
+
notices.each_with_index do |notice, index|
|
|
163
|
+
say "" if index.positive?
|
|
164
|
+
notice.each_line { |line| say line.chomp, :yellow }
|
|
165
|
+
end
|
|
166
|
+
say ("=" * 72) + "\n", :yellow
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def upgrade_with_missing_columns(migration_dir:, migrate_command:)
|
|
110
170
|
missing_columns = detect_missing_columns
|
|
111
171
|
|
|
112
172
|
if missing_columns.empty?
|
|
113
|
-
|
|
173
|
+
if @initializer_updated
|
|
174
|
+
say "Schema is up to date. Review initializer changes with git diff.", :green
|
|
175
|
+
else
|
|
176
|
+
say "Rails Pulse is up to date! No migration needed.", :green
|
|
177
|
+
end
|
|
114
178
|
return
|
|
115
179
|
end
|
|
116
180
|
|
|
@@ -126,31 +190,91 @@ module RailsPulse
|
|
|
126
190
|
"#{migration_dir}/upgrade_rails_pulse_tables.rb"
|
|
127
191
|
)
|
|
128
192
|
|
|
129
|
-
say
|
|
193
|
+
say "\nUpgrade migration created successfully!", :green
|
|
194
|
+
missing_names = missing_columns.keys.map(&:to_s)
|
|
195
|
+
include_route_backfill = missing_names.intersect?(%w[controller_action http_methods])
|
|
196
|
+
say_route_backfill_warning if include_route_backfill
|
|
197
|
+
say_next_steps(migrate_command, include_route_backfill: include_route_backfill)
|
|
198
|
+
say "\nThis migration will add: #{missing_columns.keys.join(', ')}\n"
|
|
199
|
+
end
|
|
130
200
|
|
|
131
|
-
|
|
201
|
+
ROUTE_BACKFILL_MIGRATIONS = %w[
|
|
202
|
+
change_rails_pulse_routes_to_multi_verb_model
|
|
203
|
+
add_null_action_unique_index_to_routes
|
|
204
|
+
].freeze
|
|
132
205
|
|
|
133
|
-
|
|
134
|
-
|
|
206
|
+
def requires_route_backfill?(migrations)
|
|
207
|
+
migrations.any? do |name|
|
|
208
|
+
ROUTE_BACKFILL_MIGRATIONS.any? { |fragment| name.include?(fragment) }
|
|
209
|
+
end
|
|
210
|
+
end
|
|
135
211
|
|
|
136
|
-
|
|
212
|
+
def say_route_backfill_warning
|
|
213
|
+
say "\nIMPORTANT: This upgrade changes how routes are identified.", :yellow
|
|
214
|
+
say "Schema migrate alone leaves the Action column empty. After migrating, run:", :yellow
|
|
215
|
+
say " rails rails_pulse:migrate_routes", :yellow
|
|
216
|
+
say "Skipping this leaves GET/POST on the same path unmerged in the dashboard.", :yellow
|
|
217
|
+
say "\nWARNING: Migration 20260610000002 is irreversible (drops routes.method).", :red
|
|
218
|
+
say "db:rollback past this point requires a database restore.", :red
|
|
219
|
+
end
|
|
137
220
|
|
|
138
|
-
|
|
221
|
+
def say_next_steps(migrate_command, include_route_backfill:)
|
|
222
|
+
n = 1
|
|
223
|
+
say "\nNext steps:", :green
|
|
224
|
+
say "#{n}. Run: #{migrate_command}"
|
|
225
|
+
n += 1
|
|
226
|
+
if include_route_backfill
|
|
227
|
+
say "#{n}. Run: rails rails_pulse:migrate_routes"
|
|
228
|
+
n += 1
|
|
229
|
+
end
|
|
230
|
+
if @initializer_updated
|
|
231
|
+
say "#{n}. Review config/initializers/rails_pulse.rb with git diff"
|
|
232
|
+
n += 1
|
|
233
|
+
end
|
|
234
|
+
say "#{n}. Restart ALL processes (web + workers) together — a rolling restart"
|
|
235
|
+
say " that leaves old processes against the new schema breaks tracking."
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def sync_initializer
|
|
239
|
+
path = File.join(root_path, "config/initializers/rails_pulse.rb")
|
|
240
|
+
result = RailsPulse::Installers::ConfigUpdater.update(
|
|
241
|
+
destination: path,
|
|
242
|
+
source: File.join(self.class.source_root, "rails_pulse.rb")
|
|
243
|
+
)
|
|
244
|
+
@initializer_updated = result[:status] == :updated
|
|
245
|
+
return unless @initializer_updated
|
|
246
|
+
|
|
247
|
+
say "\nUpdated config/initializers/rails_pulse.rb with new settings:", :blue
|
|
248
|
+
result[:keys].each { |key| say " - config.#{key}", :blue }
|
|
249
|
+
result[:hash_keys].each { |key| say " - #{key}", :blue }
|
|
250
|
+
say "Review with git diff and keep or discard hunks.", :green
|
|
139
251
|
end
|
|
140
252
|
|
|
141
|
-
def
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
253
|
+
def warn_if_missing_schema_dump_false
|
|
254
|
+
return unless separate_database_missing_schema_dump_false?
|
|
255
|
+
|
|
256
|
+
say "\nIMPORTANT: Add schema_dump: false to the rails_pulse entry in", :yellow
|
|
257
|
+
say "config/database.yml. Without it, Rails may dump or load", :yellow
|
|
258
|
+
say "db/rails_pulse_structure.sql and db:migrate can fail with", :yellow
|
|
259
|
+
say "\"relation already exists\". Delete that structure file if present.", :yellow
|
|
146
260
|
end
|
|
147
261
|
|
|
148
|
-
def
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
262
|
+
def separate_database_missing_schema_dump_false?
|
|
263
|
+
config_path = File.join(root_path, "config/database.yml")
|
|
264
|
+
return false unless File.exist?(config_path)
|
|
265
|
+
|
|
266
|
+
require "yaml"
|
|
267
|
+
require "erb"
|
|
268
|
+
yaml_content = ERB.new(File.read(config_path)).result
|
|
269
|
+
db_config = YAML.safe_load(yaml_content, aliases: true)
|
|
270
|
+
pulse_entries = db_config.values.filter_map do |env|
|
|
271
|
+
env["rails_pulse"] if env.is_a?(Hash)
|
|
272
|
+
end
|
|
273
|
+
return false if pulse_entries.empty?
|
|
274
|
+
|
|
275
|
+
pulse_entries.any? { |entry| !entry.is_a?(Hash) || entry["schema_dump"] != false }
|
|
276
|
+
rescue
|
|
277
|
+
false
|
|
154
278
|
end
|
|
155
279
|
|
|
156
280
|
def offer_conversion_to_migrations
|
|
@@ -170,7 +294,11 @@ module RailsPulse
|
|
|
170
294
|
def detect_missing_columns
|
|
171
295
|
return {} unless rails_pulse_tables_exist?
|
|
172
296
|
|
|
173
|
-
connection =
|
|
297
|
+
connection = if @is_separate_db && defined?(RailsPulse::ApplicationRecord)
|
|
298
|
+
RailsPulse::ApplicationRecord.connection
|
|
299
|
+
else
|
|
300
|
+
ActiveRecord::Base.connection
|
|
301
|
+
end
|
|
174
302
|
missing = {}
|
|
175
303
|
|
|
176
304
|
get_expected_schema_from_file.each do |table_name, columns|
|