Analytics 管线:净化、分级与双 Sink 上报机制
对应官方文档:claude-code-docs/docs/analytics.md 里的 Track team usage with analytics。
从定义开始
官方文档中的 Analytics 页面主要展示 Usage Dashboard、PR Attribution 和 Leaderboard。
但在 2.1.88 源码中,本地 CLI 的 Analytics 实现本质上是一套“事件上报与净化管线”,而不是 dashboard 计算器。 它的核心任务是:在本地捕捉产品运行时事件,对其进行严格的隐私净化和分级处理,然后将它们分流上报到不同的后端(Sink)。你看到的报表是云端后处理的结果,而本地源码负责的是:决定什么能上报、什么必须截断,以及哪些特权字段只能去特定的地方。
实现细节
这套管线的实现细节在于 src/services/analytics/ 下的统一分发层:
- 窄口径入口(Unified Entry):
src/services/analytics/index.ts是唯一入口。为了防止 PII(个人身份信息)泄露,Metadata 的类型被严格限制为boolean | number | undefined。注释里明文规定:不让随手塞字符串,是为了减少代码片段、文件路径或其他敏感内容被意外打进 Analytics 的风险。 - 双 Sink 分流(Sink Fanout):
src/services/analytics/sink.ts负责路由。它将同一个事件同时发往 Datadog 和 1P Event Logging(Anthropic 内部日志)。 - 字段分级(Field Grading):这是源码中最亮眼的设计。通过
_PROTO_*前缀定义的字段属于特权字段,可能包含 PII。在分流时,sink.ts会在发往 Datadog 前统一调用stripProtoFields(...)剥离这些字段;而 1P Exporter 则会保留它们,并将它们提升到 Proto 的顶层。 - 1P 独立通道(Independent 1P Logger):
firstPartyEventLogger.ts并不受普通 Customer OTel 开关(CLAUDE_CODE_ENABLE_TELEMETRY)的控制。它在src/entrypoints/init.ts中非常早的阶段就异步初始化了,拥有自己的 Batch 配置和重建逻辑。它能提前初始化是因为它没有 Trust 风险。 - 本地净化策略(Local Sanitization):在
metadata.ts中,源码对敏感字段做了大量的主动处理:- MCP 隐藏:普通用户定义的 MCP Tool 名会被统一标记为
mcp_tool。只有官方 registry 或特许场景才允许带出真实名称。 - Tool Input 截断:所有工具输入都会经过限深、限长、限集合大小的处理,绝对不是“原样上报”。
- MCP 隐藏:普通用户定义的 MCP Tool 名会被统一标记为
- 初始化与 Sink 注入:
src/utils/sinks.ts将 Analytics Sink 与 Error Log Sink 绑定。在src/main.tsx中,系统会先init()后再initSinks(),确保即使是进程极短的子命令(Subcommands)也能在退出前 Drain 掉内存队列中的事件。
边界条件
- 非 Dashboard 计算器:本地不产出报表,只负责上报原始事件。不要期望在源码里找到 PR 归因的聚合算法。
- 两套遥测系统:Customer-facing OTel(Monitoring)和 Anthropic-facing Analytics 是两套并行的通路。前者受 Trust 和用户开关控制,后者由 GrowthBook / Privacy Level / Sink Killswitch 裁决。
- 第三方提供商限制:如果你使用 Bedrock、Vertex 或 Foundry 这类第三方模型提供商,本地 Analytics 会自动进入短路状态,不再上报。
- Privileged 字段隔离:
_PROTO_*字段是 1P 专供的,外部 Datadog 或普通监控后端永远看不见。
相关主题
- 如果你关心用户自己配置的遥测通路,下一篇该看 Monitoring (OTel)。
- 如果你想看 privileged 字段最后是如何在 1P 后端落地的,去看 firstPartyEventLoggingExporter.ts。
- 如果你想看具体业务点(如权限审批)的埋点,去看各业务入口对
logEvent()的调用。
源码锚点
claude-code-opensource/src/services/analytics/index.ts:入口定义、事件排队与_PROTO_*字段分级标准。
📄 src/services/analytics/index.ts — 入口定义、事件排队与 `_PROTO_*` 字段分级标准。
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/analytics/sink.ts:事件分流(Fanout)、采样配置与 Datadog 前的敏感字段剥离。
📄 src/services/analytics/sink.ts — 事件分流(Fanout)、采样配置与 Datadog 前的敏感字段剥离。
typescript
* initialized during app startup. It routes events to Datadog and 1P event
* logging.
*
* Usage: Call initializeAnalyticsSink() during app startup to attach the sink.
*/
import { trackDatadogEvent } from './datadog.js'claude-code-opensource/src/services/analytics/metadata.ts:本地净化逻辑:MCP 名隐藏、Tool Input 截断、Skill 名条件放行。
📄 src/services/analytics/metadata.ts — 本地净化逻辑:MCP 名隐藏、Tool Input 截断、Skill 名条件放行。
typescript
* MCP tool names follow the format `mcp__<server>__<tool>` and can reveal
* user-specific server configurations, which is considered PII-medium.
* This function redacts MCP tool names while preserving built-in tool names
* (Bash, Read, Write, etc.) which are safe to log.
*
* @param toolName - The tool name to sanitize
* @returns The original name for built-in tools, or 'mcp_tool' for MCP tools
*/
export function sanitizeToolNameForAnalytics(
toolName: string,
): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {
if (toolName.startsWith('mcp__')) {
return 'mcp_tool' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
}
return toolName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
}claude-code-opensource/src/services/analytics/firstPartyEventLogger.ts:1P 独立遥测的 LoggerProvider 与 Batch 刷新逻辑。
📄 src/services/analytics/firstPartyEventLogger.ts — 1P 独立遥测的 LoggerProvider 与 Batch 刷新逻辑。
typescript
LoggerProvider,
} from '@opentelemetry/sdk-logs'
import {claude-code-opensource/src/entrypoints/init.ts:证明 1P 遥测可以在 Trust 建立前提前异步初始化。
📄 src/entrypoints/init.ts — 证明 1P 遥测可以在 Trust 建立前提前异步初始化。
typescript
export function initializeTelemetryAfterTrust(): void {
if (isEligibleForRemoteManagedSettings()) {
// For SDK/headless mode with beta tracing, initialize eagerly first
// to ensure the tracer is ready before the first query runs.
// The async path below will still run but doInitializeTelemetry() guards against double init.
if (getIsNonInteractiveSession() && isBetaTracingEnabled()) {
void doInitializeTelemetry().catch(error => {
logForDebugging(
`[3P telemetry] Eager telemetry init failed (beta tracing): ${errorMessage(error)}`,
{ level: 'error' },
)
})
}
logForDebugging(
'[3P telemetry] Waiting for remote managed settings before telemetry init',
)
void waitForRemoteManagedSettingsToLoad()
.then(async () => {
logForDebugging(
'[3P telemetry] Remote managed settings loaded, initializing telemetry',
)
// Re-apply env vars to pick up remote settings before initializing telemetry.
applyConfigEnvironmentVariables()
await doInitializeTelemetry()
})
.catch(error => {
logForDebugging(
`[3P telemetry] Telemetry init failed (remote settings path): ${errorMessage(error)}`,
{ level: 'error' },
)claude-code-opensource/src/utils/sinks.ts:Analytics Sink 在不同 CLI 入口中的挂载点。
📄 src/utils/sinks.ts — Analytics Sink 在不同 CLI 入口中的挂载点。
typescript
initializeAnalyticsSink()
}