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:
claude-code-best
2026-05-09 22:02:04 +08:00
parent 0707284939
commit 2006ab25ff
4 changed files with 71 additions and 43 deletions

View File

@@ -1,5 +1,6 @@
import { feature } from 'bun:bundle'; import { feature } from 'bun:bundle';
import chalk from 'chalk'; import chalk from 'chalk';
import { SentryErrorBoundary } from './SentryErrorBoundary.js';
import type { UUID } from 'crypto'; import type { UUID } from 'crypto';
import type { RefObject } from 'react'; import type { RefObject } from 'react';
import * as React from 'react'; import * as React from 'react';
@@ -890,7 +891,7 @@ const MessagesImpl = ({
); );
return ( return (
<> <SentryErrorBoundary name="MessagesBoundary">
{/* Logo */} {/* Logo */}
{!hideLogo && !(renderRange && renderRange[0] > 0) && <LogoHeader agentDefinitions={agentDefinitions} />} {!hideLogo && !(renderRange && renderRange[0] > 0) && <LogoHeader agentDefinitions={agentDefinitions} />}
@@ -977,7 +978,7 @@ const MessagesImpl = ({
/> />
</Box> </Box>
)} )}
</> </SentryErrorBoundary>
); );
}; };

View File

@@ -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
}
}

View 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;
}
}

View File

@@ -18,11 +18,14 @@ export async function launchRepl(
renderAndRun: (root: Root, element: React.ReactNode) => Promise<void>, renderAndRun: (root: Root, element: React.ReactNode) => Promise<void>,
): Promise<void> { ): Promise<void> {
const { App } = await import('./components/App.js'); const { App } = await import('./components/App.js');
const { SentryErrorBoundary } = await import('./components/SentryErrorBoundary.js');
const { REPL } = await import('./screens/REPL.js'); const { REPL } = await import('./screens/REPL.js');
await renderAndRun( await renderAndRun(
root, root,
<App {...appProps}> <SentryErrorBoundary name="RootREPLBoundary">
<REPL {...replProps} /> <App {...appProps}>
</App>, <REPL {...replProps} />
</App>
</SentryErrorBoundary>,
); );
} }