Track Errors in Ruby on Rails Application with Sentry

joker666

Rafi

Posted on November 26, 2020

Track Errors in Ruby on Rails Application with Sentry

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, Sentry, Bugsnag 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. Prerequisites

  • Docker

Let's clone the repo and run shell script



$ git clone https://github.com/getsentry/onpremise
$ cd onpremise
$ ./install.sh


Enter fullscreen mode Exit fullscreen mode

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



docker-compose up -d


Enter fullscreen mode Exit fullscreen mode

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

First page

Then, let's create a project

Projects

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

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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



# config/application.rb
Bundler.require(*Rails.groups)
Dotenv::Railtie.load
Raven.configure do |config|
  config.dsn = ENV['SENTRY_DSN']
end


Enter fullscreen mode Exit fullscreen mode

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



# config/routes.rb
Rails.application.routes.draw do
    scope :api do
        get '/track/:id', action: :track, controller: 'track'
    end
end


Enter fullscreen mode Exit fullscreen mode

We scope the routes with api and then write a get route. Let's write the controller now.



# app/controllers/track_controller.rb
class TrackController < ApplicationController
    def track
        1 / 0
        render json: params[:id]
    end
end


Enter fullscreen mode Exit fullscreen mode

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

When we click, we see

Alt Text

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, Resque but for this setup, we would use Active Job that's baked in Rails itself. Add a new job in jobs folder.



# app/jobs/sentry_job.rb
class SentryJob < ActiveJob::Base
    queue_as :sentry

    def perform(event)
        Raven.send_event(event)
    end
end


Enter fullscreen mode Exit fullscreen mode

Then modify Raven config with



# config/application.rb
Raven.configure do |config|
  config.dsn = ENV['SENTRY_DSN']
  config.async = lambda { |event| SentryJob.perform_later(event) }
end


Enter fullscreen mode Exit fullscreen mode

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,



# 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


Enter fullscreen mode Exit fullscreen mode

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



# app/controller/track_controller.rb
when 4
    Raven.capture_message('Something went very wrong', {
        extra: { 'key' => 'value' },
        backtrace: caller
    })
end


Enter fullscreen mode Exit fullscreen mode

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.



# 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


Enter fullscreen mode Exit fullscreen mode

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



# 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


Enter fullscreen mode Exit fullscreen mode

All other controllers inherit from ApplicationController. So here we make a around_action 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 and params

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

More Data
More Data

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



# 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


Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
joker666
Rafi

Posted on November 26, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related