Skip to content
Demonstrating FullStory and Sentry in a React + Redux app
JavaScript HTML CSS Shell
Branch: master
Clone or download
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
public updating snippet with latest version Jul 3, 2019
src updating snippet with latest version Jul 3, 2019
.editorconfig init commit May 12, 2019
.gitignore first commit Oct 10, 2018
.prettierrc first commit Oct 10, 2018
.travis.yml first commit Oct 10, 2018
README.md Update README.md Jul 5, 2019
deploy.sh cleaning up some leftover log statements May 24, 2019
package-lock.json upgrading to latest react-scripts Sep 29, 2019
package.json

README.md

Understanding React + Redux errors with Sentry and FullStory

Sentry is an error monitoring platform used by many development teams to identify when issues crop up in their applications. FullStory lets development teams view user experience friction through the eyes of their users.

Sentry + FullStory arms development teams with an unprecedented ability to understand the details around the issues impacting their users.

Searching Hacker News

The Search Hacker News React + Redux app example in this repo is based on Robin Weiruch’s fantastic tutorial. There are a few differences from Robin’s original example:

  1. React Hooks are used instead of class components and lifecycle events.
  2. Redux-thunk is used for fetching stories from Hacker News rather than redux-saga.
  3. This app is riddled with bugs 🐞

You can try out the Search Hacker News app here or you can clone this repo and npm install then npm run start. The code is built with Create React App.

Setting up FullStory

You’ll need a FullStory account. Once you’ve setup your account, update the _fs_org value in the FullStory snippet in public/index.html.

window['_fs_org'] = 'your org id here';

Setting up Sentry

Sentry should be initialized as soon as possible during your application load up. In Search Hacker News, initSentry is called before the App component is loaded in src/index.js.

...
import { initSentry } from './api/error';

initSentry('<your Sentry key>', '<your Sentry project>');

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Once you are logged into Sentry, go here to find your Sentry.init statement (prefilled with your key and project values).

How FullStory links with Sentry

FullStory’s FS.getCurrentSessionURL API function generates a session replay URL for a particular moment in time. These URLs are deep links that can be shared with other tools and services. Session URLs are embedded into Sentry events when extra context is configured by providing a value for event.extra.fullstory in the beforeSend hook. The src/api/error.js module puts it all together.

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 || {};

      // getCurrentSessionURL isn't available until after the FullStory script is fully bootstrapped.
      // If an error occurs before getCurrentSessionURL is ready, make a note in Sentry and move on.
      // More on getCurrentSessionURL here: https://help.fullstory.com/develop-js/getcurrentsessionurl
      event.extra.fullstory = FullStory.getCurrentSessionURL(true) || 'current session URL API not ready';

      // FS.event is immediately ready even if FullStory isn't fully bootstrapped
      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 };

We’re also using the FullStory custom events API to send error data into FullStory. This lets us search for all users that experienced errors on the Search Hacker News app.

All the things that can go wrong...

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, sending error data to Sentry along with a FullStory session replay URL.

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:

image

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

image

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. The "Application Error" event is visible in the event stream on the right-hand side of the screen.

Hacker_News_Florida_Error

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 doFetchStoriesAsync = 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,
  doFetchStoriesAsync,
};

...which dispatches the caught exception to a doError action creator that calls recordError.

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
};

Type "break it" into the search field to trigger yet another contrived error :)

Catching unhandled errors in 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, 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 crashReporter.

You can greatly simplify this code by removing redux-thunk and handling the thunk on your own. See this issue for an explanation of how to do this.

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 event handler, 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.

Monitor, Alert, Watch, Fix

Bug-awareness is the critical first step in maintaining quality in your applications. Sentry let's you know that your users may be feeling pain. FullStory shows you exactly what they are doing in those moments before an error strikes and gives you the complete picture you need to remediate issues as fast as possible.

You can’t perform that action at this time.