Search code examples
javascriptnode.jsreactjserror-handlingstack-trace

Get stack trace as a string when using source map?


To get as much as possible from my errors, I want to capture stack trace to display in the browser console and also to send it to an external logger service.

The first case works fine, when I console.error(myError) the source map file is interpreted correctly by the browser and the stack trace display relevant file names.

However, when I try to get the stack trace as a string with (new Error()).stack, the filenames are not relevant:

My Error
at applicationError (http://localhost:3000/static/js/main.chunk.js:29259:
at http://localhost:3000/static/js/main.chunk.js:1624:
at onError (http://localhost:3000/static/js/0.chunk.js:82415:3)
at apiCall (http://localhost:3000/static/js/0.chunk.js:82449:12)
at async http://localhost:3000/static/js/main.chunk.js:26165:21
at async App.componentWillMount (http://localhost:3000/static/js/main.chunk.js:246:5)

How can I get the "parsed" stack trace so I can send the relevant information to my logger?

I have seen these SO questions but, while the question seems relevant at first sight, none answer my question:


Solution

  • I had the same issue, and finally, I found this npm module: https://www.npmjs.com/package/sourcemapped-stacktrace

    Example from README:

    try {
      // break something
      bork();
    } catch (e) {
      // pass e.stack to window.mapStackTrace
      window.mapStackTrace(e.stack, function(mappedStack) {
        // do what you want with mappedStack here
        console.log(mappedStack.join("\n"));
      }, {
        filter: function (line) {
          // process only sources containing `spec.js`
          return /(spec\.js)/.test(line);
        }
      });
    }
    

    Edit:
    I was not able to get the above library to work, so I created a simple implementation for this myself. Might fail in some edge cases, and is probably not the most efficient, but for my needs it's enough.

    import {RawSourceMap, SourceMapConsumer} from 'source-map-js';
    
    const sourceMaps: {[key: string] : RawSourceMap} = {};
    async function getSourceMapFromUri(uri: string) {
      if (sourceMaps[uri] != undefined) {
        return sourceMaps[uri];
      }
      const uriQuery = new URL(uri).search;
      const currentScriptContent = await (await fetch(uri)).text();
    
      let mapUri = RegExp(/\/\/# sourceMappingURL=(.*)/).exec(currentScriptContent)[1];
      mapUri = new URL(mapUri, uri).href + uriQuery;
    
      const map = await (await fetch(mapUri)).json();
    
      sourceMaps[uri] = map;
    
      return map;
    }
    
    async function mapStackTrace(stack: string) {
      const stackLines = stack.split('\n');
      const mappedStack = [];
    
      for (const line of stackLines) {
        const match = RegExp(/(.*)(https?:\/\/.*):(\d+):(\d+)/).exec(line);
        if (match == null) {
          mappedStack.push(line);
          continue;
        }
    
        const uri = match[2];
        const consumer = new SourceMapConsumer(await getSourceMapFromUri(uri));
    
        const originalPosition = consumer.originalPositionFor({
          line: parseInt(match[3]),
          column: parseInt(match[4]),
        });
    
        if (originalPosition.source == null || originalPosition.line == null || originalPosition.column == null) {
          mappedStack.push(line);
          continue;
        }
    
        mappedStack.push(`${originalPosition.source}:${originalPosition.line}:${originalPosition.column + 1}`);
      }
    
      return mappedStack.join('\n');
    }
    

    (You could also probably use the normal source-map library, I had to use source-map-js because of some bug which happens in my specific environment)