From ed945e3101460b389f7e7791e5d47a5be2c73ebb Mon Sep 17 00:00:00 2001 From: Eric Wheeler Date: Tue, 24 Jun 2025 13:47:07 -0700 Subject: [PATCH 1/4] debug: React error handling with proper stack traces Implements ErrorBoundary component to: - Capture and display detailed error stack traces - Provide clear error reporting instructions - Prevent app crashes by isolating errors May help troubleshoot https://github.com/RooCodeInc/Roo-Code/issues/2270 Based on work from https://github.com/Kilo-Org/kilocode/pull/843 Signed-off-by: Eric Wheeler --- webview-ui/src/App.tsx | 60 +++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 3c4c14f5dfe..21ca4f952c5 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -280,18 +280,60 @@ const App = () => { ) } +type ErrorProps = { + children: React.ReactNode +} + +type ErrorState = { + error?: string +} + +class ErrorBoundary extends Component { + constructor(props: ErrorProps) { + super(props) + this.state = {} + } + + static getDerivedStateFromError(error: unknown) { + return { + error: error instanceof Error ? (error.stack ?? error.message) : `${error}`, + } + } + + render() { + if (!this.state.error) { + return this.props.children + } + return ( +
+

Something went wrong

+

+ Please help us improve by reporting this error on + + Github + +

+

Please copy and paste the following error message:

+
{this.state.error}
+
+ ) + } +} + const queryClient = new QueryClient() const AppWithProviders = () => ( - - - - - - - - - + + + + + + + + + + + ) export default AppWithProviders From b17b326745fe057bef6f51f8874f75d0795b871f Mon Sep 17 00:00:00 2001 From: Eric Wheeler Date: Tue, 24 Jun 2025 15:37:43 -0700 Subject: [PATCH 2/4] debug: add ErrorBoundary component with source map support - Create dedicated ErrorBoundary component with proper internationalization - Add source map support for better error stack traces - Include version information in error display - Move test functionality to About page - Add comprehensive error and component stack display - Add translations for all error messages Signed-off-by: Eric Wheeler --- webview-ui/src/App.tsx | 41 +---- webview-ui/src/__tests__/App.spec.tsx | 84 ++++++++++ webview-ui/src/components/ErrorBoundary.tsx | 167 +++++++++++++++++++ webview-ui/src/components/settings/About.tsx | 35 +++- webview-ui/src/i18n/locales/en/common.json | 8 + 5 files changed, 294 insertions(+), 41 deletions(-) create mode 100644 webview-ui/src/components/ErrorBoundary.tsx diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 21ca4f952c5..2b263fd85e2 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -19,6 +19,7 @@ import { MarketplaceView } from "./components/marketplace/MarketplaceView" import ModesView from "./components/modes/ModesView" import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog" import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog" +import ErrorBoundary from "./components/ErrorBoundary" import { AccountView } from "./components/account/AccountView" import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick" import { TooltipProvider } from "./components/ui/tooltip" @@ -280,46 +281,6 @@ const App = () => { ) } -type ErrorProps = { - children: React.ReactNode -} - -type ErrorState = { - error?: string -} - -class ErrorBoundary extends Component { - constructor(props: ErrorProps) { - super(props) - this.state = {} - } - - static getDerivedStateFromError(error: unknown) { - return { - error: error instanceof Error ? (error.stack ?? error.message) : `${error}`, - } - } - - render() { - if (!this.state.error) { - return this.props.children - } - return ( -
-

Something went wrong

-

- Please help us improve by reporting this error on - - Github - -

-

Please copy and paste the following error message:

-
{this.state.error}
-
- ) - } -} - const queryClient = new QueryClient() const AppWithProviders = () => ( diff --git a/webview-ui/src/__tests__/App.spec.tsx b/webview-ui/src/__tests__/App.spec.tsx index 2c55d1cf074..78d026cf360 100644 --- a/webview-ui/src/__tests__/App.spec.tsx +++ b/webview-ui/src/__tests__/App.spec.tsx @@ -11,6 +11,20 @@ vi.mock("@src/utils/vscode", () => ({ }, })) +// Mock the ErrorBoundary component +vi.mock("@src/components/ErrorBoundary", () => ({ + __esModule: true, + default: ({ children }: { children: React.ReactNode }) => <>{children}, +})) + +// Mock the telemetry client +vi.mock("@src/utils/TelemetryClient", () => ({ + telemetryClient: { + capture: vi.fn(), + updateTelemetryState: vi.fn(), + }, +})) + vi.mock("@src/components/chat/ChatView", () => ({ __esModule: true, default: function ChatView({ isHidden }: { isHidden: boolean }) { @@ -88,11 +102,81 @@ vi.mock("@src/components/account/AccountView", () => ({ const mockUseExtensionState = vi.fn() +// Mock the HumanRelayDialog component +vi.mock("@src/components/human-relay/HumanRelayDialog", () => ({ + HumanRelayDialog: ({ _children, isOpen, onClose }: any) => ( +
+ Human Relay Dialog +
+ ), +})) + +// Mock i18next and react-i18next +vi.mock("i18next", () => { + const tFunction = (key: string) => key + const i18n = { + t: tFunction, + use: () => i18n, + init: () => Promise.resolve(tFunction), + changeLanguage: vi.fn(() => Promise.resolve()), + } + return { default: i18n } +}) + +vi.mock("react-i18next", () => { + const tFunction = (key: string) => key + return { + withTranslation: () => (Component: any) => { + const MockedComponent = (props: any) => { + return + } + MockedComponent.displayName = `withTranslation(${Component.displayName || Component.name || "Component"})` + return MockedComponent + }, + Trans: ({ children }: { children: React.ReactNode }) => <>{children}, + useTranslation: () => { + return { + t: tFunction, + i18n: { + t: tFunction, + changeLanguage: vi.fn(() => Promise.resolve()), + }, + } + }, + initReactI18next: { + type: "3rdParty", + init: vi.fn(), + }, + } +}) + +// Mock TranslationProvider to pass through children +vi.mock("@src/i18n/TranslationContext", () => { + const tFunction = (key: string) => key + return { + __esModule: true, + default: ({ children }: { children: React.ReactNode }) => <>{children}, + useAppTranslation: () => ({ + t: tFunction, + i18n: { + t: tFunction, + changeLanguage: vi.fn(() => Promise.resolve()), + }, + }), + } +}) + vi.mock("@src/context/ExtensionStateContext", () => ({ useExtensionState: () => mockUseExtensionState(), ExtensionStateContextProvider: ({ children }: { children: React.ReactNode }) => <>{children}, })) +// Mock environment variables +vi.mock("process.env", () => ({ + NODE_ENV: "test", + PKG_VERSION: "1.0.0-test", +})) + describe("App", () => { beforeEach(() => { vi.clearAllMocks() diff --git a/webview-ui/src/components/ErrorBoundary.tsx b/webview-ui/src/components/ErrorBoundary.tsx new file mode 100644 index 00000000000..a8d35d5f5f5 --- /dev/null +++ b/webview-ui/src/components/ErrorBoundary.tsx @@ -0,0 +1,167 @@ +import React, { Component } from "react" +import { telemetryClient } from "@src/utils/TelemetryClient" +import { withTranslation, WithTranslation } from "react-i18next" + +type ErrorProps = { + children: React.ReactNode +} & WithTranslation + +type ErrorState = { + error?: string + componentStack?: string | null + timestamp?: number +} + +class ErrorBoundary extends Component { + constructor(props: ErrorProps) { + super(props) + this.state = {} + } + + static getDerivedStateFromError(error: unknown) { + // Ensure we're getting the full stack trace with source maps + let errorMessage = "" + + if (error instanceof Error) { + // Use Error.stack which should include source-mapped locations + errorMessage = error.stack ?? error.message + + // If we have access to sourcemap-related properties, use them + if ("sourceMappedStack" in error && typeof error.sourceMappedStack === "string") { + errorMessage = error.sourceMappedStack + } + } else { + errorMessage = `${error}` + } + + return { + error: errorMessage, + timestamp: Date.now(), + } + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + // Process the component stack to ensure it uses source maps + const componentStack = errorInfo.componentStack || "" + + // Format the error stack to highlight TypeScript files + const formattedStack = this.formatStackTrace(error.stack || "") + + // Log to telemetry with enhanced error information + telemetryClient.capture("error_boundary_caught_error", { + error: error.message, + stack: formattedStack, + componentStack: componentStack, + timestamp: Date.now(), + sourceMapEnabled: true, // Flag to indicate we're trying to use source maps + errorType: error.name, + errorLocation: this.extractErrorLocation(error), + }) + + // Update state with component stack and formatted stack + this.setState({ + error: formattedStack, + componentStack: componentStack, + }) + } + + // Helper method to extract location information from error + private extractErrorLocation(error: Error): string { + if (!error.stack) return "unknown" + + // Try to extract the first line with a file path from the stack + const stackLines = error.stack.split("\n") + for (const line of stackLines) { + // Look for TypeScript file references (.ts or .tsx) + if (line.includes(".ts:") || line.includes(".tsx:")) { + return line.trim() + } + } + + // Fallback to the first line with any file reference + for (const line of stackLines) { + if (line.includes("(") && line.includes(")") && line.includes(":")) { + return line.trim() + } + } + + return "unknown" + } + + // Format stack trace to highlight TypeScript files + private formatStackTrace(stack: string): string { + if (!stack) return "" + + const lines = stack.split("\n") + const formattedLines = lines.map((line) => { + // Highlight TypeScript file references + if (line.includes(".ts:") || line.includes(".tsx:")) { + // Extract the TypeScript file path and line/column numbers + const match = + line.match(/\(([^)]+\.tsx?):(\d+):(\d+)\)/) || line.match(/at\s+([^)]+\.tsx?):(\d+):(\d+)/) + + if (match) { + const [_, filePath, lineNum, colNum] = match + // Format with the TypeScript file path highlighted + return line.replace(match[0], `(${filePath}:${lineNum}:${colNum})`) + } + } + return line + }) + + return formattedLines.join("\n") + } + + render() { + const { t } = this.props + + if (!this.state.error) { + return this.props.children + } + + // In production, truncate the error details to avoid exposing sensitive info + const isProduction = process.env.NODE_ENV === "production" + + // Format the error stack + const errorDisplay = isProduction ? this.state.error?.split("\n").slice(0, 3).join("\n") : this.state.error + + // Format the component stack if available + const componentStackDisplay = isProduction + ? this.state.componentStack?.split("\n").slice(0, 3).join("\n") + : this.state.componentStack + + // Get the package version from environment variables + const version = process.env.PKG_VERSION || "unknown" + + return ( +
+

+ {t("errorBoundary.title")} (v{version}) +

+

+ {t("errorBoundary.reportText")}{" "} + + {t("errorBoundary.githubText")} + +

+

{t("errorBoundary.copyInstructions")}

+ + {/* Error stack trace */} +
+

{t("errorBoundary.errorStack")}

+
{errorDisplay}
+
+ + {/* Component stack trace - only show if available */} + {componentStackDisplay && ( +
+

{t("errorBoundary.componentStack")}

+
{componentStackDisplay}
+
+ )} +
+ ) + } +} + +export default withTranslation("common")(ErrorBoundary) diff --git a/webview-ui/src/components/settings/About.tsx b/webview-ui/src/components/settings/About.tsx index 5075643e6e1..830875c0085 100644 --- a/webview-ui/src/components/settings/About.tsx +++ b/webview-ui/src/components/settings/About.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes } from "react" +import { HTMLAttributes, useState } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" import { Trans } from "react-i18next" import { Info, Download, Upload, TriangleAlert } from "lucide-react" @@ -22,9 +22,36 @@ type AboutProps = HTMLAttributes & { export const About = ({ telemetrySetting, setTelemetrySetting, className, ...props }: AboutProps) => { const { t } = useAppTranslation() + const [shouldThrowError, setShouldThrowError] = useState(false) + + // Function to trigger error for testing ErrorBoundary + const triggerTestError = () => { + setShouldThrowError(true) + } + + // Test component that throws an error when shouldThrow is true + const ErrorThrower = ({ shouldThrow = false }) => { + if (shouldThrow) { + // Create a more realistic error with a proper stack trace + try { + // Intentionally cause a type error by accessing a property on undefined + // This will generate a stack trace with TypeScript source maps + const obj: any = undefined + obj.nonExistentMethod() + } catch (e) { + // Rethrow with a custom message but preserve the original stack trace + const error = new Error("Test error: Accessing property on undefined") + error.stack = e instanceof Error ? e.stack : undefined + throw error + } + } + return null + } return (
+ {/* Test component that throws an error when shouldThrow is true */} + {t("settings:footer.settings.reset")} + + {/* Test button for ErrorBoundary - only visible in development */} +
diff --git a/webview-ui/src/i18n/locales/en/common.json b/webview-ui/src/i18n/locales/en/common.json index 74cd7086053..743eb482549 100644 --- a/webview-ui/src/i18n/locales/en/common.json +++ b/webview-ui/src/i18n/locales/en/common.json @@ -1,4 +1,12 @@ { + "errorBoundary": { + "title": "Something went wrong", + "reportText": "Please help us improve by reporting this error on", + "githubText": "GitHub", + "copyInstructions": "Please copy and paste the following error message:", + "errorStack": "Error Stack:", + "componentStack": "Component Stack:" + }, "answers": { "yes": "Yes", "no": "No", From 130671b0f4652e2dcc9fffe6812e26be7ef8c7b5 Mon Sep 17 00:00:00 2001 From: Eric Wheeler Date: Wed, 25 Jun 2025 13:32:45 -0700 Subject: [PATCH 3/4] debug: enable source-mapped stack traces in production Implements comprehensive source map support for better error reporting in production builds: - Adds StackTrace.js integration for reliable source map resolution - Updates Content Security Policy to allow source map fetching - Creates custom Vite plugin to ensure source maps are properly included - Adds global error handlers to enhance errors with source maps - Improves ErrorBoundary component to display source-mapped stack traces - Includes debugging utilities for troubleshooting in production This change makes debugging production errors much easier by showing original TypeScript file locations instead of minified bundle positions. Signed-off-by: Eric Wheeler --- pnpm-lock.yaml | 14 ++ src/.vscodeignore | 2 +- src/core/webview/ClineProvider.ts | 4 +- .../webview/__tests__/ClineProvider.spec.ts | 4 +- src/esbuild.mjs | 2 +- webview-ui/package.json | 2 + webview-ui/src/App.tsx | 15 ++ .../src/__tests__/ErrorBoundary.spec.tsx | 88 ++++++++ webview-ui/src/components/ErrorBoundary.tsx | 95 ++------- .../__tests__/ErrorBoundary.spec.tsx | 97 +++++++++ webview-ui/src/components/settings/About.tsx | 21 +- webview-ui/src/i18n/locales/en/common.json | 4 +- .../utils/__tests__/sourceMapUtils.spec.ts | 88 ++++++++ webview-ui/src/utils/sourceMapInitializer.ts | 180 +++++++++++++++++ webview-ui/src/utils/sourceMapUtils.ts | 190 ++++++++++++++++++ .../src/vite-plugins/sourcemapPlugin.ts | 116 +++++++++++ webview-ui/vite.config.ts | 10 +- 17 files changed, 830 insertions(+), 102 deletions(-) create mode 100644 webview-ui/src/__tests__/ErrorBoundary.spec.tsx create mode 100644 webview-ui/src/components/__tests__/ErrorBoundary.spec.tsx create mode 100644 webview-ui/src/utils/__tests__/sourceMapUtils.spec.ts create mode 100644 webview-ui/src/utils/sourceMapInitializer.ts create mode 100644 webview-ui/src/utils/sourceMapUtils.ts create mode 100644 webview-ui/src/vite-plugins/sourcemapPlugin.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5030055feac..0b94b47a1e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1036,6 +1036,9 @@ importers: source-map: specifier: ^0.7.4 version: 0.7.4 + stacktrace-js: + specifier: ^2.0.2 + version: 2.0.2 styled-components: specifier: ^6.1.13 version: 6.1.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1097,6 +1100,9 @@ importers: '@types/shell-quote': specifier: ^1.7.5 version: 1.7.5 + '@types/stacktrace-js': + specifier: ^2.0.3 + version: 2.0.3 '@types/vscode-webview': specifier: ^1.57.5 version: 1.57.5 @@ -3888,6 +3894,10 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/stacktrace-js@2.0.3': + resolution: {integrity: sha512-B6JnMic4NAZ4mLWmRi4RvayCN2HZQvpcVF0MkoqubtuZx1AQB0/kRlrngGiocEPyO7R+TFocTEoLKQ0HzmEOPw==} + deprecated: This is a stub types definition. stacktrace-js provides its own type definitions, so you do not need this installed. + '@types/stream-chain@2.1.0': resolution: {integrity: sha512-guDyAl6s/CAzXUOWpGK2bHvdiopLIwpGu8v10+lb9hnQOyo4oj/ZUQFOvqFjKGsE3wJP1fpIesCcMvbXuWsqOg==} @@ -13090,6 +13100,10 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/stacktrace-js@2.0.3': + dependencies: + stacktrace-js: 2.0.2 + '@types/stream-chain@2.1.0': dependencies: '@types/node': 20.19.1 diff --git a/src/.vscodeignore b/src/.vscodeignore index 8eed90df07e..9695b185a62 100644 --- a/src/.vscodeignore +++ b/src/.vscodeignore @@ -14,7 +14,7 @@ !dist # Include the built webview -**/*.map +!**/*.map !webview-ui/audio !webview-ui/build/assets/*.js !webview-ui/build/assets/*.ttf diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 107122dcb46..fe859d6b731 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -680,7 +680,7 @@ export class ClineProvider `img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:`, `media-src ${webview.cspSource}`, `script-src 'unsafe-eval' ${webview.cspSource} https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`, - `connect-src https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`, + `connect-src ${webview.cspSource} https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`, ] return /*html*/ ` @@ -762,7 +762,7 @@ export class ClineProvider - +