Track Errors in Ruby on Rails Application with Sentry
Tags: rails, ruby, sentry, performance
Published: 2020-11-26 19:51:19

---
title: Track Errors in Ruby on Rails Application with Sentry
published: true
description: 
tags: #rails, #ruby, #sentry, #performance
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/9yplil833j9yx9q6icgu.png
---

Project Link: https://github.com/Joker666/sentry-rails

Error tracking is one of the most important parts of modern software development. When an app goes to production, it goes out from your development environment to users or customers for who various cases can arise that might not be handled in the application. And enter crashes, errors, unusual behaviors. Unless we track these, we do not have full visibility into the performance of the software.

Rails is great at recovering from crashes automatically. But the error remains buried in logs. There are a lot of error tracking software and SaaS businesses that can integrate with Rails. Most popular ones are [Rollbar](https://rollbar.com/), [Sentry](https://sentry.io/), [Bugsnag](https://www.bugsnag.com/) etc. All of them have free tiers that handle a generous amount of events and retain data for 1 month to 3 months. For heavy workload, it's better to opt for paid tiers. But Sentry is amazing because it has its core software open sourced that anyone can deploy and scale. Plus, it has a very nice UI. We are going to choose Sentry for this tutorial.

## Sentry
You can opt for the free tier through their website and continue with the next part of the tutorial, but we are open source enthusiastic here and we would do a local deployment of Sentry in docker-compose. Fortunately, Sentry provided a one-time setup with a shell script that get the software up and running in a matter of minutes.

The instruction for this is available [here](https://develop.sentry.dev/self-hosted/). Prerequisites
- Docker

Let's clone the repo and run shell script

```bash
$ git clone https://github.com/getsentry/onpremise
$ cd onpremise
$ ./install.sh
```
It will prompt for a user account and password in the midway. Please provide relevant details and continue. The script will pull all the images and build them properly. Next we run

```bash
docker-compose up -d
```

Aafter waiting for a bit if you head over to `localhost:9000`, you should see Sentry running and asking for some details

![First page](https://i.imgur.com/IHxjRRq.png)

Then, let's create a project

![Projects](https://i.imgur.com/7Wyw3Nj.png)

We see Sentry supports a variety of languages and frameworks. Let's choose `Rails`, there's one for `Ruby` but the `Rails` one would ease our life much better.

![DSN](https://i.imgur.com/oI6SoF2.png)

The next page shows us `getting started` guide for how to integrate into Rails project. We will go through that in the next section. For now, just copy the `DSN` to the clipboard since that would be needed for us to connect to the Sentry server from Rails.

## Ruby on Rails Setup
### Preparation
We will need a few tools installed in the system to get started with Rails development
- Ruby 2.7
- Rails 6.0
- Docker
With these installed, let's generate our API only project
```bash
rails new sentry-rails \
  --skip-action-mailbox \
  --skip-action-text \
  --skip-active-storage \
  --skip-javascript \
  --skip-spring -T \
  --skip-turbolinks \
  --skip-sprockets \
  --skip-test \
  --api
```
We are skipping most of the goodies needed for frontend development since we are going to write API only codes here.
Let's create a `.env` file at the root of the project and paste the `DSN` that we copied from the last section and a release number since with sentry we can track releases as well.
```bash
SENTRY_DSN=http://1d0405d68ff04d47bcc61b1e146dea6b@localhost:9000/2
RELEASE_NO=1.0.17
```
Replace it with the value you got from Sentry. Next, we install Sentry gem and `dotenv` gem to load these environment variables into Rails application
```
$ bundle add dotenv-rails
$ bundle add sentry-raven
```
Let's do one `bundle install` after that and we are set to start coding.

### Basic Setup
So, we will start with a very basic setup and that's as simple as, putting the `DSN` in config
```ruby
# config/application.rb
Bundler.require(*Rails.groups)
Dotenv::Railtie.load
Raven.configure do |config|
  config.dsn = ENV['SENTRY_DSN']
end
```
We make sure our environment variables are loaded and then we configure `Raven` which is all we need to do setup Sentry in Rails.

Let's configure routes to send requests
```ruby
# config/routes.rb
Rails.application.routes.draw do
    scope :api do
        get '/track/:id', action: :track, controller: 'track'
    end
end
```
We scope the routes with `api` and then write a get route. Let's write the controller now.
```ruby
# app/controllers/track_controller.rb
class TrackController < ApplicationController
    def track
        1 / 0
        render json: params[:id]
    end
end
```
This is obviously a runtime error. Let's run the app with `rails s` and hit the route with `localhost:3000/api/track/1` curl or Postman.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/xd4tvl6u1bcl8qqukfax.png)

When we click, we see

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/vzk0q5a79ah3ts3zqqdx.png)

This has pinpointed the error to the exact location of the error. This is good. We have setup error tracking with Sentry.
## Beyond The Basics
### Handle Errors Asynchronously
Next, we improve our basic setup. With this setup, whenever an error occurs, it sends to Sentry right away. Which is not optimal for production and we do not need it. It's better to send them asynchronously to not hurt our main application performance. We can leverage job runners like [Sidekiq](https://github.com/mperham/sidekiq), [Resque] (https://github.com/resque/resque) but for this setup, we would use `Active Job` that's baked in Rails itself. Add a new job in `jobs` folder.
```ruby
# app/jobs/sentry_job.rb
class SentryJob < ActiveJob::Base
    queue_as :sentry

    def perform(event)
        Raven.send_event(event)
    end
end
```
Then modify Raven config with
```ruby
# config/application.rb
Raven.configure do |config|
  config.dsn = ENV['SENTRY_DSN']
  config.async = lambda { |event| SentryJob.perform_later(event) }
end
```

Now if Sentry catches an error, it would send it through Active job rather than sending synchronously.

### Capture Exception Manually
While Sentry can catch errors automatically, there are handy methods to capture it ourselves. Let's explore that,
```ruby
# app/controller/track_controller.rb
class TrackController < ApplicationController
    def track
        id = params[:id].to_i
        case id
            when 1
                1 / 0
            when 2
                Raven.capture do
                    1 / 0
                end
            when 3
                begin
                    1 / 0
                rescue ZeroDivisionError => exception
                    Raven.capture_exception(exception)
                end
            end
        render json: params[:id]
    end
end
```

We can wrap our codes in `Raven.capture` block or do `begin..rescue` block and catch exception with `Raven.capture_exception`, both works the same way and sends error to Sentry server.

### Sending Logs
Sentry is not a traditional logger, we would explore logging in depth in another article, but it supports sending messages with extra fields if wanted. Let's change the controller action to see that
```ruby
# app/controller/track_controller.rb
when 4
    Raven.capture_message('Something went very wrong', {
        extra: { 'key' => 'value' },
        backtrace: caller
    })
end
```
This captures the message and a hash as extra. We can also send backtrace with it for additional context. But we should not put it everywhere as logs, this should serve a specific purpose.

### Additional Tracking Data
We are tracking a lot of data about the nature of the error and host machine by default in Sentry. But sometimes we want to add additional context so that it's easier for us to debug. Let's add a service class for this.
```ruby
# app/services/sentry_service.rb
class SentryService
    attr_accessor :user_id, :_request, :_params, :error, :payload

    def initialize(user_id, request, params, error, payload = {})
        @user_id = user_id
        @_request = request
        @_params = params
        @error = error
        @payload = payload
    end

    def register
        Raven.user_context(user_context)
        Raven.tags_context(tags_context)
        Raven.extra_context(extra_context)
    end

    private

    def user_context
        return {} if @user_id.blank?

        { id: @user_id }
    end

    def tags_context
        {
            component: 'api',
            action: @_params[:action],
            controller: @_params[:controller],
            environment: Rails.env,
            error_class: error.class
        }
    end

    def extra_context
        extra = {
            params: @_params.to_enum.to_h.with_indifferent_access,
            url: @_request.try(:url),
            uuid: @_request.try(:uuid),
            ip: @_request.try(:ip),
            fullpath: @_request.try(:fullpath),
            error_message: error.message
        }

        @payload.each do |k, v|
            extra[k] = v.try(:as_json).try(:with_indifferent_access)
        end

        extra
    end
end
```

A lot is happening here, so let's go through this one by one.
We are initializing `SentryService` with user info, request info, parameters, error and any payload if passed. Next there are three context methods `user_context`, `tags_context`, `extra_context`.

`user_context` returns `user_id`. `tags_context` returns some values like the action, the controller, the environment and error class. This would help us group errors into tags in sentry later for better debugging. `extra_context` returns some values extracted from `request` and `params`. Extra context values would be displayed on the error page.

We then register these contexts in `Raven` inside the `register` method. Now we have to use this class. Let's modify `ApplicationController` class so that all errors are handled through this service

```ruby
# app/controller/application_controller.rb
class ApplicationController < ActionController::API
    around_action :set_sentry_context

    protected

    # capture any unspecified internal errors
    def set_sentry_context
        begin
            yield
        rescue StandardError => e
            SentryService.new('12', request, params, e, context_variables).register
            raise e
        end
    end

    # override this in any other controllers to customize payload
    def context_variables
        {}
    end
end
```
All other controllers inherit from `ApplicationController`. So here we make a [around_action](https://guides.rubyonrails.org/action_controller_overview.html#after-filters-and-around-filters) method `set_sentry_context` which yields any controller method but captures the error thrown and wraps the error into `SentryService` with `register` call before rethrowing the error again which would then be caught by Sentry itself. We send dummy `user_id` but if you use a gem like `devise`, you should have the user's id in `current_user`. We also send [request](https://guides.rubyonrails.org/action_controller_overview.html#the-request-object) and [params](https://guides.rubyonrails.org/action_controller_overview.html#parameters)

Now let's test it. If we curl `localhost:3000/api/track/1` again, we should see

![More Data](https://dev-to-uploads.s3.amazonaws.com/i/tic5mru9i7sg842kiq3j.png)
![More Data](https://dev-to-uploads.s3.amazonaws.com/i/7hzrjxaqelhv76f7a6k6.png)

We see that more tags are visible in error dashboard which would let us filter by tags. Also, we have tracked additional data along with the User ID

### Track Release
It's very important to know from which version of the application the error is generating from. So Sentry has that configuration as well. Remember we kept our release number inside `.env`. Now update the config method like this

```ruby
# config/application.rb
Raven.configure do |config|
    config.release = ENV['RELEASE_NO']
    config.dsn = ENV['SENTRY_DSN']
    config.async = lambda { |event| SentryJob.perform_later(event) }
end
```
Now, in the error dashboard, we should see which error is coming from which version of the application.

## Conclusion
We have covered how we can track Rails errors in Sentry as well as how we can do a lot more with it. You can configure Sentry to send you alerts in various channels as well. Monitoring errors are a critical part of having a healthy application running. We would also explore how logging can help us find bugs in our application. Til then, keep coding!

## Resources
- https://docs.sentry.io/platforms/ruby/guides/rails/
- https://hixonrails.com/ruby-on-rails-tutorials/ruby-on-rails-sentry-logger-installation-and-configuration/
- https://blog.thinkdataworks.com/integrating-sentry-io-into-the-namara-platform