The missing R client for Sentry is finally here

João Santiago
Billie Engineering Crew
5 min readJun 2, 2020

Billie approves a request for financing on average within 5 minutes. Such speed can only be achieved with quick teams, sound infrastructure, and great models.

The data science team at Billie provides several models to different consumers of data inside the company. We are very hands-on, and go beyond just training models — we take care of our infrastructure, and implement REST APIs often as plumber packages.

For us, in production means we thoroughly documented and tested a piece of software — whether that is a new model or the infrastructure to serve it. Sometimes though, things go wrong in ways we couldn’t predict 🤷‍♀. In such cases, real-time alerts are critical to keeping our microservices ecosystem running, and our customers happy.

Our engineering teams at Billie are long time users of Sentry, a cross-platform application monitoring [service], with a focus on error reporting. Having Sentry means as soon as an error occurs, the team knows about it via e-mail and Slack.

Until recently, the data-science team had to rely on simple log alerts or reports from other teams, because there was no Sentry client for R. We were mostly reacting to our other engineering team’s error reports, then looking through our logs to find the problem. Our alerts were not aggregated, so we couldn’t know if a new issue was the same as something we saw yesterday. This process was slow and inefficient. So, we used Sentry’s API and wrote our own client.

Enter sentryR

You won't miss an error ever again.

We wanted sentryRto have good defaults and stay out of the way, but still be extensible to arbitrary situations. Waterproof and breathable. We achieved this by using lists and dot-dot-dot arguments. The user has full flexibility about what is sent to Sentry, starting with a basic capture(message = "This is fine"). Want to include a list of all the installed packages and their versions? Sure, just add it as a list named modules. Don’t want the name of your app? NULLify it with app_name = NULL. Knowing which fields you can add is as simple as checking the Sentry API documentation.

The defaults create a report with the commonly needed information:

  • the name and version of your app/API
  • the environment it is running on, so you don’t freak out with errors while QA is testing things
  • a stack trace of the error including the approximate location in your code where the error happened (we do this whenever possible, but R has its quirks)
  • query parameters/request body if present
  • versions of the kernel/os
  • the R version

When the same error happens more than once, Sentry has the built-in capability to aggregate all its instances. This way you can track how often a specific error happened. When, or if, a regression occurs, you’ll know.

Sentry aggregates errors when they happen more than once.

In production

In a production environment, you will likely want to pre-configure sentryR's settings like👇. In this example, we are adding some tags and skipping information about the runtime.

remotes::install_github("ozean12/sentryR")library(sentryR)configure_sentry(dsn = Sys.getenv("SENTRY_DSN"),
app_name = "elappo",
app_version = "8.8.8",
environment = Sys.getenv("APP_ENV"),
tags = list(foo = "tag1", bar = "tag2"),
runtime = NULL)
capture_message("This is not going to end well",
culprit = "API call to external provider was slow!",
level = "warning")

Even though you can use sentryR in pretty much any R script/application (as shown above), its potential is greatest when integrated into an API. This is especially true when that API is part of a larger microservices ecosystem, as in our case at Billie. To integrate it into a plumber API you have to configure sentryR with at least the DSN, and set the error handler:

library(plumber)library(sentryR)
configure_sentry(dsn = Sys.getenv('SENTRY_DSN'))api <- plumb("plumber.R")api$setErrorHandler(sentryR::sentry_error_handler)api$run(host = "0.0.0.0", port = 8000)

here we use the default plumber error handler, pre-wrapped with sentryR. This is the way to go if you want plumber 's default behavior. It is also possible to use your own error handler and then wrap it with wrap_error_handler_with_sentry.

Finally, in order to have access to the full error stack, you’ll have to wrap your Plumber endpoints with with_captured_calls

#* @get /errorapi_error <- with_captured_calls(function(res, req){stop("I'm a broken teapot")})

We had to write this function, because of how R's condition system is designed. Without it sentryR wouldn't capture a stack trace. Error handling in R is too long a topic for this blog post, but if you want to know more take a look here.

After this, your API is ready to go. That’s it! Every time you explicitly stop() your code, or it explodes somewhere, plumber will pass the error to the error handler. The error handler then responds with 500, or your chosen HTTP code, and sends a report to Sentry. You will get an alert through the channels you configured (we get an email and a slack message in a dedicated channel).

Making it nice

Errors sent to Sentry this way will invariably be of type simpleError. That’s base R for you. Using rlang you can make the errors more informative by creating custom conditions:

not_found <- function (message) {    rlang::abort("error_not_found", message = message, code = 404)}not_found(sprintf("Customer with UUID %s not found!", 
customer_uuid))

This way Sentry will display a more informative title, and you can quickly filter and search for the type of issue you are interested.

Conclusion

Using Sentry and sentryR lets us react quicker, and more precisely than before. Our APIs stay in top shape, and our customers get the fast service they’ve come to expect.

Get sentryR from CRAN, or the latest development release directly from github. If you have a problem write an issue or reach out on Twitter. We definitely welcome PRs if you have ideas for improvements. Let’s keep #rinprod awesome!

--

--