mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 13:55:50 +00:00
feat: 更新 sentry 错误上报
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
160
src/utils/sentry.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user