UNPKG

backtrace-node

Version:
233 lines (208 loc) 7.12 kB
import { createHash, randomBytes } from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; import { scanFile } from 'source-scan'; import { ISourceCode, ISourceLocation, ISourceScan } from './sourceCode'; /** * Reprresent single stack frame in stack trace */ interface IBacktraceStackFrame { funcName: string; sourceCode: string | undefined; path: string; line: number; column: number; } /** * Analyse stack trace generated by exception * Create Stack Frames and find calling library/program informaiton */ export class BacktraceStackTrace { public readonly fault: boolean = true; public readonly name = 'main'; public stack: IBacktraceStackFrame[] = []; public sourceCodeInformation: { [index: string]: ISourceCode } = {}; public symbolicationMaps?: Array<{ file: string; uuid: string }>; private symbolicationPaths = new Set<string>(); private callingModulePath = ''; private readonly stackLineRe = /\s+at (.+) \((.+):(\d+):(\d+)\)/; private requestedSourceCode: { [path: string]: { id: string; data: ISourceLocation[] } } = {}; private tabWidth: number = 8; private contextLineCount: number = 200; constructor(private readonly error: Error) {} public setSourceCodeOptions(tabWidth: number, contextLineCount: number) { this.tabWidth = tabWidth; this.contextLineCount = contextLineCount; } /** * Get calling module path */ public getCallingModulePath(): string | undefined { if (!this.stack || this.stack.length === 0) { return undefined; } // handle a situation when every one stack frame is from node_modules if (!this.callingModulePath) { const sourceCode = this.stack.find((n) => !!n.sourceCode); if (!sourceCode) { return undefined; } for (const key in this.requestedSourceCode) { if (this.requestedSourceCode.hasOwnProperty(key)) { if (this.requestedSourceCode[key].id === sourceCode.sourceCode) { this.callingModulePath = key; } } } } return this.callingModulePath; } /** * Get Json data from Stack trace object */ public toJson() { return { name: this.name, fault: this.fault, stack: this.stack, }; } /** * Get source code information */ public getSourceCode(): { [index: string]: ISourceCode } { return this.sourceCodeInformation; } /** * Start parsing stack frames */ public async parseStackFrames(includeSymbolication: boolean): Promise<void> { const stackTrace = this.error.stack; if (!stackTrace) { return; } const appPath = process.cwd(); // get exception lines and remove first line of descrtiption const lines = stackTrace.split('\n').slice(1); const backtracePath = path.join('node_modules', 'backtrace-node'); lines.forEach((line) => { const match = line.match(this.stackLineRe); if (!match || match.length < 4) { return; } const fullSourceCodePath = match[2]; const backtraceLibStackFrame = fullSourceCodePath.indexOf(backtracePath) !== -1; if (backtraceLibStackFrame) { return; } let sourcePath = fullSourceCodePath; if (sourcePath) { sourcePath = path.relative(appPath, sourcePath); } const stackFrame = { funcName: match[1], path: sourcePath, line: parseInt(match[3], 10), column: parseInt(match[4], 10), } as IBacktraceStackFrame; // ignore not existing stack frames if (fs.existsSync(fullSourceCodePath)) { stackFrame.sourceCode = this.addSourceRequest(stackFrame, fullSourceCodePath); // extend root object with symbolication information if (includeSymbolication) { this.symbolicationPaths.add(fullSourceCodePath); } if (this.isCallingModule(fullSourceCodePath)) { this.callingModulePath = fullSourceCodePath; } } this.stack.push(stackFrame); }); // read source code information after reading all existing stack frames from stack trace await this.readSourceCode(); if (includeSymbolication) { this.generateSymbolicationMap(); } } private generateSymbolicationMap(): void { if (this.symbolicationPaths.size === 0) { return; } this.symbolicationMaps = []; this.symbolicationPaths.forEach(async (symbolicationPath) => { const file = fs.readFileSync(symbolicationPath, 'utf8'); const hash = createHash('md5').update(file).digest('hex'); this.symbolicationMaps?.push({ file: symbolicationPath, uuid: this.convertHexToUuid(hash), }); }); } private convertHexToUuid(hex: string): string { return ( hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' + hex.slice(16, 20) + '-' + hex.slice(20, 32) ); } private addSourceRequest(stackFrame: IBacktraceStackFrame, fullPath: string): string { // add source code to existing list. Otherwise create empty array if (!this.requestedSourceCode[fullPath]) { this.requestedSourceCode[fullPath] = { data: [], id: randomBytes(20).toString('hex'), }; } this.requestedSourceCode[fullPath].data.push({ line: stackFrame.line, column: stackFrame.column, }); return this.requestedSourceCode[fullPath].id; } private async readSourceCode(): Promise<void> { for (const key in this.requestedSourceCode) { if (this.requestedSourceCode.hasOwnProperty(key)) { const element = this.requestedSourceCode[key]; let minLine = element.data[0].line; let maxLine = minLine; for (let i = 1; i < element.data.length; i += 1) { const item = element.data[i]; minLine = Math.min(minLine, item.line); maxLine = Math.max(maxLine, item.line); } const parameter = { startLine: Math.max(minLine - this.contextLineCount + 1, 0), endLine: maxLine + this.contextLineCount, filePath: key, }; const res = await this.getSourceCodeInformation(parameter); this.sourceCodeInformation[element.id] = res; } } } private async getSourceCodeInformation(parameter: ISourceScan): Promise<ISourceCode> { return new Promise<ISourceCode>((res, rej) => { scanFile(parameter, (err: Error, buff: Buffer) => { if (err) { rej(err); return; } res({ startLine: parameter.startLine + 1, startColumn: 1, text: buff.toString('utf8'), tabWidth: this.tabWidth, }); }); }); } private isCallingModule(sourcePath: string): boolean { return !!sourcePath && !this.callingModulePath && !sourcePath.includes('node_modules'); } }