How you can use FullStory and Sentry to understand React and Redux errors with an example “Search Hacker News” app.

All the things that can go wrong will go wrong.

Any software application that does a meaningful amount of work is inherently complex. And complex systems fail.

Constantly.

The best systems work despite this axiom through consistent application of defensive measures paired with unyielding oversight by intrepid support and engineering staff. Automated error monitoring, alerting, and reporting tools are required for any software application to defend against failure.

Error monitoring tools generally pinpoint specific lines of code that emit exceptions and roll multiple errors into aggregate statistics: total number of errors/day, error origins broken out by frequency of errors, etc. This information is necessary to zero in on single points of failure but may miss upstream user actions that contributed to the fail-state in the first place.

In complex systems, no one event is the cause of catastrophic failure.

When errors happen, error monitoring tools help us understand the details of the error when it occurred. This information is crucial when planning a fix.

Sentry is among the most widely adopted error management platforms on the market, so it’s a good choice to highlight how FullStory integration with error monitoring tools can reduce the time spent understanding bugs and provide better overall remediation.

FullStory helps development teams see the larger context around the errors that land in their issue queues. Viewing user activity prior to catastrophe identifies all of the states that lead up to the failed one and shines light on the complex sequence of events that tripped up your complex system. Simply being able to see the unexpected ways users work with your application is enlightening on its own. Details on how FullStory works can be found here.

React and Redux are popular development libraries that are often paired together in complex JavaScript applications. We’ll explore error management in the React + Redux stack with an eye towards intelligent error collection and review with FullStory + Sentry.

Handling Errors in React + Redux With "Search Hacker News"

We’ve created a sample application called “Search Hacker News” that demonstrates how FullStory and Sentry work together. You can find details about Search Hacker News as well as instructions on setting up your own FullStory and Sentry accounts in the Readme on GitHub. All of the code samples in this article are available on GitHub, as well.

A sample successful search return on Search Hacker News.

Search Hacker News is riddled with bugs. For example, you can search for mobile apps and it will return results. However, other, specific searches will intentionally break it. Search Hacker News is designed to explore how Redux + React apps fail and strategies for handling failure. We’re going to look at four scenarios:

  1. Handling errors in React components
  2. Handling errors in Redux action creators
  3. Catching unhandled errors in Redux action creators and reducers
  4. Uncaught error notifications

We'll explain how these scenarios work below—and then walk through an example of how you can use your own installation of Sentry and FullStory to monitor and handle React + Redux errors like Search Hacker News does.


1. Handling Errors in React Components

React 16 introduced Error Boundaries to handle exceptions thrown while rendering components. Error Boundaries will capture errors thrown from any component nested within them.

All child components of the App component are wrapped in an Error Boundary, which means errors in any component will be handled.


import React from 'react';
import './App.css';
import SearchStories from './SearchStories';
import Stories from './Stories';
import ErrorToast from './ErrorToast';
import ErrorBoundary from './ErrorBoundry';

const App = () => (
  <ErrorBoundary>
    <div className="app">
      <ErrorToast></ErrorToast>
      <div className="interactions">
        <SearchStories />
      </div>
      <Stories />
    </div>
  </ErrorBoundary>
);

export default App;

This is our ErrorBoundry component definition:

import React, { Component } from 'react';
import recordError from '../api/error';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { error: null, eventId: null };
  }

  componentDidCatch(error, errorInfo) {
    this.setState({ error });
    recordError(error, errorInfo);
  }

  render() {
    if (this.state.error) {
      //render fallback UI
      return (
        <div className="error">
          <h1>Something bad happened and we've been notified</h1>
          <p>In the mean time, search for <a href="/?query=happiness">happiness</a></p>
        </div>
      );
    } else {
      //when there's not an error, render children untouched
      return this.props.children;
    }
  }
}

export default ErrorBoundary;

recordError is invoked in componentDidCatch, which sends the error event to Sentry and FullStory for alerts and analysis.

We’ll review how Sentry and FullStory link together a little later.

2. Handling Errors in Redux Action Creators

Action creator functions are another likely source of errors if you're performing side-effects and dispatching other actions. The integration with the Hacker News API occurs in the story action creator.

import { STORIES_ADD } from '../constants/actionTypes';
import { doBeginLoad, doEndLoad } from './loader';
import { doError } from './error';
import fetchStories from '../api/storys';

const doAddStories = stories => ({
  type: STORIES_ADD,
  stories,
});

const doFetchStories = query => async dispatch => {
  dispatch(doBeginLoad());
  try {
    if (query === 'break it') throw new Error('Broken on demand!');
    const response = await fetchStories(query);
    dispatch(doAddStories(response.hits));
  } catch (err) {
    dispatch(doError(err));
  }
  dispatch(doEndLoad());  
};

export {
  doAddStories,
  doFetchStories,
};

Whereas we called recordError directly from the ErrorBoundry, in the action creator we’re delegating error recording by dispatching the doError action.

import { ERROR, CLEAR_ERROR } from '../constants/actionTypes';
import recordError from '../api/error';

const doError = (error) => {
  recordError(error);
  return { type: ERROR,
    error,
  }
};

const doClearError = () => ({ type: CLEAR_ERROR });

export {
  doError,
  doClearError
};

You probably noticed the small hand grenade embedded in the story action creator code (search Hacker News for “break it”). There are tiny explosives embedded throughout the Search Hacker News app.

3. Catching Unhandled Errors in Redux Action Creators and Reducers

What if an action creator or reducer forgets to handle errors appropriately? Redux Middleware can help. The Search Headline News app includes a crashReporter middleware that will catch unhandled exceptions thrown from thunk action creators (action creators like src/actions/story.js that return a function) and any reducer.

import { doError } from '../actions/error';

const crashReporter = store => next => action => {
  // we got a thunk, prep it to be handled by redux-thunk middleware
  if (typeof action === 'function') {
    // wrap it in a function to try/catch the downstream invocation
    const wrapAction = fn => (dispatch, getState, extraArgument) => {
      try {
        fn(dispatch, getState, extraArgument);
      } catch (e) {
        dispatch(doError(e));
      }
    }
    // send wrapped function to the next middleware
    // this should be upstrem from redux-thunk middleware
    return next(wrapAction(action));
  }
  
  try {
    return next(action);
  } catch (e) {
      store.dispatch(doError(e));
  }
};

export default crashReporter;

When you click the "Archive" button in Search Hacker News, a thunk action creator is dispatched and an unhandled exception is thrown, to be caught and handled by the crashReporter middleware.

This middleware will capture any uncaught reducer errors as well as any action creator error thrown from a thunk. Uncaught exceptions thrown from plain action creators will not be caught by this middleware.

4. Uncaught Error Notifications

Ideally, all exceptions are caught and handled appropriately to provide proper user feedback. Using crashReporter will help in case a try/catch statement was left out in certain situations, but there are types of unhandled exceptions that middleware can't catch.

These include unhandled exceptions thrown from:

  • action creators that aren't thunks
  • event handlers in React components (onClick, onSubmit, etc.)
  • setTimeout or setInterval

There’s no way to report back to users that something went wrong when unhandled exceptions occur in these scenarios, but because Sentry shims the global onerror and onunhandledrejection event handlers, you will receive an error alert with a FullStory session replay URL as well as a FullStory custom event whenever an uncaught JavaScript runtime error occurs. All of this is taken care of in the initSentry function in the error API module.

import * as Sentry from '@sentry/browser';
import * as FullStory from './fullstory';

let didInit = false;
const initSentry = (sentryKey, sentryProject) => {
  if (didInit) {
    console.warn('initSentry has already been called once. Additional invocations are ignored.');
    return;
  }
  Sentry.init({
    dsn: `https://${sentryKey}@sentry.io/${sentryProject}`,
    beforeSend(event, hint) {
      const error = hint.originalException;
      event.extra = event.extra || {};
      event.extra.fullstory = FullStory.getCurrentSessionURL(true) || 'current session URL API not ready';

      FullStory.event('Application Error', {
        name: error.name,
        message: error.message,
        fileName: error.fileName,
        lineNumber: error.lineNumber,
        stack: error.stack,
        sentryEventId: hint.event_id,
      });
      
      return event;
    }
  });
  didInit = true;
};

const recordError = (error) => {
  if (!didInit) throw Error('You must call initSentry once before calling recordError');
  Sentry.captureException(error);
};

export default recordError;
export { initSentry };

Debugging Search Hacker News Errors in FullStory

Now we’ll dive into how one of these four scenarios plays out by breaking Search Hacker News.

Let’s get started.

No Offense Fellow Floridians

If you search for “Florida,” an error is thrown from the SearchStories component (a poke at my home state). Sentry captures the stack trace and highlights the line of code that threw the error.

Sentry Event.

A FullStory session replay URL is included in the Sentry issue that deep links to the moment just before the error occurred.

FullStory Session Replay URL.

Clicking on this link lets you see the user’s actions leading up to and following the error in a FullStory session replay. In this example, we see our user type the unsearchable term ("Florida") into the search box and submit before they see the Error Boundary screen. An "Application Error" custom event is visible in the event stream on the right-hand side of the screen.

Searching for Florida.

Now that you can see the error in motion, you have all the context you need to take action.

Even More Search Hacker News Fail to Explore

If you like breaking things, then Search Hacker News is the app for you. Here’s a review:

  • Search for “break it” to trigger an error handled in an action creator
  • Click the “Archive” button to trigger an error handled by middleware
  • Search for “Florida” to trigger a component render error

Each error will immediately create a Sentry issue with a FullStory session replay URL for you to review.

Finding All Users Who Experienced Errors

Imagine if your team wanted to scope out the impact of an error. How might you go about doing that?

FullStory’s custom events API is used in src/api/error.js to create error events in the FullStory app that you can use to find users that had problems on your site and review their sessions.

// send error data into FullStory to find any user who experienced errors
FullStory.event('Application Error', {
  name: error.name,
  message: error.message,
  fileName: error.fileName,
  lineNumber: error.lineNumber,
  stack: error.stack,
  sentryEventId: hint.event_id,
});

You can search for all users who triggered an Application Error in FullStory and filter further by error names, messages, and source file names.

Searching for users who had Application Errors

Collating errors by impacted user groups is a fantastic way for teams to quickly understand the impact of individual errors on their entire user base. FullStory's search interface gives both developer and non-developer team members an easy way to explore which errors are hitting users and how many are being affected.

Monitor and Alert. Watch and Fix!

Complex systems never behave as expected, and all of the ways people interact with these systems can’t be anticipated. Errors are inevitable, but the right tools can help you discover and mitigate failure before it causes lasting damage.

Use a tool like Sentry so you can know when your users may be feeling pain. Use FullStory to show you exactly what they are doing in those moments before an error strikes so you can get the complete picture you need to remediate issues as fast as possible.

These two platforms work together to dramatically reduce the time to repair system errors by linking your users’ first-person view of an error with essential runtime error details flowing through Sentry.

P.S. We’re incredibly proud of the work we do, and want to share our love for engineering with the world. FullStory is constantly on the lookout for others who share our enthusiasm for finding creative solutions to exceptionally challenging software problems. If that sounds like you, say "Hi." 👋