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:
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)