feat: 更新 sentry 错误上报

This commit is contained in:
claude-code-best
2026-04-03 09:39:25 +08:00
parent 1f0a2e44c8
commit 119518599e
11 changed files with 493 additions and 2 deletions

View File

@@ -1,7 +1,10 @@
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 {
@@ -18,6 +21,13 @@ export class SentryErrorBoundary extends React.Component<Props, 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

View File

@@ -48,6 +48,7 @@ import { configureGlobalAgents } from '../utils/proxy.js'
import { isBetaTracingEnabled } from '../utils/telemetry/betaSessionTracing.js'
import { getTelemetryAttributes } from '../utils/telemetryAttributes.js'
import { setShellIfWindows } from '../utils/windowsPaths.js'
import { initSentry } from '../utils/sentry.js'
// initialize1PEventLogging is dynamically imported to defer OpenTelemetry sdk-logs/resources
@@ -150,6 +151,9 @@ export const init = memoize(async (): Promise<void> => {
logForDebugging('[init] configureGlobalAgents complete')
profileCheckpoint('init_network_configured')
// Initialize Sentry for error reporting (no-op if SENTRY_DSN not set)
initSentry()
// Preconnect to the Anthropic API — overlap TCP+TLS handshake
// (~100-200ms) with the ~100ms of action-handler work before the API
// request. After CA certs + proxy agents are configured so the warmed

View File

@@ -2805,7 +2805,8 @@ export function REPL({
})) {
onQueryEvent(event);
}
if (feature('BUDDY')) {
// TODO: implement fireCompanionObserver — companion model reaction after each query turn
if (feature('BUDDY') && typeof fireCompanionObserver === 'function') {
void fireCompanionObserver(messagesRef.current, reaction => setAppState(prev => prev.companionReaction === reaction ? prev : {
...prev,
companionReaction: reaction as string | undefined

View File

@@ -20,6 +20,7 @@ import { logForDebugging } from './debug.js'
import { getFsImplementation } from './fsOperations.js'
import { attachErrorLogSink, dateToFilename } from './log.js'
import { jsonStringify } from './slowOperations.js'
import { captureException } from './sentry.js'
const DATE = dateToFilename(new Date())
@@ -171,6 +172,9 @@ function logErrorImpl(error: Error): void {
appendToLog(getErrorsPath(), {
error: `${context}${errorStr}`,
})
// Also report to Sentry (no-op if not initialized)
captureException(error)
}
/**

View File

@@ -42,6 +42,7 @@ import { logForDiagnosticsNoPII } from './diagLogs.js'
import { isEnvTruthy } from './envUtils.js'
import { getCurrentSessionTitle, sessionIdExists } from './sessionStorage.js'
import { sleep } from './sleep.js'
import { closeSentry } from './sentry.js'
import { profileReport } from './startupProfiler.js'
/**
@@ -503,7 +504,7 @@ export async function gracefulShutdown(
// Lost analytics on slow networks are acceptable; a hanging exit is not.
try {
await Promise.race([
Promise.all([shutdown1PEventLogging(), shutdownDatadog()]),
Promise.all([shutdown1PEventLogging(), shutdownDatadog(), closeSentry(2000)]),
sleep(500),
])
} catch {

160
src/utils/sentry.ts Normal file
View File

@@ -0,0 +1,160 @@
/**
* Sentry integration module
*
* Initializes Sentry SDK when SENTRY_DSN environment variable is set.
* When DSN is not configured, all exports are no-ops.
*/
import * as Sentry from '@sentry/node'
import { logForDebugging } from './debug.js'
let initialized = false
/**
* Initialize Sentry SDK. Safe to call multiple times — subsequent calls are no-ops.
* Only activates when SENTRY_DSN environment variable is set.
*/
export function initSentry(): void {
if (initialized) {
return
}
const dsn = process.env.SENTRY_DSN
if (!dsn) {
logForDebugging('[sentry] SENTRY_DSN not set, skipping initialization')
return
}
Sentry.init({
dsn,
release: typeof MACRO !== 'undefined' ? MACRO.VERSION : undefined,
environment:
typeof BUILD_ENV !== 'undefined' ? BUILD_ENV : process.env.NODE_ENV || 'development',
// Limit breadcrumbs and attachments to control payload size
maxBreadcrumbs: 20,
// Sample rate for error events (1.0 = capture all)
sampleRate: 1.0,
// Filter sensitive information before sending
beforeSend(event) {
// Strip auth headers from request data
const request = event.request
if (request?.headers) {
const sensitiveHeaders = [
'authorization',
'x-api-key',
'cookie',
'set-cookie',
]
for (const key of Object.keys(request.headers)) {
if (sensitiveHeaders.includes(key.toLowerCase())) {
delete request.headers[key]
}
}
}
return event
},
// Ignore specific error patterns
ignoreErrors: [
// Network errors from unreachable hosts — not actionable
'ECONNREFUSED',
'ECONNRESET',
'ENOTFOUND',
'ETIMEDOUT',
// User-initiated aborts
'AbortError',
'The user aborted a request',
// Interactive cancellation signals
'CancelError',
],
beforeSendTransaction(event) {
// Don't send performance transactions for now — errors only
return null
},
})
initialized = true
logForDebugging('[sentry] Initialized successfully')
}
/**
* Capture an exception and send it to Sentry.
* No-op if Sentry has not been initialized.
*/
export function captureException(error: unknown, context?: Record<string, unknown>): void {
if (!initialized) {
return
}
try {
Sentry.withScope(scope => {
if (context) {
scope.setExtras(context)
}
Sentry.captureException(error)
})
} catch {
// Sentry itself failed — don't let it crash the app
}
}
/**
* Set a tag on the current scope for grouping/filtering in Sentry.
* No-op if Sentry has not been initialized.
*/
export function setTag(key: string, value: string): void {
if (!initialized) {
return
}
try {
Sentry.setTag(key, value)
} catch {
// Ignore
}
}
/**
* Set user context in Sentry for error attribution.
* No-op if Sentry has not been initialized.
*/
export function setUser(user: { id?: string; email?: string; username?: string }): void {
if (!initialized) {
return
}
try {
Sentry.setUser(user)
} catch {
// Ignore
}
}
/**
* Flush pending Sentry events and close the client.
* Call during graceful shutdown to ensure events are sent.
*/
export async function closeSentry(timeoutMs = 2000): Promise<void> {
if (!initialized) {
return
}
try {
await Sentry.close(timeoutMs)
logForDebugging('[sentry] Closed successfully')
} catch {
// Ignore — we're shutting down anyway
}
}
/**
* Check if Sentry is initialized. Useful for conditional UI rendering.
*/
export function isSentryInitialized(): boolean {
return initialized
}