One of the revelations of adopting Redux for state management in a frontend app is the ability to follow state changes precisely through Redux dev tools (available as a browser extension). Debugging is an order of magnitude easier when app state is explicit, unified, and each update can be traced to a specific action. This is great for development environments, but what about error monitoring in production?

Until recently, we had been using Opbeat to monitor the Lingo web app frontend built with React & Redux. Their setup was pretty nice, almost what we needed: nice stacktrace, list of Redux action types fired before the error, and (a small) part of the Redux state at the time of the error. After Opbeat was acquired by Elastic, they shifted focus and never upgraded their monitoring library to support React 16. As a result, we migrated to Sentry, which we have been using to monitor our other product, Noun Project.

I decided to dig into how we could replicate the things we liked about Opbeat, and discovered we could really improve on it, and even get close to the dream of remote dev tools in production. Here are the highlights of our current setup:

Significant Redux action types are logged as “breadcrumbs”

Full action object logged for last action prior to error

Redux store state at time of error is attched to issue in “Additional data”

Users can provide feedback any time a React component is unable to render due to an error, and this feedback is attached to the issue

We’re not doing anything really novel here — most of it is covered in Sentry’s React documentation—but seeing it all together really shows the power of modern JavaScript monitoring and some of the big payoffs of using Redux to manage app state. Here’s how it works.

Redux middleware

Sentry’s breadcrumb feature allows logging the events that lead up to an error. By default, Sentry tracks native DOM events and XHR requests, but also allows tracking custom app-specific events. This is a great fit for Redux’s action abstraction, where a simple JavaScript object with a type key must be created to make any change to the app’s local state. Each action runs through the middleware chain, so by adding some custom middleware you can easily log and analyze every state change.

Breadcrumbs for an error as shown in Sentry

Here we’re just logging the action type, but it is also possible to send additional keys (such as an entity ID value from the action). For the most recent action, the entire object is sent with “Additional data”:

Full Redux action processed before the error logged under “Additional data” in Sentry issue

So now we know (more or less) what the user is doing to change the app state, but it would be very handy to have the entire state of the app at the time the error occurred. As it happens, Redux also enforces the idea of a single source of truth for app state, which is just a plain JavaScript object that can easily be converted to JSON and sent to Sentry.

Redux state object logged under “Additional data” in Sentry issue

One significant limitation is that the entire payload for the Sentry issue has to be less than 100k (as of this writing). When you’re storing the entire app state in a single object, that object can quickly grow larger than 100k. Below I’m going to describe what we do to send the store while keeping the payload size manageable. I should note, however, that if you exceed 100k in the payload, Sentry will silently fail to record the error. So depending on your situation, going down this path may be too risky.

We follow the pattern of storing domain entity objects (spaces, kits, assets, users, invitations, etc) under an “entities” key in the Redux store. Each type of entity has its own object in the state that allows accessing entities by primary key (we use the normalizr library to help normalize the entities from API responses).

In order to keep the Sentry issue payload small enough, and to protect user privacy, we just replace each object under state.entities with an array of the primary keys, so that the entities themselves are not logged.

Although sending actions and state to Sentry would be fairly straightforward to implement with a custom Redux middleware, we decided to use Raven for Redux middleware, which fits our use case, is well tested, and has a responsive maintainer (I contributed a small feature to get past one stumbling block for us, and it was merged within a day).

Our middleware configuration looks something like this ( flattenEntities() just converts an object of entities to an array of primary keys):

User feedback

Sentry also has a great new feature called user feedback — when an error occurs, give your users a chance to let you know what their experience was. Once the feedback is submitted, it will be attached to the Sentry issue for the specific error. Stacktrace, events, app state, browser details AND user feedback all in one place, oh my!

This feature is a great fit for React’s new error boundaries feature (introduced in React 16), which is basically try/catch for React components. Just create a simple component with your generic error state, and React will use it to replace other components whenever they throw an error anywhere in their lifecycle methods (e.g. render , componentDidUpdate , etc).

Within our error component, we just include a link with an onClick handler that calls Raven.showReportDialog() :

When a component lifecycle method throws an otherwise unhandled error, instead of the app crashing, the broken component is replaced:

Error boundary replaces a broken component within a modal

The “let us know” link will open a new modal that allows the user to send feedback to Sentry, and attach the feedback to the issue created for the most recently thrown error.

Front end error monitoring has advanced dramatically in the last couple of years. These examples showcase some of Sentry’s good design decisions, but also make a strong case for the power of Redux’s core principles.