Rodrigo Rosenfeld Rosas

Why proxying Bugsnag (or similar service) might be a good idea?

Thu, 01 Mar 2018 19:45:00 +0000

Bugsnag is a great error monitoring service that takes care of reporting and filtering/notifying exceptions in several kind of applications. I used to use my own error reporting tool in the app I currently maintain but as I'm currently evaluating creating a new application, I started to evaluate Bugsnag to save me some time. But I stumbled upon an issue I didn't have to deal with my custom error reporting tool.

When reporting errors, it's a good idea to attach as much meaningful data as they could be quite helpful when trying to understand some errors, specially when they aren't easily reproducible. Such data include user information which I'd prefer not to expose to the front-end, including the user id.

I was initially worried about exposing the API key to the front-end, which someone could use to report errors to my account, but then I figured out I was being too paranoid and that proxying the request wouldn't prevent users from reporting errors to my account, unless I'd implement some sort of rate limit protection or disabling errors reporting for non authenticated users (after all, I'd be able to track authenticated users acting that way and take some action against them).

However, hiding from the front-end user data meant to be used only internally is important to me. That's why I decided to take a few hours to proxy browsers errors through the back-end. Here's how it was implemented using the official bugsnag-js npm package and the bugsnag Ruby gem.

In the JavaScript code, there's something like showed below. I used XMLHttpRequest rather than fetch in order to support IE11 since the polyfills are lazy loaded as required in our application and fetch may not be available when Bugsnag is initialized in the client:

1import bugsnag from 'bugsnag-js';
2const bugsnagClient = bugsnag({
3 apiKey: '000000000000000000000000', // the actual api key will be inserted in the back-end
4 beforeSend: report => {
5 const original = report.toJSON(), event = {};
6 let v;
7 for (let k in original) if ((v = original[k]) !== undefined) event[k] = v;
8 report.ignore();
9
10 const csrf = (document.querySelector('meta[name=_csrf]') || {}).content;
11 const xhr = new XMLHttpRequest();
12 xhr.open('POST', '/errors/bugsnag-js/notify?_csrf=' + csrf);
13 xhr.setRequestHeader('Content-type', 'application/json');
14 xhr.send(JSON.stringify(event));
15 }
16});

The back-end is a Ruby application built on top of the Roda toolkit. It uses the multi_run plugin, splitting the main applications into multiple apps (which can be seen as powerful controllers if it helps understanding how it works). These are the relevant parts of the back-end:

lib/setup_bugsnag.rb:

1# frozen-string-literal: true
2
3require 'app_settings'
4require_relative '../app_root'
5
6if api_key = AppSettings.bugsnag_api_key
7 require 'bugsnag'
8
9 Bugsnag.configure do |config|
10 config.api_key = AppSettings.bugsnag_api_key
11 config.project_root = APP_ROOT
12 config.delivery_method = :synchronous
13 config.logger = AppSettings.loggers
14 end
15end

app/apps/errors_app.rb:

1# frozen-string-literal: true
2
3require 'json'
4require_relative 'base_app'
5require 'bugsnag_setup'
6
7module Apps
8 class ErrorsApp < BaseApp
9 private
10
11 def process(r)
12 super
13 r.post('bugsnag-js/notify'){ notify_bugsnag }
14 end
15
16 def notify_bugsnag
17 api_key = settings.bugsnag_api_key
18 head :ok unless api_key && settings.store_front_end_errors
19 event = JSON.parse request.body.read
20 user_data = auth_session.to_h
21 user_data['id'] = user_data['profile_id']
22 event['user'] = user_data
23 event['apiKey'] = api_key
24 event['appVersion'] = settings.app_version
25 payload = { apiKey: api_key, notifier: {
26 name: 'Bugsnag JavaScript', version: '4.3.0', url: 'https://github.com/bugsnag/bugsnag-js'
27 }, events: [event] }
28 configuration = Bugsnag.configuration
29 options = {
30 headers: {
31 'Bugsnag-Api-Key' => api_key,
32 'Bugsnag-Payload-Version' => event['payloadVersion'],
33 }
34 }
35 Bugsnag::Delivery[configuration.delivery_method].
36 deliver(configuration.endpoint, JSON.unparse(payload), configuration, options)
37
38 'OK' # optional response body, could be empty as well, we don't check the response
39 end
40 end
41end

That's it, some extra code, but it allows me to send useful information to Bugsnag while not requiring us to expose them to the front-end application. Hopefully next time I need something like that it will help to have it written down here ;)

Powered by Disqus