Exceptify

Gem Version Build Status

Exceptify sends exception reports from Rails and Rack applications to the channels your team already watches: email, chat, webhooks, and monitoring tools.

This repository is maintained by @lukin-io as lukin-io/exceptify with the exceptify gem name and Exceptify API. Version 1.0.0 starts the maintained Exceptify release line.

Quick Start

Add the gem to your Rails application's Gemfile:

gem "exceptify"

Install it and generate the initializer:

bundle install
bundle exec rails generate exceptify:install

Configure at least one notifier:

# config/initializers/exceptify.rb
require "exceptify/rails"
require "exceptify/rake"

Exceptify.configure do |config|
  config.add_notifier :email, {
    email_prefix: "[#{Rails.env.upcase}] ",
    sender_address: %("Exceptify" <notifier@example.com>),
    exception_recipients: %w[exceptions@example.com]
  }

  config.ignore_if do |_exception, _options|
    Rails.env.local?
  end
end

Email delivery uses your application's ActionMailer configuration. See ActionMailer configuration if emails do not send.

How To Test It

After configuring a notifier, trigger a real exception in a non-production environment.

# config/routes.rb
get "/exceptify_test", to: proc {
  raise "Exceptify test"
}

Start Rails, visit /exceptify_test, and confirm the notification arrives. Remove the route after testing.

Contents

What It Does

Exceptify installs a Rack middleware that watches for unhandled exceptions during web requests and sends a notification with request, session, environment, backtrace, and optional application data.

Use it when you want a small, self-hosted notification layer for exceptions without adopting a hosted error tracker.

It supports:

  • Rails integration through a generator and initializer.
  • Rack middleware configuration for Rails, Sinatra, and other Rack apps.
  • Built-in notifiers for email, Slack, Teams, Amazon SNS, Datadog, and webhooks.
  • Manual reporting for rescued exceptions, background jobs, scripts, rake tasks, and runners.
  • Ignore rules, crawler filtering, per-notifier filtering, and repeated-error grouping.

Compatibility

  • Exceptify 1.0.0 or newer.
  • Ruby 3.4.4 or newer.
  • Rails 8.0.2 or newer, below Rails 9.
  • Rack applications, including Sinatra.

The changelog starts at 1.0.0 for the maintained exceptify release line.

Installation

Add the gem to your application's Gemfile:

gem "exceptify"

Install it:

bundle install

To use this maintained repository directly from Git:

gem "exceptify", git: "https://github.com/lukin-io/exceptify.git"

For local gem development, point a test application at your checkout:

gem "exceptify", path: "../exceptify"

If the application should keep a Git source in its Gemfile while Bundler uses a local checkout, configure a local override from that application:

bundle config set local.exceptify ../exceptify
bundle install

Rails Setup

Generate the Rails initializer:

bundle exec rails generate exceptify:install

The generated file is written to config/initializers/exceptify.rb.

Keep the gem available to every environment that loads the initializer. If you want notifications only in production, keep the gem outside a production-only bundle group and add an ignore rule for local environments.

Initializer Notes

The generated initializer should load the Rails and Rake integrations, then register one or more notifiers. The email example in Quick Start is enough for a first setup; add other notifiers from Notifiers as needed.

Rails Runner Support

If you want rails runner commands to report exceptions, load the Rails integration from config/application.rb below Bundler.require:

# config/application.rb
require "exceptify/rails"

The initializer is too late for runner callbacks. You can still keep notifier configuration in config/initializers/exceptify.rb. The runner hook is guarded, so loading the integration more than once does not install duplicate at_exit callbacks.

Common Workflows

Send to Email and Slack

Slack notifications require the slack-notifier gem:

gem "slack-notifier"

Then register both notifiers:

Exceptify.configure do |config|
  config.add_notifier :email, {
    email_prefix: "[#{Rails.env.upcase}] ",
    sender_address: %("Exceptify" <notifier@example.com>),
    exception_recipients: %w[exceptions@example.com]
  }

  if (webhook_url = Rails.application.credentials.dig(:slack, :exceptions_webhook_url))
    config.add_notifier :slack, {
      webhook_url: webhook_url,
      channel: "#exceptions",
      additional_parameters: {
        mrkdwn: true
      }
    }
  end
end

Attach Request Context

Store application data in the request environment before an exception is raised:

class ApplicationController < ActionController::Base
  before_action :prepare_exceptify_notification

  private

  def prepare_exceptify_notification
    request.env["exceptify.exception_data"] = {
      current_user_id: current_user&.id,
      account_id: &.id,
      request_id: request.request_id
    }
  end
end

That data is included in supported notifier payloads and email sections.

Report a Handled Controller Error

Middleware only sees exceptions that continue up the Rack stack. If a controller rescues an error, notify manually:

class OrdersController < ApplicationController
  rescue_from PaymentGateway::Timeout, with: :payment_gateway_timeout

  private

  def payment_gateway_timeout(exception)
    Exceptify.notify_exception(
      exception,
      env: request.env,
      data: {
        order_id: params[:id],
        request_id: request.request_id
      }
    )

    render json: {error: "payment_gateway_timeout"}, status: :bad_gateway
  end
end

Report from Plain Ruby Code

begin
  ImportCustomers.call(file_path)
rescue => exception
  Exceptify.notify_exception(
    exception,
    data: {
      job: "ImportCustomers",
      file_path: file_path
    }
  )

  raise
end

Filter Sensitive Parameters

Exception emails can include request parameters. Use Rails parameter filtering for secrets:

# config/application.rb
config.filter_parameters += [
  :password,
  :password_confirmation,
  :credit_card_number,
  :secret_details
]

Notifiers

Built-in notifier docs and support status:

Notifier Status Docs
Email Supported Email
Slack Supported Slack
Teams Supported Teams
Amazon SNS Supported Amazon SNS
Datadog Supported Datadog
WebHook Supported WebHook
Custom notifiers Supported extension point Custom notifiers

Notifier Setup Rules

Network notifiers validate required options during setup. Missing webhook_url, url, or AWS credentials raise ArgumentError instead of silently disabling notifications.

For tests, custom transports, or apps that already wrap provider clients, pass an injected client:

Exceptify.configure do |config|
  config.add_notifier :webhook, {
    url: "https://example.com/exception-webhook",
    http_client: MyHTTPClient
  }

  config.add_notifier :sns, {
    topic_arn: "arn:aws:sns:us-east-1:123456789012:exceptions",
    client: Aws::SNS::Client.new(region: "us-east-1")
  }
end

Slack accepts notifier: for a prebuilt Slack client. Use fail_silently: true only as a temporary compatibility option while migrating old silent configurations.

You can also register any object that responds to #call(exception, options):

Exceptify.configure do |config|
  config.add_notifier :logger, lambda { |exception, options|
    Rails.logger.error(
      "[exceptify] #{exception.class}: #{exception.message} #{options[:data].inspect}"
    )
  }
end

Noise Control

Ignore Known Exceptions

Exceptify.configure do |config|
  config.ignored_exceptions += %w[
    ActionView::TemplateError
    MyApp::ExpectedError
  ]
end

The default ignored exceptions include common routing, record-not-found, and invalid-parameter errors.

Ignore Crawlers

Exceptify.configure do |config|
  config.ignore_crawlers %w[Googlebot bingbot]
end

Ignore by Condition

Exceptify.configure do |config|
  config.ignore_if do |exception, options|
    path = options.dig(:env, "PATH_INFO")

    Rails.env.local? ||
      path == "/health" ||
      exception.message.match?(/Couldn't find Page with ID=/)
  end
end

Ignore Only One Notifier

Use per-notifier filtering when email should still send but chat should stay quiet, or the reverse:

Exceptify.configure do |config|
  config.ignore_notifier_if(:slack) do |exception, _options|
    exception.is_a?(ActionController::RoutingError)
  end
end

Group Repeated Errors

Error grouping prevents notification floods for the same exception. With the default trigger, notifications are sent at counts 1, 2, 4, 8, 16, and so on.

Exceptify.configure do |config|
  config.error_grouping = true
  config.error_grouping_cache = Rails.cache
  config.error_grouping_period = 5.minutes

  config.notification_trigger = lambda { |_exception, count|
    count == 1 || (count % 10).zero?
  }
end

Production Checklist

Before relying on exception notifications in production:

  • Configure at least one notifier.
  • Verify delivery with a real test exception in staging.
  • Confirm ActionMailer delivery settings if using email.
  • Filter secrets with config.filter_parameters.
  • Ignore local and test environments.
  • Add crawler or health-check ignores if they create noise.
  • Enable error grouping for high-traffic applications.
  • Make sure background jobs are covered separately from web requests.

Background Jobs

The Rack middleware only catches exceptions during web requests. For jobs and command-line work, use one of the integrations below.

Rake Tasks

The generated initializer includes:

require "exceptify/rake"

That reports unhandled exceptions from rake tasks.

Rails Runner

For rails runner, require the Rails integration from config/application.rb as shown in Rails Runner Support.

Sidekiq, Resque, and Solid Queue

Generate an initializer with the integration you need:

bundle exec rails generate exceptify:install --sidekiq

or:

bundle exec rails generate exceptify:install --resque

or:

bundle exec rails generate exceptify:install --solid-queue

The Solid Queue integration hooks into Active Job failure notifications and reports unhandled exceptions for jobs using the solid_queue adapter. It preserves Active Job failure behavior, so failed jobs still end up in Solid Queue's failed executions table and retry_on or discard_on handlers still run normally.

Manual Job Reporting

If a job system is not integrated, report exceptions directly:

class RebuildSearchIndexJob
  def perform()
    RebuildSearchIndex.call()
  rescue => exception
    Exceptify.notify_exception(
      exception,
      data: {
        job: self.class.name,
        account_id: 
      }
    )

    raise
  end
end

Rack and Sinatra

Use the Rack middleware directly when you are not using the Rails generator, or when you need Rack-only options such as ignore_cascade_pass.

require "exceptify"

use Exceptify::Rack,
  email: {
    email_prefix: "[RACK ERROR] ",
    sender_address: %("Exceptify" <notifier@example.com>),
    exception_recipients: %w[exceptions@example.com]
  },
  ignore_exceptions: ["Sinatra::NotFound"],
  ignore_cascade_pass: true

Options passed directly to Exceptify::Rack are local to that middleware instance. To use one application-wide configuration, configure Exceptify once and mount the middleware without notifier options:

require "exceptify"

Exceptify.configure do |config|
  config.add_notifier :email, {
    sender_address: %("Exceptify" <notifier@example.com>),
    exception_recipients: %w[exceptions@example.com]
  }
end

use Exceptify::Rack

Sinatra users can also review the example application.

Maintenance

This repository is the current home for the exceptify gem. Maintenance focuses on modern Ruby and Rails support, clear documentation, and practical bug fixes around the Exceptify API.

Issues and pull requests are welcome when they include enough context to reproduce the behavior. The current refactoring direction is tracked in REFACTORING.md.

Development

Install dependencies:

bundle install

Run tests:

bundle exec rake test

Open a console with the gem loaded:

bundle exec rake console

Build the gem locally:

bundle exec rake build

Pull requests and issues are welcome. Please read the Contributing Guide and follow the Code of Conduct.

License

Released under the MIT license.

Maintainer: @lukin-io