Sentry Error Tracking Part II: Integration with Rails, Webpacker source maps and release deploy

12th October 2019 – 1506 words

In the previous article I’ve written about installing Sentry on premise using Docker. In this post, I show some integration points, to improve the error reporting in our Rails+Webpacker settings. This is the same, if you are using Sentry.io SaaS or self-hosted.

Integration aspects in our case:

  • (self-hosted) Gitlab, important for commit/release tracking
  • Mattermost chat notifications
  • Release creation on deploy
  • “normal” git based deploy, having a GIT_REVISION file in our root project after every deployment that sentry can pick up to determine a “release”

Basic procedure / Overview

  1. Create a new project on your Sentry instance or Sentry.io, note the dsn
  2. Install Sentry plugins (Raven, Webpacker), Copy over the dsn in your secrets/credentials/production env
  3. Configure … Adjust, see below, this might take a few rounds to get the best result
  4. Alerting/notification, in our case: Link the Mattermost project channel with the Sentry project
  5. Create releases / Source Maps during deployment process, this needs an auth-key that you can generate on Sentry

Creating a project

Yeah, that’s simple, create a Sentry project, activate your alert and link to a chat channel, like Mattermost, Slack etc. Also make sure to link your Repository (Gitlab/Github), so you can attach commits to releases and therefore errors, and create a Gitlabhub Issue directly from Sentry.

I’ve written in the previous article on how to add Mattermost to our onprem Sentry instance.

Dependencies / Rails config

$ yarn add @sentry/browser
$ bundle add "sentry-raven"

Now, add a config/initializers/sentry.rb:

# config/initializers/sentry.rb
## TODO: Some mechanism to get the current git revision into the Rails app,
##       that depends on your deploy process.
##       e.g. ENV['CI_BUILD_REF'],
##            or `git rev-parse`.strip if you are deploying with the whole .git
SENTRY_RELEASE = if File.exist?("GIT_REVISION")
                   File.read("GIT_REVISION")
                 else
                   ""
                 end
Raven.configure do |config|
  if Rails.env.production?
    config.dsn = 'https://xxxxxxxxxxxxxxxxxxx:xxxxxxxxxxxxxx@sentry.xxxxxxxx.com/xx'
    # env: config.dsn = ENV['...']
    # secrets: config.dsn = Rails.application.secrets.sentry_dsn
  end
  # which envs to report, could be staging to
  config.environments = %w[production]
  config.release = SENTRY_RELEASE
  # Do this to send POST data, sentry DOES NOT send POST params by default
  config.processors -= [Raven::Processor::PostData]
  # Do this to send cookies by default, otherwise session/cookies info will not be send
  config.processors -= [Raven::Processor::Cookies]
  # What fields do you want to sanitize?
  config.sanitize_fields = ["first_name", "last_name", "email", "comment", "telephone", "phone", "password", "password_confirmation"]
  config.excluded_exceptions =
    Raven::Configuration::IGNORE_DEFAULT + [
      'ActiveRecord::RecordNotFound',
      'ActionController::RoutingError',
      'ActionController::InvalidAuthenticityToken',
      'CGI::Session::CookieStore::TamperedWithCookie',
      'ActionController::UnknownAction',
      'AbstractController::ActionNotFound',
      'Mongoid::Errors::DocumentNotFound'
    ].freeze
end

Integrating Javascript Tracking into Rails

We create:

  1. a new webpacker pack: app/javascripts/packs/error_tracking.js
  2. a new partial, e.g. app/views/layouts/_error_tracking.html.erb
  3. we include that partial in the head of all relevant layouts, e.g. “app/views/layouts/application.html.erb” etc.

1. packs/error_tracking.js

// app/javascript/packs/error_tracking.js
import * as Sentry from '@sentry/browser';

// same DSN like production, but without http passowrd, could also be injected like the SENTRY_RELEASE
Sentry.init({ dsn: 'https://xxxxxxxxxxx', release: window.SENTRY_RELEASE, environment: 'production' });
// to allow easy access from all areas append to window:
window.Sentry = Sentry

2. views/layouts/_error_tracking.html.erb

<!-- app/views/layouts/_error_tracking.html.slim -->
<% if Rails.env.production? %>
  <script type='text/javascript'>
  window.SENTRY_RELEASE = "<%= SENTRY_RELEASE %>"
  </script>
  <%= javascript_pack_tag 'error_tracking' %>
<% end %>

3. Include in all layouts

// app/views/layouts/application.html.slim
<head>
  ...
  <%= render 'layouts/error_tracking' %>

Deployment: Generate source maps

This will already work, but the Javascript will have no source maps and we only see the minified stacktraces in our application Javascript. Sentry provides a webpack plugin that can be included. Using this, will upload all source files just during the usual rake assets:precompile steps, without any adjustments in our build process, Great!

$ yarn add @sentry/webpack-plugin

Add to the config/webpack/production.js just before module.exports = environment.toWebpackConfig():

const SentryCliPlugin = require('@sentry/webpack-plugin');

const fs = require("fs");

// Again, getting the release here at this point, depends on your setup!
const release = fs.readFileSync("GIT_REVISION").toString();

environment.plugins.append('sentry',
    new SentryCliPlugin({
      release,
      // what folders to scan for sources
      include: ['app/javascript', 'public/assets'],
      // ignore
      ignore: ['node_modules', 'webpack.config.js', 'vendor'],
      // also set the last commit for the current release
      setCommits: {
        commit: release,
        // link that to your gitlab/github repository, to get the correct name
        //   head to Sentry -> Organisation settings -> Repos and take the name verbatim, no url!
        //   in our case, with self hosted Gitlab, it looked like this
        repo: 'developers / Myrepos'
      }
    })

Deployment: Add creds on build host

To make sure, the SentryCLIPlugin works, we need to add the credentials to the system that builds the assets, e.g. using environment variables, add:

# You must create a new Auth on your Sentry profile. Make sure to have R/W access on releases
SENTRY_AUTH_TOKEN='xxxxxxxxxxxxxxxxxxxxxxxx'
SENTRY_URL="https://sentry.yourcompany.com"
SENTRY_ORG="yourorg"
SENTRY_PROJECT="verbatim-project-name-slug"

Now, we could deploy with the Webpack Plugin. That plugin will upload the sources into a new release. Additionaly, Sentry recommends to tell it about, when you just “deployed” a new release, so you could create a sentry “deploy” at the end your deployment script, e.g.:

./node_modules/.bin/sentry-cli releases deploys `cat GIT_REVISION` new -e production

Improving errors: adding contexts

Your app probably has some kind of User object, I18n locales or other interesting context that you wanna include into the error tracking. Therefore, add a function set_raven_context into ApplicationController (or your API base controller), like

class ApplicationController < ActionController::Base
  before_action :set_raven_context
  # ...
  def set_raven_context
    Raven.tags_context(
      language: I18n.locale,
      # timezone?
    )
    if current_user
      # GDPR: you probably wouldn't send PII like name/email at this point
      Raven.user_context(id: current_user.id, company: current_user.company.name)
    else
      # masking ip? https://github.com/ankane/ip_anonymizer
      Raven.user_context(ip: request.ip)
    end
  end
end

Specific controllers could override the method and call super and then add custom context.

Ad-Hoc error reporting of catched exceptions

Sometimes you have situations, where you rescue an exception that you wanna report, but just want to continue with another program flow (e.g. Elasticsearch is down, falling back to naive SQL search, catching rare PDF conversion exceptions etc.).

When using Airbrake before, that was Airbrake.notify(exception, more: info, here: hash). With Sentry Raven, wrap the extra params in a Hash attribute called, …extra

  response = HTTP.get(url)
  ...
rescue StandardError => ex
  if Rails.env.production?
    Raven.capture_exception(ex, extra: { url: url })
  end
  false
end

Grape/Sinatra Raw Rack

Include the Rack module. I didn’t test this, yet:

+  require 'raven/integrations/rack'

class Api < Grape::API
+  use Raven::Rack

   ...

Happy error tracking!