Roo-Code/webview-ui/src/utils/sourceMapUtils.ts
KJ7LNW 3e89b06f25
debug: Add ErrorBoundary component for better error handling (#5085)
Co-authored-by: Eric Wheeler <roo-code@z.ewheeler.org>
2025-07-23 08:45:35 -04:00

190 lines
6.2 KiB
TypeScript

import * as StackTrace from "stacktrace-js"
/**
* Extended Error interface with source mapped stack trace
*/
export interface EnhancedError extends Error {
sourceMappedStack?: string
sourceMappedComponentStack?: string
}
/**
* Apply source maps to a stack trace using StackTrace.js
* Returns the original stack trace if source maps can't be applied
*/
export async function applySourceMapsToStack(stack: string): Promise<string> {
if (!stack) {
console.debug("applySourceMapsToStack: Empty stack trace provided")
return stack
}
console.debug("Original stack trace:", stack)
try {
// Create a temporary Error object with the provided stack
const tempError = new Error()
tempError.stack = stack
// Extract the error message (first line)
const errorMessage = stack.split("\n")[0]
console.debug("Error message:", errorMessage)
// Use StackTrace.js to get source mapped stack frames
const stackFrames = await StackTrace.fromError(tempError)
console.debug("StackTrace.js parsed frames:", stackFrames)
// Convert stack frames back to string format
const mappedFrames = stackFrames.map((frame: StackTrace.StackFrame) => {
const functionName = frame.functionName || "<anonymous>"
const fileName = frame.fileName || "unknown"
const lineNumber = frame.lineNumber || 0
const columnNumber = frame.columnNumber || 0
return ` at ${functionName} (${fileName}:${lineNumber}:${columnNumber})`
})
// Reconstruct the stack trace with the error message
const result = [errorMessage, ...mappedFrames].join("\n")
console.debug("Final mapped stack trace:", result)
return result
} catch (error) {
console.error("Error applying source maps with StackTrace.js:", error)
return stack // Return original stack on error
}
}
/**
* Apply source maps to a React component stack trace using StackTrace.js
*/
export async function applySourceMapsToComponentStack(componentStack: string): Promise<string> {
if (!componentStack) {
console.debug("applySourceMapsToComponentStack: Empty component stack provided")
return componentStack
}
console.debug("Original component stack:", componentStack)
try {
// Component stack has a different format than error stack
// Example: at ComponentName (file:///path/to/file.tsx:123:45)
const lines = componentStack.split("\n")
const mappedLines = await Promise.all(
lines.map(async (line) => {
// Skip empty lines
if (!line.trim()) return line
// Extract file path, line and column numbers
const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/)
if (!match) return line
const [_, componentName, fileName, lineNumber, columnNumber] = match
console.debug(`Processing component stack line:`, { componentName, fileName, lineNumber, columnNumber })
try {
// Create a synthetic stack frame for StackTrace.js
const syntheticError = new Error()
syntheticError.stack = `Error\n at ${componentName} (${fileName}:${lineNumber}:${columnNumber})`
// Use StackTrace.js to resolve source maps
const stackFrames = await StackTrace.fromError(syntheticError)
if (stackFrames.length > 0) {
const frame = stackFrames[0]
const mappedFileName = frame.fileName || fileName
const mappedLineNumber = frame.lineNumber || parseInt(lineNumber, 10)
const mappedColumnNumber = frame.columnNumber || parseInt(columnNumber, 10)
return `at ${componentName} (${mappedFileName}:${mappedLineNumber}:${mappedColumnNumber})`
}
} catch (e) {
console.debug(`Error processing component stack line with StackTrace.js:`, e)
}
return line
}),
)
const result = mappedLines.join("\n")
console.debug("Final mapped component stack:", result)
return result
} catch (error) {
console.error("Error applying source maps to component stack with StackTrace.js:", error)
return componentStack
}
}
/**
* Enhance an Error object with source mapped stack trace and component stack
*/
export function enhanceErrorWithSourceMaps(error: Error, componentStack?: string): Promise<EnhancedError> {
console.debug("Enhancing error with source maps using StackTrace.js:", error)
return new Promise<EnhancedError>((resolve) => {
if (!error.stack) {
console.debug("Error has no stack trace")
resolve(error as EnhancedError)
return
}
// Process both stacks in parallel
const stackPromise = applySourceMapsToStack(error.stack)
const componentStackPromise = componentStack
? applySourceMapsToComponentStack(componentStack)
: Promise.resolve(undefined)
Promise.all([stackPromise, componentStackPromise])
.then(([sourceMappedStack, sourceMappedComponentStack]) => {
console.debug("Source mapped stacks applied successfully with StackTrace.js")
// Extend the error object with the source mapped stack
Object.defineProperty(error, "sourceMappedStack", {
value: sourceMappedStack,
writable: true,
configurable: true,
})
// Add the source mapped component stack if available
if (sourceMappedComponentStack) {
Object.defineProperty(error, "sourceMappedComponentStack", {
value: sourceMappedComponentStack,
writable: true,
configurable: true,
})
}
resolve(error)
})
.catch((mapError) => {
console.error("Error applying source maps with StackTrace.js:", mapError)
// If anything fails, just return the original error
resolve(error)
})
})
}
/**
* Parse a stack trace string into structured stack frames
* This is kept for backward compatibility with tests
*/
export async function parseStackTrace(stack: string): Promise<any[]> {
if (!stack) return []
try {
// Create a temporary Error object with the provided stack
const tempError = new Error()
tempError.stack = stack
// Use StackTrace.js to parse the stack
const frames = await StackTrace.fromError(tempError)
return frames.map((frame: StackTrace.StackFrame) => ({
functionName: frame.functionName || "<anonymous>",
fileName: frame.fileName,
lineNumber: frame.lineNumber,
columnNumber: frame.columnNumber,
source: `at ${frame.functionName || "<anonymous>"} (${frame.fileName}:${frame.lineNumber}:${frame.columnNumber})`,
}))
} catch (error) {
console.error("Error parsing stack trace with StackTrace.js:", error)
return [] // Return empty array if parsing fails
}
}