Exception tracking 🚫 with Bugsnag and Redwood
Tags: webdev, javascript, serverless
Published: 2020-09-11 14:35:40

So you’ve built your amazing redwood app, tested it thoroughly and are ready to go live. You, with your hipster beard and oat milk flat whites ☕️, you’re an awesome dev, so there’s no bugs in your code...... or are there?

Of course there are hotshot 🛑! It might be an edge case you hadn’t considered, maybe it’s something in your infrastructure, or that third party service that isn’t super reliable or maybe  just that users will do what you least expect and they hit an unhandled error!

For a production app, and for your app to delight users, you need to be able to diagnose issues quickly, and push up a fix as soon as you can. You need good logs to be able to see what the issue is, and you want your tooling to minimise this friction. In enterprise circles this is measured using  mean time to recovery - MTTR - in simple terms, you want to fix and ship as quickly as possible.

- [Introduction](#introduction)
- [Tldr;](#tldr)
- [Part 1: Frontend](#part-1-frontend)
    - [Setting up exception handler](#setting-up-exception-handler)
    - [Webpack setup & uploading source maps](#webpack-setup--uploading-source-maps)
    - [Validate your setup](#validate-your-setup)
    - [Dev & Prod configuration](#dev--prod-configuration)
- [Part 2: API and Graphql](#part-2-api-and-graphql)
    - [Create custom plugin for graphql](#create-custom-plugin-for-graphql)
    - [Custom functions](#custom-functions)
    - [Prod configuration](#prod-configuration)



# Introduction

This tutorial will walk through setting up Bugsnag, which is an exception tracking tool (primarily used by mobile devs) but also supports web and Node.js, with Redwood - both on the frontend and the api side. 

Since we first launched [**Tape.sh**](https://tape.sh?utm_source=bugsnag-redwood-article), a screen recording tool for mobile devs, we needed this visibility to fix very hard to reproduce issues. It even helped us contribute back to Redwood!

We really like Bugsnag, but you can follow the exact same process, and use a tool of your own choice - I don’t imagine it being too different.

# Tldr;

For the frontend, wrap the Redwood app with the bugsnag exception handler component and make sure you upload sourcemaps (with the webpack plugin or otherwise).

For the backend, create a custom apollo server plugin, and pass through exceptions to bugsnag. Make sure you have a method available to report errors to use in your custom functions too.

# Part 1: Frontend

First sign in to your bugsnag dashboard, and create your project.

New Project > Browser > React 

Grab your API key, and we'll use it in a little bit.

### Setting up exception handler

Let's add the Bugsnag library and react plugin

```bash
# -W because we'll use it in both web and api
yarn add -W @bugsnag/js 
yarn workspace web add @bugsnag/plugin-react
```

Now we have to wrap our entire frontend react app, with the exception handler.  In *web/src/index.js*

```diff
+ import Bugsnag from '@bugsnag/js'
+ import BugsnagPluginReact from '@bugsnag/plugin-react'

+ Bugsnag.start({
+  apiKey: process.env.BUGSNAG_NOTIFIER_API_KEY,
+  plugins: [new BugsnagPluginReact()],
+  releaseStage: process.env.CONTEXT || process.env.NODE_ENV,
+  appVersion: process.env.DEPLOY_ID,
+ })

+ const BugsnagBoundary = Bugsnag.getPlugin('react').createErrorBoundary(React)

ReactDOM.render(
- <FatalErrorBoundary page={FatalErrorPage}>
+ <BugsnagBoundary
+    FallbackComponent={<FatalErrorBoundary page={FatalErrorPage} />}
+  >
      <RedwoodProvider>
        <Routes />
      </RedwoodProvider>
+ </BugsnagBoundar>
- </FatalErrorBoundary>,
  document.getElementById('redwood-app')
)
```

The fallback component I'm using is the default Redwood FatalErrorBoundary, but you can use your own here.

Notice how we're using `process.env` variables. By default Redwood (rightly!) doesn't expose env variables to the frontend. So lets modify **redwood.toml** to include these variables

```diff
[web]
  port = 8910
  apiProxyPath = "/.netlify/functions"
+ includeEnvironmentVariables = ['BUGSNAG_NOTIFIER_API_KEY', 'CONTEXT', 'NODE_ENV', 'DEPLOY_ID']
[api]
  port = 8911
[browser]
```

### `REMEMBER!`
And finally, remember to add `BUGSNAG_NOTIFIER_API_KEY` to your .env file

Done ✨! Now you’ll get notified when your user’s hit an exception. But the logs you’ll see won’t be that helpful yet, because your javascript is minified. So far we know *what* is happening, now let’s setup the *why*

### Webpack setup & uploading source maps

We're going to use the Bugsnag's webpack plugins to set this up. Let's install them:

```bash
yarn workspace web add webpack-bugsnag-plugins
```

To customise your webpack config for Redwood, you need to create a file at `web/config/webpack.config.js`. If you already have it, great just add to it.

```jsx
/* ==== web/config/webpack.config.js ==== */

// Important, so webpack can use the environment variables
require('dotenv-defaults').config()

const {
  BugsnagSourceMapUploaderPlugin,
  BugsnagBuildReporterPlugin,
} = require('webpack-bugsnag-plugins')

module.exports = (config) => {
  // Check if its building in netlify
	// No need to upload source maps when building locally
  const netlifyBuild = !!process.env.NETLIFY

  const bugsnagPlugins = netlifyBuild
    ? [
        new BugsnagBuildReporterPlugin({
          apiKey: process.env.BUGSNAG_NOTIFIER_API_KEY,
          appVersion: process.env.DEPLOY_ID,
          releaseStage: process.env.CONTEXT || process.env.NODE_ENV,
          sourceControl: {
            provider: 'github',
            repository: process.env.REPOSITORY_URL,
            revision: process.env.COMMIT_REF,
          },
        }),
        new BugsnagSourceMapUploaderPlugin({
          apiKey: process.env.BUGSNAG_NOTIFIER_API_KEY,
          appVersion: process.env.DEPLOY_ID,
        }),
      ]
    : []

  config.plugins = [...config.plugins, ...bugsnagPlugins]

  return config
}
```

Notice that we're using the `process.env.NETLIFY` environment variable. This is so that we don't upload source maps for local builds. The environment variables `REPOSITORY_URL`, `COMMIT_REF`, `DEPLOY_ID` and `CONTEXT` come from Netlify, so modify according to where you're going to deploy your code. 

### Validate your setup

So let’s validate our setup now. Just add 

```jsx
throw new Error('Catch me bugsnag!')
```

anywhere in your frontend code, and when it get’s triggered you should see it come through on your dashboard (and email). You’ll be able to see *what* happened, *why* it happened and also, *how* it happened through the Breadcrumbs tab.

![Bugsnag UI preview](http://tapes.tape.sh/static/bugsnag-Screentape.gif)

> A note on sourcemaps with Netlify. If you have "optimise JS bundles" enabled on Netlify, they move your bundles to the Cloudfront CDN, and Bugsnag isn't able to match up the sourcemap to the js file.

If you switch off the setting in the Netlify UI, you'll be able to see the stacktrace fully. I haven't looked into making it work with Cloudfront yet, because unfortunately Netlify doesn't expose what they rename the files to and what the hostname is going to be, before uploading.

### Dev & Prod configuration

Great, now that you're seeing errors come through, you want to disable it for dev. So let's create `<EnvironmentAwareErrorBoundary>`

```jsx
const EnvironmentAwareErrorBoundary = React.memo(({ children, ...otherProps }) => {
  if (process.env.NODE_ENV === 'development') {
    return (
      <FatalErrorBoundary page={FatalErrorBoundary} {...otherProps}>
        {children}
      </FatalErrorBoundary>
    )
  } else {
    Bugsnag.start({
      apiKey: process.env.BUGSNAG_NOTIFIER_API_KEY,
      plugins: [new BugsnagPluginReact()],
      releaseStage: process.env.CONTEXT || process.env.NODE_ENV,
      appVersion: process.env.DEPLOY_ID,
    })

    const BugsnagBoundary = Bugsnag.getPlugin('react').createErrorBoundary(
      React
    )

    return (
      <BugsnagBoundary
        FallbackComponent={<FatalErrorBoundary page={FatalErrorPage} />}
        {...otherProps}
      >
        {children}
      </BugsnagBoundary>
    )
  }
})
```

What this does is use the default Redwood FatalErrorBoundary on development, but reports the exception to Bugsnag in production.

You can then wrap your app in this component like this:

```diff
ReactDOM.render(
+  <EnvironmentAwareErrorBoundary>
			{*/ your other stuff */}
      <RedwoodProvider>
        <Routes />
      </RedwoodProvider>
+  </EnvironmentAwareErrorBoundary>,
  document.getElementById('redwood-app')
)
```

---

# Part 2: API and Graphql

### Create custom plugin for graphql

For the backend, we want to capture errors from graphql. So let's start with create a util module to house the Bugsnag code. 

**api/src/lib/bugsnag.js**
```jsx
import Bugsnag from '@bugsnag/js'
import { isEmpty } from 'lodash'

Bugsnag.start({
  apiKey: process.env.BUGSNAG_SERVER_API_KEY,
  releaseStage: process.env.CONTEXT || process.env.NODE_ENV,
  appVersion: process.env.DEPLOY_ID,
})

export const reportErrorFromContext = (requestContext) => {
  const { errors, metrics, request, context } = requestContext

	// Call bugsnag here
  // But you could easily use something else here
  Bugsnag.notify(new Error(errors), function (event) {
    event.severity = 'error'
    event.addMetadata('metrics', metrics)
    event.addMetadata('errors', errors)
    event.addMetadata('query', request)
  })
}

export const reportError = (error) => {
  Bugsnag.notify(error)
}
```

We expose the `reportReportErrorFromContext` to use in our custom apollo server plugin, but leave the `reportError` for use elsewhere.

Now let's create the plugin and add it to our server setup

```diff
// === api/src/functions/graphql.js ===

+ import { reportErrorFromContext } from 'src/lib/bugsnag'


+ const bugSnagExceptionPlugin = {
+   requestDidStart() {
+     return {
+       didEncounterErrors(requestContext) {
+         reportErrorFromContext(requestContext)
+       },
+     }
+   },
+ }

export const handler = createGraphQLHandler({
  getCurrentUser,
+  plugins: [bugSnagExceptionPlugin],
  schema: makeMergedSchema({
    schemas,
    services: makeServices({ services }),

// ....rest of the file omitted for brevity
```

### Custom functions

Remember how we created the `reportError` method? You can now use this in your custom functions 

### Prod configuration

> Quick headsup here, Netlify’s functions annoyingly don’t set the NODE_ENV value at runtime by default. So if you wanted to check if you’re running on prod, you’ll have to make sure you set the value in the Netlify UI, but then in your netlify.toml set NODE_ENV to development for the build env (otherwise the build will fail). 

Same as the frontend, we want to disable logging exceptions during dev. So let's wrap the code in some if statements and we're done! In our case we're using the `process.env.LOG_EXCEPTIONS` variable.

**api/src/lib/bugsnag.js**
```diff
+ if (!isEmpty(process.env.LOG_EXCEPTIONS)) {
  Bugsnag.start({
    apiKey: process.env.BUGSNAG_SERVER_API_KEY,
    releaseStage: process.env.CONTEXT || process.env.NODE_ENV,
    appVersion: process.env.DEPLOY_ID,
  })
+ }

export const reportReportErrorFromContext = (requestContext) => {
  const { errors, metrics, request, context } = requestContext

  // Note that netlify doesn't set node_env at runtime in functions
+  if (isEmpty(process.env.LOG_EXCEPTIONS)) {
+    return
+  }
```

---

That's all folks! 🎉 You're now ready to launch your app, with the confidence that you can find, trace and fix exceptions if they happen!



#### 👋🏽 PS Here's what we're working on with Redwood:

[![Friction-free mobile & tvOS screen recording & sharing](https://dev-to-uploads.s3.amazonaws.com/i/0voxp8oo0bhtgv8iwln0.png)](https://tape.sh?utm_source=bottom-bugsnag-article)