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

2

10

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.

Meakem answered 8/5, 2018 at 0:3 Comment(3)
What kind of errors are you trying to catch?Choi
@GiovanniKleinCampigoto I am catching everything using jsnlog lib. The problem is not catching the errors but that in production they are useless. (I've updated the issue to reflect your question)Meakem
I'm guessing that react supresses the logs in production, and jsnlog stops working because of that. The only logs I see on React production env are network errorsChoi
M
8

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;
Meakem answered 15/6, 2018 at 17:42 Comment(4)
Are you sure it works? Using stackTrace in prod mode still gives me only minified info and is pretty useless then. I used your code and checked payload, it is still "at new t", "at Ta" kind of info. Have you solved that?Festschrift
Yes I'm using it already in my production app and is working for me... May be make sure you are adding also "stacktrace-gps". Let me know what happens. I'll add it to the answerMeakem
I guess my problem was that I have not build source maps. Now I've done it and it works nice! Thank you!Festschrift
Have you had a problem with multiple logger request, it is many of them (the same ones) and it'd be great if I could take only one of themFestschrift
B
4

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.

Bombay answered 30/10, 2018 at 15:45 Comment(2)
Good call on reducing bundle sizeMeakem
You can actually do all these on the backend, so no bundle size increase on the front-end at all! Should be cleaned up, but here is my take: gist.github.com/balazsorban44/7381bc1f6b9ecfd538902959f8de0140Schoonmaker

© 2022 - 2024 — McMap. All rights reserved.