Skip to content

Instantly share code, notes, and snippets.

@kyle-villeneuve
Created March 25, 2022 23:29
Show Gist options
  • Save kyle-villeneuve/101302cf89d77a180d041408c6bc89b2 to your computer and use it in GitHub Desktop.
Save kyle-villeneuve/101302cf89d77a180d041408c6bc89b2 to your computer and use it in GitHub Desktop.
Decode client error stacktrace
import { STATIC_ASSETS_PATH } from '@skusavvy/config';
import { readFileSync } from 'fs';
import { join } from 'path';
import { NullableMappedPosition, SourceMapConsumer } from 'source-map';
type ParsedLocation = { file: string; line: number; column: number; token: string };
type ParsedStack = { message: string; files: Record<string, ParsedLocation[]> };
const RE_TOKENIZE_STACK_ITEM = /at (\w+) \((https?:\/\/[\w\.\d]+(:\d+)?)\/([\/\w\.]+):(\d+):(\d+)\)/g;
function parseStack(stack: string): ParsedStack {
const [message, ...lines] = stack.split('\n').map((l) => l.trim());
const files: ParsedStack['files'] = {};
lines.forEach((l) =>
l.replace(RE_TOKENIZE_STACK_ITEM, (_full, token, _host, _port, file, line, column) => {
const location: ParsedLocation = { file, token, line: Number(line), column: Number(column) };
if (!files[file]) files[file] = [];
files[file].push(location);
return '';
})
);
return { message, files };
}
async function decodeError(parsed: ParsedStack) {
const decoded: NullableMappedPosition[] = [];
for (const [filePath, locations] of Object.entries(parsed.files)) {
// load corresponding sourcemap of file where function(s) were called
const joinedPath = join('src', STATIC_ASSETS_PATH, filePath + '.map');
const file = readFileSync(joinedPath);
if (!file) {
throw new Error(`Cannot decode stacktrace, sourcemap of file "${joinedPath}" not found`);
}
// when instantiated with `.with` you don't need to dispose of the consumer instance
// when instantiated with `new` you must call `.destroy()` when completed to g/c WASM
await SourceMapConsumer.with(file.toString('utf-8'), null, async (consumer) => {
locations.forEach((p) => {
const position = consumer.originalPositionFor({
line: p.line,
column: p.column,
});
position.line && decoded.push(position);
});
});
}
return { message: parsed.message, decoded };
}
function encodeStack({ message, decoded }: { message: string; decoded: NullableMappedPosition[] }) {
const encoded = [
message,
...decoded.map((d) => `\tat ${d.name || '???'} (${d.source}:${d.line}:${d.column})`),
];
return encoded.join('\n');
}
export default async function decodeErrorStackTrace(stack: string) {
try {
const parsed = parseStack(stack);
const decoded = await decodeError(parsed);
return encodeStack(decoded);
} catch (err) {
console.log(err);
return null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment