错误分类与安全上报:在用户体验与隐私边界间寻找平衡
对应官方文档:claude-code-docs/docs/08-analytics-telemetry/error-classification.md(英文版)。
核心概念
在 2.1.88 源码中,错误处理远超简单的 try-catch 和控制台打印。它是一套面向三方受众的“分类分级系统”。
这套系统需要同时满足:
- 告知模型(Model feedback):给 Claude 提供足够的上下文(如错误栈、原因),让它决定是重试、修复还是放弃。
- 告知用户(User messaging):提供清晰、带路径或具体原因的报错,方便人工介入。
- 告知遥测(Telemetry reporting):将错误归类上报(如 Auth、Network、FS),但必须剔除 PII(个人身份信息),确保遥测后端看不到敏感的文件路径或代码片段。
源码级拆解
这套机制的核心位于 src/utils/errors.ts 和各业务入口的分类器中:
- 分类分级基础(Error Taxonomy):
ClaudeError:所有 CLI 内部报错的基类,确保统一的命名规范(name字段),便于在 Catch 块中进行模式匹配。TelemetrySafeError:这是隐私保护的核心。它分离了用户消息(可以包含文件路径)和遥测消息(保证是安全的、匿名的)。如果你在源码里看到TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,这说明作者要求开发者显式确认上报内容不含 PII。
- 异步与网络错误归一化(Network Classification):
src/utils/errors.ts中的classifyAxiosError负责将各种 HTTP 报错打上标签。它将错误归类为:auth(401/403)、timeout(超时)、network(DNS/拒绝连接)和http(其他)。这让云端统计能精准区分是网络波动还是授权过期。 - 模型友好的错误剪枝(Stack Pruning):当错误被抛回给模型时,系统会调用
shortErrorStack(...)将堆栈修剪为前 5 帧。这避免了将成千上万个 Token 浪费在冗长的库内部调用链上,只保留最相关的业务代码帧。 - 文件系统感知(FS Sensitivity):
isFsInaccessible用于识别特定的 FS 错误(如ENOENT、EACCES)。这让模型能智能地判断:是目录不存在(需要创建),还是权限不足(需要提示用户手动修改)。 - 中止状态管理(Abort Handling):
AbortError同时捕获AbortController信号和 Anthropic SDK 的各种中止语义。这确保了用户 Ctrl-C 取消操作时,系统能优雅地清理资源并停止上报事件,而不是将其记为“非预期崩溃”。 - 权限决策分类(Permission Decision Classification):在
toolExecution.ts和权限 Prompt 逻辑中,决策结果会被打上decisionClassification标签(如allow_always、deny_once)。这些标签是预设的,不含具体文件名,从而在不侵犯隐私的情况下收集用户的授权偏好趋势。
踩坑指南
- PII 兜底清洗:即使没有使用
TelemetrySafeError,上报到 Datadog 的错误消息也会经过一层正则清洗,剔除潜在的路径和代码。 - 类型转换安全(toError):由于三方库可能抛出字符串、Null 或非 Error 对象,源码中大量使用了
toError()封装,确保每个 Catch 块拿到的都是有标准的 Error 实例。 - 截断规则:所有上报的消息都有 20KB 左右的硬上限,防止超长报错消耗过多的网络带宽或后端存储。
- Expiry 自动检测:通过
isExpiredErrorType,系统能识别过期的 Session 并将其归类为“信息级(Info)”而非“错误级(Error)”,从而降低生产环境的告警噪音。
延伸阅读
- 如果你关心这些错误如何最终流向后端,下一篇该看 Analytics Pipeline。
- 如果你关心如何手动调试这些错误,去看
verbose模式下的logForDebugging实现。 - 如果你关心模型如何尝试自主修复错误,去看 Self-Correction 模式。
源码锚点
claude-code-opensource/src/utils/errors.ts:整个错误分类系统的定义:ClaudeError、TelemetrySafeError、classifyAxiosError、toError。
📄 src/utils/errors.ts — 整个错误分类系统的定义:`ClaudeError`、`TelemetrySafeError`、`classifyAxiosError`、`toError`。
typescript
export class ClaudeError extends Error {
constructor(message: string) {
super(message)
this.name = this.constructor.name
}
}claude-code-opensource/src/services/analytics/index.ts:错误事件的上报入口,以及隐私清洗逻辑。
📄 src/services/analytics/index.ts — 错误事件的上报入口,以及隐私清洗逻辑。
typescript
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never
/**
* Marker type for values routed to PII-tagged proto columns via `_PROTO_*`
* payload keys. The destination BQ column has privileged access controls,
* so unredacted values are acceptable — unlike general-access backends.
*
* sink.ts strips `_PROTO_*` keys before Datadog fanout; only the 1P
* exporter (firstPartyEventLoggingExporter) sees them and hoists them to the
* top-level proto field. A single stripProtoFields call guards all non-1P
* sinks — no per-sink filtering to forget.
*
* Usage: `rawName as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED`
*/
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED = never
/**
* Strip `_PROTO_*` keys from a payload destined for general-access storage.
* Used by:
* - sink.ts: before Datadog fanout (never sees PII-tagged values)
* - firstPartyEventLoggingExporter: defensive strip of additional_metadata
* after hoisting known _PROTO_* keys to proto fields — prevents a future
* unrecognized _PROTO_foo from silently landing in the BQ JSON blob.
*
* Returns the input unchanged (same reference) when no _PROTO_ keys present.
*/
export function stripProtoFields<V>(
metadata: Record<string, V>,
): Record<string, V> {
let result: Record<string, V> | undefinedclaude-code-opensource/src/services/tools/toolExecution.ts:如何将修剪后的错误栈(Short Stack)反馈给模型。
📄 src/services/tools/toolExecution.ts — 如何将修剪后的错误栈(Short Stack)反馈给模型。
typescript
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from 'src/services/analytics/index.js'
import {claude-code-opensource/src/bridge/bridgeApi.ts:isExpiredErrorType探测与过期 Session 的分类处理。
📄 src/bridge/bridgeApi.ts — `isExpiredErrorType` 探测与过期 Session 的分类处理。
typescript
export function isExpiredErrorType(errorType: string | undefined): boolean {
if (!errorType) {
return false
}
return errorType.includes('expired') || errorType.includes('lifetime')
}claude-code-opensource/src/utils/permissions/PermissionPromptToolResultSchema.ts:权限决策的分类标准(decisionClassification)。
📄 src/utils/permissions/PermissionPromptToolResultSchema.ts — 权限决策的分类标准(`decisionClassification`)。
typescript
decisionClassification: decisionClassificationField(),
}),
)
const PermissionDenyResultSchema = lazySchema(() =>
z.object({claude-code-opensource/src/utils/debug.ts:用于本地诊断的logForDebugging实现。
📄 src/utils/debug.ts — 用于本地诊断的 `logForDebugging` 实现。
typescript
export function logForDebugging(
message: string,
{ level }: { level: DebugLogLevel } = {
level: 'debug',
},