mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
fix: 添加 React Error Boundary 防止生产环境渲染崩溃
增强 SentryErrorBoundary 组件,捕获渲染错误时输出诊断信息 (错误消息 + component stack)到 stderr 和终端,而非静默返回 null。在 replLauncher 根节点和 Messages 组件层级包裹 Error Boundary,防止 Ink 内部的 Error Boundary 直接终止进程。 Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import chalk from 'chalk';
|
||||
import { SentryErrorBoundary } from './SentryErrorBoundary.js';
|
||||
import type { UUID } from 'crypto';
|
||||
import type { RefObject } from 'react';
|
||||
import * as React from 'react';
|
||||
@@ -890,7 +891,7 @@ const MessagesImpl = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SentryErrorBoundary name="MessagesBoundary">
|
||||
{/* Logo */}
|
||||
{!hideLogo && !(renderRange && renderRange[0] > 0) && <LogoHeader agentDefinitions={agentDefinitions} />}
|
||||
|
||||
@@ -977,7 +978,7 @@ const MessagesImpl = ({
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
</SentryErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { captureException } from 'src/utils/sentry.js'
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode
|
||||
/** Optional label for identifying which component boundary caught the error */
|
||||
name?: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean
|
||||
}
|
||||
|
||||
export class SentryErrorBoundary extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = { hasError: false }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(): State {
|
||||
return { hasError: true }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||
captureException(error, {
|
||||
componentBoundary: this.props.name || 'SentryErrorBoundary',
|
||||
componentStack: errorInfo.componentStack,
|
||||
})
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
if (this.state.hasError) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
62
src/components/SentryErrorBoundary.tsx
Normal file
62
src/components/SentryErrorBoundary.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '@anthropic/ink';
|
||||
import { captureException } from 'src/utils/sentry.js';
|
||||
import { logError } from 'src/utils/log.js';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
/** Optional label for identifying which component boundary caught the error */
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
errorInfo: React.ErrorInfo | null;
|
||||
}
|
||||
|
||||
export class SentryErrorBoundary extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null, errorInfo: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Pick<State, 'hasError' | 'error'> {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||
this.setState({ errorInfo });
|
||||
|
||||
// Log to stderr so the diagnostic info is visible even in production builds
|
||||
const boundary = this.props.name || 'SentryErrorBoundary';
|
||||
const lines = ['', `[ErrorBoundary:${boundary}] React rendering error caught`, ` Message: ${error.message}`];
|
||||
if (errorInfo.componentStack) {
|
||||
lines.push(` Component stack:\n${errorInfo.componentStack}`);
|
||||
}
|
||||
// eslint-disable-next-line no-console -- intentional stderr diagnostic output
|
||||
console.error(lines.join('\n'));
|
||||
|
||||
logError(error);
|
||||
captureException(error, {
|
||||
componentBoundary: boundary,
|
||||
componentStack: errorInfo.componentStack,
|
||||
});
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1} paddingY={1}>
|
||||
<Text color="error" bold>
|
||||
React Rendering Error
|
||||
</Text>
|
||||
<Text color="error">{this.state.error?.message}</Text>
|
||||
{this.props.name && <Text dimColor>Boundary: {this.props.name}</Text>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -18,11 +18,14 @@ export async function launchRepl(
|
||||
renderAndRun: (root: Root, element: React.ReactNode) => Promise<void>,
|
||||
): Promise<void> {
|
||||
const { App } = await import('./components/App.js');
|
||||
const { SentryErrorBoundary } = await import('./components/SentryErrorBoundary.js');
|
||||
const { REPL } = await import('./screens/REPL.js');
|
||||
await renderAndRun(
|
||||
root,
|
||||
<App {...appProps}>
|
||||
<REPL {...replProps} />
|
||||
</App>,
|
||||
<SentryErrorBoundary name="RootREPLBoundary">
|
||||
<App {...appProps}>
|
||||
<REPL {...replProps} />
|
||||
</App>
|
||||
</SentryErrorBoundary>,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user