/** * Frame represents a stack frame. */ export interface Frame { function: string | null; file: string; line: number; column: number; } /** * getStackTrace returns the current stack trace * with frames from the internals of the javascript runtime removed. */ export function getStackTrace(drop = 0): Frame[] { // First try to get the stack trace from an error const obj = new Error("placeholder"); if (obj.stack) { let frames = extractFrames(obj.stack); if (frames && frames.length > drop) { frames = frames.slice(drop + 1); return frames.filter((frame) => !isCommonFrame(frame)); } } return []; } /** * extractFrames extracts the frames from a stack trace string generated by Error.stack */ export function extractFrames(stack?: string): Frame[] | undefined { if (!stack) { return undefined; } const frames: Frame[] = []; const lines = stack.split("\n"); for (const line of lines) { const frame = extractFrame(line); if (frame) { frames.push(frame); } } if (frames.length === 0) { return undefined; } return frames; } /** * isCommonFrame returns true if the frame is a common frame that should be ignored. * as it is outside the users code base (such as it comes from the node runtime). */ export function isCommonFrame(stack: Frame): boolean { // "node:" is a node internal frame // "bun:" is a bun internal frame // "" is a frame with no file, so a core JS function return ( stack.file.startsWith("node:") || stack.file.startsWith("bun:") || stack.file === "" ); } /** * Typeguard for a stack trace */ export function isStackFrames(stack: unknown): stack is Frame[] { if (stack === undefined || stack === null || !Array.isArray(stack)) { // This is not a stack trace return false; } if (stack.length === 0) { // This is a valid stack trace, just nothing is in it! return true; } // Check that every array element is a Frame for (const frame of stack) { if (typeof frame !== "object" || frame === null || frame === undefined) { return false; } if ( typeof frame.function !== "string" || typeof frame.file !== "string" || typeof frame.line !== "number" || typeof frame.column !== "number" ) { return false; } } return true; } const frameRegex = /^\s*at\s+(.+)\s+\((.*):(\d+):(\d+)\)\s*$/; const frameRegex2 = /^\s*at\s+(.+):(\d+):(\d+)\s*$/; function extractFrame(line: string): Frame | undefined { const parts = frameRegex.exec(line); if (parts) { return { function: parts[1], file: parts[2], line: parseInt(parts[3], 10), column: parseInt(parts[4], 10), }; } const parts2 = frameRegex2.exec(line); if (parts2) { return { function: null, file: parts2[1], line: parseInt(parts2[2], 10), column: parseInt(parts2[3], 10), }; } return undefined; }