Crystal error tracking with 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 canrequire
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, unlessConfiguration#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 to1.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.