Exception Tracking

Capture unhandled exceptions from web requests and background jobs, group them by class and location, and review backtraces with filtered request params, all inside your own database.

Overview

Rails Pulse captures unhandled exceptions raised during web requests and failed background jobs, groups them by exception class and the application frame that raised them, and shows them in an Exceptions tab alongside your performance data. You can see recurring production errors without a separate error monitoring service, and nothing leaves your app.

Each exception group records the class, message, location, first and last seen times, total occurrences, and a status. Each occurrence stores the first 50 backtrace frames, the request URL and method, filtered request params, the environment, and the deployment revision that was live at the time (when deployment tracking is in use).

Rake tasks and other code outside a request or job are not captured automatically. Call RailsPulse::ExceptionCaptureService.capture(exception) yourself if you need them.

Enabling exception tracking

Exception tracking is opt-in. The gem default is false. New installs get true from the install generator’s initializer; existing installs upgraded from 0.3.x get an explicit false so nothing starts capturing on upgrade.

# config/initializers/rails_pulse.rb
RailsPulse.configure do |config|
  config.track_exceptions = true

  # Store request params with each occurrence, filtered via Rails' filter_parameters.
  # Occurrences whose params exceed 10 KB after filtering are stored without params.
  config.capture_exception_params = true
end

The Exceptions routes are only mounted when track_exceptions is true, so the tab disappears entirely when tracking is off.

Restart web and worker processes after changing these settings.

What is redacted

Exception messages are sanitized before they are stored:

  • key=value, key: value, and "key":"value" fragments are masked whenever the key matches your app’s config.filter_parameters, using the same rules Rails applies to logged params.
  • The SQL and echoed values in ActiveRecord::StatementInvalid messages are stripped. MySQL Duplicate entry '<value>' values and PostgreSQL DETAIL: clauses are redacted.
  • Messages are truncated to 500 characters.
  • Job failure messages stored on job runs go through the same sanitizer.

Request params are filtered with filter_parameters too. Set capture_exception_params = false to store no params at all, for example under strict data-minimisation requirements.

Backtrace source snippets are read only from app/, lib/, and config/routes.rb (.rb, .erb, .rake, .haml, .slim, .jbuilder). Initializers, config/database.yml, and .env are never displayed.

Custom message filter

For app-specific patterns, add a hook that runs after the built-in redaction. It receives the message and the exception and returns the message to store. If the hook raises, the message is stored as [FILTERED] rather than unfiltered.

RailsPulse.configure do |config|
  config.exception_message_filter = ->(message, exception) {
    message.gsub(/\b\d{13,16}\b/, "[FILTERED]")  # card-number-shaped digits
  }
end

Thresholds

Exception thresholds are occurrence counts over the selected dashboard period, not durations. They drive the health bar and the Needs Attention panel on the dashboard.

RailsPulse.configure do |config|
  config.exception_thresholds = {
    warning:  10,   # default
    critical: 100   # default
  }
end

Using the Exceptions tab

The index lists open exception groups by default, with a frequency chart for the selected time range. Each group shows how often it fires, when it was first and last seen, and where in your code it originates.

On a group’s detail page you can:

  • Read the most recent backtrace with source context for application frames
  • Browse individual occurrences, each with its request URL, filtered params, and deploy revision
  • Change the status to Open, Resolved, or Ignored. Resolved groups record when they were resolved; a resolved group that fires again is easy to spot because its last-seen time moves.
  • Mark a group as Preserve so cleanup never removes its occurrences
  • Copy the Raw Data block, a Markdown summary of the group with the backtrace, source snippets, and recent-versus-prior frequency, formatted for pasting into an LLM or an issue

Exceptions also feed the dashboard: the health bar counts firing groups, the Needs Attention panel surfaces groups over threshold, and the error rate cards include them.

Performance and storage

Capture runs synchronously on the calling thread, one upsert for the group and one insert for the occurrence. Under an error storm that adds database latency to already-failing requests and jobs. If that is a concern, set track_exceptions = false until the storm passes.

Cleanup applies to exception tables like everything else. Occurrences older than full_retention_period are removed, and max_table_records caps both tables:

config.max_table_records = {
  rails_pulse_exception_occurrences: 50_000,  # default
  rails_pulse_exception_groups: 10_000        # default
}

Groups marked Preserve keep their occurrences through cleanup. See Advanced Configuration.

Next Steps