Crystal error tracking with Sentry

programming, crystal, sentry

Crystal… 🔮? Is it pure, you ask?

As pure as the driven snow! Crystal is a compiled, object-oriented programming language with static type-checking and syntax similar to Ruby.

It advertises itself as Fast as C, slick as Ruby, and you betcha it grows up to that slogan!

If you haven't heard about it already, I'd recommend checking it out now.

What's up with Sentry?

Having great programming language is all nice and dandy but even then bugs creep into the codebase now and again, creating uncertainty about the proper functioning of your app at all times.

Let me quote someone who knows a bit about the topic of uncertainty (since he actively takes part in creating it…).

In February 2002, Donald Rumsfeld, the then US Secretary of State for Defence, stated at a Defence Department briefing: There are known knowns; there are things we know we know. We also know there are known unknowns; that is to say we know there are some things we do not know. But there are also unknown unknowns—the ones we don't know we don't know.

We can't be too sure about the knowns in the world of programming. That's why it's so important to embrace failure, very often caused by both kinds of before-mentioned unknowns.

Ok, ok, get on with the theory and tell me finally what is this Sentry thing?!

Sentry is an open-source error tracking software that helps in keeping those pesky bugs at bay — in other words, turning the unknowns into knowns, so you can go and fix 'em, or just leave 'em for everyone to see.

There are other similar services, some with better feature-set, UI or what have you - I chose Sentry first and foremost because it's OSS - in addition of being feature-rich, actively developed, supporting many languages/platforms and having pretty slick/modern UI too.

Meet raven.cr - 🐛 hunting companion

raven.cr is a client and integration layer for the Sentry error reporting API. It's written as a shard for Crystal programming language and will be used as a basis for this tutorial.

I dig it, bring it on!

For starters, you need to add raven dependency to your application's shard.yml:

dependencies:
  raven:
    github: Sija/raven.cr

Now, it's the time to run your ol' trusty shards install command, which:

  • Fetches the shard from GitHub
  • Extracts it into lib directory, so you can require it in your code
  • Updates shard.lock with shard dependencies metadata

Once it's finished, you're ready to go!

Show me the meat

You start by require-ing newly installed shard:

require "raven"

DSN muß sein!

Raven will capture and send exceptions to the Sentry server only if its DSN is set (like most Sentry libraries it will honor the SENTRY_DSN environment variable). This makes environment-based configuration easy - if you don't want to send errors in a certain environment, just don't set the DSN in that environment! Setting the DSN is done in one of two ways:

  • Using SENTRY_DSN environment variable:

    export SENTRY_DSN=https://public@example.com/project-id
  • Configuring the client in the code (not recommended - keep your DSN secret!):

    Raven.configure do |config|
      config.dsn = "https://public@example.com/project-id"
    end

Test drive it

Raven supports two methods of capturing exceptions:

  • Capture any exceptions which happen during execution of the given block:

    Raven.capture do
      1 / 0
    end
  • Capture specific exception, e.g. from within rescue block:

    begin
      1 / 0
    rescue ex : DivisionByZeroError
      Raven.capture(ex)
    end

Beware & Begone, bugs

That's it! The exception in all its glory should be visible in Sentry UI:

As you can see, lots of context data - i.e. server name, release (from git), runtime/os context or list of packages (shards) - was extracted by raven without adding a single line of code!

Adding other data might require writing that one line, what a bummer… (; Let's introduce Contexts.

Contexts

TL;DR

A short example for starters:

# send user data with every captured exception
Raven.user_context({
  id: 1,
  email: "foo@bar.org",
  username: "Foobar",
})

# send user data just for *ex* exception
Raven.capture(ex, user: {
  id: 1,
  email: "foo@bar.org",
  username: "Foobar",
})

RTFM

I recommend an excellent write-up on the topic by the Sentry authors themselves. It's written for Ruby client, though most of it (except transactions and ruby-specific features like rack) applies as well to its Crystal counterpart.

Breadcrumbs

Sentry supports a concept called Breadcrumbs, which is a trail of events which happened prior to an issue. Often times these events are very similar to traditional logs, albeit with the ability to record additional structured data.

Recording crumbs

Manual breadcrumb recording is also available and easy to use. This way breadcrumbs can be added whenever something interesting happens.

Raven.breadcrumbs.record do |crumb|
  crumb.category = "foobar"
  crumb.level = :debug
  crumb.message = "Debugging foo..."
  crumb.data = {
    last_try:  Time.now - 5.days,
    in_docker: false,
  }
end

Passing splats or NamedTuple with property names works too:

 Raven.breadcrumbs.record({
   level:   :warning,
   message: "Foo of bar alike-ness",
   data:    {score: 0.73},
 })

Logger integration

This baked-in, creature comfort integration monkey-patches Logger class from Crystal stdlib, harvesting logs from all parts of your app.

require "raven/integrations/kernel/logger"

This one line is enough to automagically record crumbs for every logged message, of any Logger instance - except for the ones with their progname blacklisted in Configuration#exclude_loggers array.

Unless provided, Logger#progname will be used as Breadcrumb#category, and Logger#level will be mapped to Bradcrumb#level:

require "logger"

# setup logger object to your needs
logger = Logger.new(STDOUT)
  .tap(&.progname = "foobar")
  .tap(&.level = :debug)

# later in the code, trying to squash that pesky bug...
logger.debug "Debugging foo..."

# remember to inform yourself of the upcoming mathematical disaster
logger.warn "About to xplode!"

Make your code fail again

# try to do the impossible
Raven.capture { 1 / 0 }

Thanks to Breadcrumbs support your error page will feature such fancy list:

By default, 100 most recent breadcrumbs will be sent, along with other exception related data.

Tips & Gotchas

  • Raven.capture calls Sentry API in a synchronous manner, unless Configuration#async option is set. If you need to keep your main thread fast and responsive (e.g. web apps), turn it on in order to make use of default, spawn-based implementation - or hook up your own.
  • If your requests are failing because of a connection timeout error, try increasing the value of Configuration#connect_timeout option (defaults to 1.second).
  • raven supports Sentry 8 and up (with private and public DSNs).

In the upcoming episodes…

  • How to add plug-n-play Sentry support to your Kemal / Amber / Lucky web application via integrations.
  • How to collect user feedback upon hitting an error.

Further reading

For more detailed information checkout the api docs or GitHub repo.