Track Errors in Ruby on Rails Application with Sentry
Rafi
Posted on November 26, 2020
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
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
Aafter waiting for a bit if you head over to localhost:9000
, you should see Sentry running and asking for some details
Then, let's create a project
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.
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
# 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
# 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.
# 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.
When we click, we see
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
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
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
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
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
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
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
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
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
Posted on November 26, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.