Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make React Create App Production Error Boundary map to source code

I am using an error boundary component to catch react errors and it works fine.

My problem is that in the production app the logging is kind of useless since the component stack looks like this:

\n    in t\n    in t\n   in t\n    in t\n    in t\n    in div\n    in t\n    in u\n    in n\n    in t\n    in t

While in development environment the component stack is way more usable:

in ErrorPage (created by Route)\n    in Route (at Routes.js:60)\n    in Switch (at Routes.js:46)\n    in Router (created by BrowserRouter)\n    in BrowserRouter (at Routes.js:45)\n    in div (at Routes.js:43)\n    in ThemeProvider (at theme.js:1262)\n    in Theme (at Routes.js:42)\n    in Provider (at Routes.js:41)\n    in ErrorBoundary (at Routes.js:40)\n    in Routes (at index.js:12)

The same happens with the message. In production we get:

t.value (http://localhost:3333/static/js/main.5a3e606e.js:1:680858

While in dev:

Uncaught TypeError: Person is not a constructor
at ErrorPage._this.click2 (ErrorPage.js:12)

Is there a way to make react errors map to the source code and make the logging actually usable in production?

UPDATE: I am using a library called http://js.jsnlog.com/ that handles the logs and actually catches everything (Even event handlers). This is how the Boundary component looks like https://pastebin.com/aBFtD7DB. The problem is not catching the errors, but that in production they are useless.

like image 918
Juan Solano Avatar asked May 08 '18 00:05

Juan Solano


People also ask

How do you write error boundaries in React?

A class component becomes an error boundary if it defines either (or both) of the lifecycle methods static getDerivedStateFromError() or componentDidCatch() . Use static getDerivedStateFromError() to render a fallback UI after an error has been thrown. Use componentDidCatch() to log error information.

How can I make use of error boundaries in functional React components?

In order to use Error Boundary in Functional Component, I use react-error-boundary. When we run this application, we will get a nice error display form the content of ErrorHandler component. React error boundary catches any error from the components below them in the tree.

How many error boundaries We can create and use in React?

Now that Error Boundaries are available since React version 16, it's generally advisable to use at least one Error Boundary at the root of your app (e.g., the App. js file). This will prevent users from seeing a blank HTML page and perhaps see a nice fallback UI instead.


2 Answers

I found a solution to this using the library https://www.stacktracejs.com/.

The method StackTrace.report() method will fetch the map and get you the unminified info you need!

So now my React Boundary looks like this. I still use window.onerror to make sure I catch everything.

First, make sure to add stacktrace-gps and stacktrace-js to your package.json

import React, { Component } from "react";
import StackTrace from "stacktrace-js";

window.onerror = function(msg, file, line, col, error) {
  StackTrace.fromError(error).then(err => {
    StackTrace.report(
      err,
      `//${window.location.hostname}:${process.env.REACT_APP_LOGGER_PORT || 3334}/jsnlog.logger`,
      {
        type: "window.onerror",
        url: window.location.href,
        userId: window.userId,
        agent: window.navigator.userAgent,
        date: new Date(),
        msg: msg
      }
    );
  });
};

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

  componentDidCatch(error, errorInfo) {
    this.setState({ error });
    StackTrace.fromError(error).then(err => {
      StackTrace.report(
        err,
        `//${window.location.hostname}:${process.env.REACT_APP_LOGGER_PORT || 3334}/jsnlog.logger`,
        {
          type: "React boundary",
          url: window.location.href,
          userId: window.userId,
          agent: window.navigator.userAgent,
          date: new Date(),
          msg: error.toString()
        }
      );
    });
  }

  render() {
    if (this.state.error) {
      //render fallback UI
      return (
        <div className="snap text-center">
          <p>We're sorry — something's gone wrong.</p>
          <p>Our team has been notified</p>
        </div>
      );
    } else {
      //when there's not an error, render children untouched
      return this.props.children;
    }
  }
}

export default ErrorBoundary;
like image 186
Juan Solano Avatar answered Oct 07 '22 00:10

Juan Solano


First, it is important to create source map. I did this by adding the devtools in webpack configuration for creating source map. Brief snippet of it is as follows:

devtools: "source-map",
new UglifyJsPlugin({
  sourceMap: true
})

Once source maps were created, I used the library https://www.stacktracejs.com/.

However, to reduce the bundle size on production, I didn't import the whole bundle of stacktrace. I implemented by seperating client side code and server side.

Client Side: I imported error-stack-parser. This creates an object, which contains filename, line number, column number and function name. I send the object created using this to server.

import ErrorStackParser from "error-stack-parser";

componentDidCatch(error) {
   let params = {stackframes: ErrorStackParser.parse(error)};
   let url = 'https://example.com';
   axios.post(url, params)
}

On the server side, I imported "stacktrace-gps" and "stackframe" and used it to find it, to get the line number and column of the actual code from the source map.

const StackTraceGPS = require("stacktrace-gps");
const request = require("request");

var logger = function(req, res) {
  let stackframes = req.body.stackframes;
  let stackframe = new StackFrame(
    stackframes[0]
  ); /* Getting stack of the topmost element as it contains the most important information */

  /* We send extra ajax function to fetch source maps from url */
  const gps = new StackTraceGPS({
    ajax: url => {
      return new Promise((resolve, reject) => {
        request(
          {
            url,
            method: "get"
          },
          (error, response) => {
            if (error) {
              reject(error);
            } else {
              resolve(response.body);
            }
          }
        );
      });
    }
  });

  gps.pinpoint(stackframe).then(
    info => {
      console.log(info); /* Actual file Info*/
    },
    err => {
      console.log(err);
    }
  );
};

This reduces the bundle size, and gives you the ability to log error on server side.

like image 29
Rajat Bhatnagar Avatar answered Oct 06 '22 22:10

Rajat Bhatnagar