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:
1 | import bugsnag from 'bugsnag-js'; |
2 | const 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 | |
3 | require 'app_settings' |
4 | require_relative '../app_root' |
5 | |
6 | if 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 |
15 | end |
app/apps/errors_app.rb:
1 | # frozen-string-literal: true |
2 | |
3 | require 'json' |
4 | require_relative 'base_app' |
5 | require 'bugsnag_setup' |
6 | |
7 | module 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 |
41 | end |
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 ;)