RUBY ON RAILS · 19 MIN READ ·

Rails Sentry: Production Error Monitoring Setup, Custom Contexts, and Performance Tracing for Rails 8

Rails Sentry setup guide for Rails 8: error capture, custom contexts, source maps, performance tracing, sampling rates, and filtering noise in production.

Rails Sentry: Production Error Monitoring Setup, Custom Contexts, and Performance Tracing for Rails 8

A SaaS client of mine processed European VAT incorrectly for thirty-seven enterprise customers over six months. The bug was a division-by-zero in a tax calculation that triggered only when a billing period spanned a fiscal year boundary and the customer’s country had a non-integer VAT rate. It raised a ZeroDivisionError, which their global rescue handler caught and silently converted into a zero return value. Invoices went out with a line item of €0.00 VAT. The customers paid. No alert fired, no exception appeared in Slack, nobody noticed until a customer’s accountant flagged it during an audit.

The engineering team had logging. Every request produced structured JSON logs shipped to CloudWatch. The error was in those logs — ten thousand times, one for every affected billing run. Nobody was reading CloudWatch logs at the granularity needed to catch one error type buried in a sea of INFO-level noise.

Rails Sentry would have fired an alert on the first occurrence and grouped every subsequent occurrence under the same issue. The engineering team would have known within minutes, not months.

Why Structured Error Monitoring Is Not Optional

Logging is pull: you have to know something is wrong before you go look. Error monitoring is push: it tells you when something is wrong and by how much. Those are fundamentally different things in production operations.

Sentry also does something logs cannot: it aggregates. When a bug hits one thousand users, you do not want one thousand separate Slack notifications. You want one alert that says “this error has occurred 1,000 times, affecting 37 distinct users, and the count is accelerating.” That aggregation is what lets you triage intelligently — you can see whether this is a cosmetic edge case affecting one free-tier user or a data-corrupting bug hitting your largest enterprise accounts.

After nineteen years of running Rails applications in production, I have never worked on a codebase that turned off error monitoring once it was in. I have worked on codebases that did not have it yet, and the war stories from those engagements are why I now treat error monitoring as infrastructure — something you set up on day one alongside the database and the deployment pipeline, not a plugin you bolt on when things start going wrong.

Installing sentry-ruby and sentry-rails in Rails 8

Sentry splits its Ruby SDK across focused gems. Add all of them in one go:

# Gemfile
gem "sentry-ruby"
gem "sentry-rails"

# Add these if you use them:
gem "sentry-sidekiq"   # Sidekiq job error capture

sentry-ruby is the core SDK — it handles the HTTP transport, event serialisation, scope management, and the Sentry.* API. sentry-rails hooks into the Rails notification system and adds automatic capture of request context, controller action names, breadcrumbs from ActiveSupport::Notifications, and exception handling middleware that catches anything before it renders a 500.

Create the initializer:

# config/initializers/sentry.rb
Sentry.init do |config|
  config.dsn = ENV["SENTRY_DSN"]
  config.breadcrumbs_logger = [:active_support_logger, :http_logger]
  config.environment = Rails.env
  config.enabled_environments = %w[staging production]
  config.release = ENV.fetch("CURRENT_REVISION", nil) || `git rev-parse --short HEAD 2>/dev/null`.strip
  config.send_default_pii = false
end

breadcrumbs_logger: [:active_support_logger] captures every ActiveSupport::Notification — every SQL query, every cache hit, every view render — as a breadcrumb on the error event. When you look at an exception in Sentry, you see the last twenty things the app did before it crashed, including the specific query that returned an unexpected result. http_logger captures outbound HTTP requests. Both are invaluable for debugging and I have never disabled either of them.

send_default_pii: false is the correct default everywhere. It strips cookies, session data, and request bodies from events before transmission. Your users’ passwords and tokens do not leave your process. If you need specific fields from the request body attached to an event, you add them explicitly as extra on that individual event.

release wires Sentry to your deployment pipeline. When fifty errors all start at 14:32 UTC and your latest release was abc123f, you know exactly which deploy introduced the bug. In Kamal 2 — which I covered in detail in the Rails 8 Kamal deployment guide — pass CURRENT_REVISION to the container at deploy time:

# config/deploy.yml (Kamal 2)
env:
  CURRENT_REVISION: <%= `git rev-parse --short HEAD`.strip %>

Setting User Context and Custom Tags

An error event without user context is a mystery. An error event with user_id: 4829, email: "enterprise@acme.com", plan: "enterprise" is a support ticket that can be resolved in five minutes.

Wire this into your base controller:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_sentry_context

  private

  def set_sentry_context
    return unless current_user

    Sentry.set_user(
      id:       current_user.id,
      email:    current_user.email,
      username: current_user.username
    )

    Sentry.set_tags(
      plan:       current_account&.plan,
      account_id: current_account&.id
    )
  end
end

Sentry.set_tags accepts a hash and attaches every key-value pair to all events generated within the current request scope. Tags are indexed and searchable in Sentry — you can filter your error feed by plan:enterprise to see which bugs affect your most important customers, or by account_id:123 when a specific account calls your support line and you want to see every exception they have triggered.

For background jobs there is no request scope and no current_user. Set the context explicitly at the start of the job:

class ProcessPaymentJob < ApplicationJob
  def perform(payment_id)
    payment = Payment.find(payment_id)

    Sentry.set_tags(account_id: payment.account_id, payment_id: payment.id)
    Sentry.set_user(id: payment.user_id)

    PaymentProcessor.new(payment).call
  end
end

If you are using Solid Queue — the Rails 8 default background job backendsentry-rails hooks into ActiveJob automatically and wraps each execution in a Sentry transaction. You get performance tracing for background jobs without additional configuration.

Add breadcrumbs inside long-running operations to produce a readable trail:

class MultiStepImportJob < ApplicationJob
  def perform(import_id)
    import = DataImport.find(import_id)
    add_crumb("Import loaded", data: { id: import.id, row_count: import.row_count })

    import.validate_source_schema!
    add_crumb("Schema validated")

    import.process_rows!
    add_crumb("Processing complete", data: { inserted: import.inserted_count })
  end

  private

  def add_crumb(message, data: {})
    Sentry.add_breadcrumb(
      Sentry::Breadcrumb.new(message: message, data: data, level: "info")
    )
  end
end

If this job raises on process_rows!, the breadcrumb trail in Sentry shows which validation passed, which step failed, and what the import state was at each point. Debugging becomes reading a structured log instead of guessing.

Performance Tracing: Finding Slow Requests Before Your Users Do

Rails Sentry ships a performance monitoring layer that instruments every request and job as a transaction, breaking it into spans: the SQL queries, view render time, external HTTP calls, Redis operations. Enable it with a sample rate:

Sentry.init do |config|
  # ...existing config...

  # Capture 5% of all transactions
  config.traces_sample_rate = 0.05
end

A fixed rate is fine for low-traffic applications. For production apps with diverse traffic patterns — health checks hitting /up every few seconds, high-volume webhook endpoints, rare admin flows — use a sampler:

Sentry.init do |config|
  # ...

  config.traces_sampler = lambda do |sampling_context|
    path = sampling_context.dig("rack_env", "PATH_INFO").to_s

    return 0    if path.match?(%r{\A/(up|health|ping)})  # never
    return 0.01 if path.start_with?("/api/v1/webhooks")  # high volume, low value
    return 1.0  if path.start_with?("/admin")            # always trace admin
    0.05  # 5% of everything else
  end
end

For instrumenting your own code — external API calls, slow business logic, third-party services — wrap the operation in a child span:

class VatCalculator
  def calculate(invoice)
    Sentry.with_child_span(op: "tax.calculate", description: "VAT lookup and computation") do |span|
      span.set_data("invoice_id", invoice.id)
      span.set_data("country_code", invoice.country_code)

      rates = TaxRateService.fetch(invoice.country_code)
      result = rates.apply(invoice.subtotal)

      span.set_data("effective_rate", rates.effective_rate)
      result
    end
  end
end

In the Sentry performance dashboard this span appears in the request’s transaction timeline. You can see at a glance if VAT calculation is consuming 800ms of a 900ms request, and drill into the data attributes to identify the country code causing the spike. This visibility used to require a dedicated APM product at many times the cost.

Filtering Noise: What Not to Report

A Sentry dashboard full of ActionController::RoutingError from bots probing /wp-admin is useless. The goal is an actionable error feed where every open issue represents a genuine bug in your code.

Use before_send to drop events before they leave the process:

Sentry.init do |config|
  # ...

  config.before_send = lambda do |event, hint|
    exception = hint[:exception]

    if exception
      return nil if exception.is_a?(ActiveRecord::RecordNotFound)
      return nil if exception.is_a?(ActionController::RoutingError)
      return nil if exception.is_a?(ActionController::UnknownFormat)
      return nil if exception.is_a?(ActionController::InvalidAuthenticityToken)
    end

    event
  end
end

A note on RecordNotFound: I drop it globally for web requests but keep it on API endpoints authenticated by API key. When an authenticated client consistently hits 404s, that is a bug in their integration and you should know about it. The filter can be conditional:

config.before_send = lambda do |event, hint|
  exception = hint[:exception]

  if exception.is_a?(ActiveRecord::RecordNotFound)
    # Keep API 404s (authenticated callers sending bad IDs is worth knowing)
    return event if event.request&.url.to_s.include?("/api/")
    return nil  # Drop web 404s (bookmarks, bots, typos)
  end

  event
end

Filter health check routes from the performance dashboard too — they dominate the transaction list and carry zero insight:

config.traces_before_send = lambda do |event, _hint|
  return nil if event.transaction&.match?(%r{\A/(up|health|ping)})
  event
end

Capturing Exceptions Manually with Context

The automatic exception capture handles anything that propagates unhandled to the Rack middleware. You need Sentry.capture_exception for exceptions you rescue explicitly but still want tracked — payment processor timeouts you handle gracefully, external API errors that trigger a retry, validation failures in an import job that you convert to a user-facing error message:

class ExternalFeedImporter
  def import(feed_url)
    response = Faraday.get(feed_url)
    parse_and_save(response.body)
  rescue Faraday::ConnectionFailed => e
    Sentry.capture_exception(e,
      level: "warning",
      extra: {
        feed_url: feed_url,
        attempts:  @retry_count
      },
      tags: { integration: "external_feed" }
    )
    schedule_retry
  rescue ParseError => e
    # Unrecoverable — let it propagate and auto-capture as error
    raise
  end
end

level: "warning" marks the event as expected-but-concerning rather than a full error. Warnings appear differently in the Sentry feed, which lets you triage the truly broken things from the reliably-handled exceptions you are tracking for frequency. Use level: "error" (the default) for genuine bugs. Use level: "warning" for things you handle gracefully but want to count. Use level: "info" for business events you want to observe — for example, tracking how often an expensive fallback code path is triggered.

Sentry vs Honeybadger vs AppSignal: An Honest Take

I have run all three in production on client codebases. Here is the honest take.

Honeybadger is the right choice for a solo developer or a small team that wants “alert me when things break” without a pricing conversation or a complex setup. The UI is simple, the Ruby integration is solid, and the free tier is genuinely usable in production. It lacks performance tracing, its error grouping algorithm is less sophisticated than Sentry’s, and it does not support polyglot stacks. For a two-person Rails shop: Honeybadger.

AppSignal is built exclusively for Ruby and Elixir, and it shows. The Rails integration understands ActiveRecord, Sidekiq, and Solid Queue in ways that a language-agnostic tool cannot replicate out of the box. Host monitoring — memory, CPU, disk — is bundled with application monitoring. The N+1 query detection is automatic and does not require Bullet or explicit instrumentation. The dashboards are beautiful and the support team is unusually responsive. I recommend AppSignal for Rails-only shops that want a single tool covering the application and the host without assembling four separate products. The pricing reflects the depth.

Sentry wins when your stack is polyglot — a Rails API, a React front-end, a Python data pipeline, a Node serverless function. Every component reports to one dashboard, errors from the front-end and the API can be correlated with a shared trace_id, and release tracking spans all your services simultaneously. The open-source self-hosted option is also real: if you have data residency requirements or want to own your monitoring infrastructure, you run Sentry on your own hardware. For teams already using Sentry for other stacks, the network effects of everything in one place are substantial.

My recommendation: Sentry for polyglot stacks or teams already on Sentry elsewhere. AppSignal for pure-Rails shops that want the tightest native integration. Honeybadger for very small teams that want something reliable without any tuning.

Connecting Sentry to the Broader Observability Picture

Error monitoring and observability are complementary tools, not alternatives to each other. I wrote about structured logging and the debugging workflow in the Rails logging post, and about setting up OpenTelemetry for distributed tracing in the Rails 8 production observability post. Sentry fills a different slot in that stack: it is user-facing and exception-centric, while OpenTelemetry with a backend like Grafana Tempo gives you infrastructure-level trace continuity across services.

The combination I run today on most client production systems: Rails Sentry for error aggregation and end-user performance monitoring, OpenTelemetry with self-hosted Tempo for distributed traces, and structured logging to CloudWatch or Loki for the raw stream. Each tool does what it does best. Sentry is the first alert and the first debug surface. The distributed trace is what you reach for when the error context alone is not sufficient.

FAQ

What is the difference between Sentry auto-capturing exceptions and calling Sentry.capture_exception manually?

sentry-rails automatically captures any exception that propagates to the Rack middleware level — unhandled errors in controllers, jobs without rescue blocks, anything that would otherwise render a 500 response or terminate a job. Sentry.capture_exception is for exceptions you rescue explicitly but still want tracked: payment processor timeouts you handle with a retry, external API errors you convert to user messages, import validation failures you write to the database. Auto-capture covers bugs you did not expect; manual capture covers situations you anticipated but want to observe and count.

How do I prevent Sentry from logging passwords or API keys?

config.send_default_pii = false removes cookies and sessions. For request bodies — where passwords travel in login forms — sentry-rails respects Rails’ filter_parameters list:

# config/application.rb
config.filter_parameters += [:password, :password_confirmation, :token, :secret, :api_key]

Any parameter name matching this list is replaced with [FILTERED] in Sentry events before they leave the process. Extend filter_parameters rather than writing custom scrubbing code in before_send — the Rails mechanism covers both your logs and Sentry in one place and is less likely to drift.

Can Sentry detect N+1 queries?

Not as a named category. Sentry’s performance tracing shows every SQL query run during a request as an individual span on the transaction timeline, which makes it obvious when a controller action fires 150 queries instead of 3 — you can see the repeated pattern in the span list. But it does not flag “this is an N+1” the way Bullet does during development. For proactive N+1 prevention, I pair Sentry with Rails strict loading in development; I covered that workflow in detail in the strict loading post. Sentry tells you the problem is happening in production; strict loading prevents it from shipping.

How does Rails Sentry work with Solid Queue background jobs?

sentry-rails hooks into ActiveJob, which Solid Queue uses as its interface. Each job execution becomes a Sentry transaction automatically. If the job raises, the exception is captured with the full breadcrumb trail, the job class name, the queue, and any serialised arguments that fit within the payload limit. The only thing you need to add manually is user and account context at the start of jobs that process user data — Sentry.set_user and Sentry.set_tags in the perform method, before the first line of business logic. Without that, exceptions from background jobs appear context-free and take much longer to tie to a specific user or account.

A production Rails application without error monitoring is a black box that only reports problems when a customer calls. After nineteen years of Rails production support, I have never seen the alternative work reliably. TTB Software sets up Rails Sentry, observability, and alerting as part of every production engagement — because the hour you spend on this setup is returned the first time an alert fires before a customer notices.

#rails-sentry #rails-error-monitoring #sentry-rails-8 #rails-sentry-setup-production #sentry-performance-tracing-rails #rails-application-monitoring

Related Articles

Last section. Then please call.

It's a phone call. That's the worst it can get.

No discovery deck. No 45-minute "qualification" call. 30 minutes, your problem, my opinion. If we're a fit, you'll know by minute 12.

Direct line — answered by Roger
+31 6 5123 6132
Mon–Fri, 09:00–18:00 CET · Currently available

OR
info@ttb.software