分析与遥测
Claude Code 如何跟踪使用情况、将事件路由到 Datadog 和第一方日志记录、使用 GrowthBook 控制功能以及在数据离开计算机之前对其进行清理。
01 架构概述
Claude Code 的分析系统分为六个文件 services/analytics/。 设计分开 what 登录自 how 事件被路由,保持公共 API 不依赖,以便任何模块都可以调用 logEvent() 无需在导入时引入 Datadog 或 OTel 传输。
公共API + 队列
零依赖。 暴露 logEvent / logEventAsync。 将事件排队直到连接接收器。
Router
启动时初始化。 将事件扇出至 Datadog 和 1P、应用采样、检查功能门、剥离 PII。
数据狗传输
批量记录日志,每 15 秒或累积 100 个条目时刷新。 仅限第一方用户。
元数据丰富
将平台、模型、会话、代理 ID、流程指标和存储库哈希附加到每个事件。
功能标志
GrowthBook 远程评估客户端。 驱动 A/B 实验、配置值和终止开关。
紧急关闭开关
成长簿配置 tengu_frond_boric 可以禁用单个接收器而不需要释放。
02 预接收队列
由于分析导入发生在应用程序完全初始化之前,因此启动期间触发的事件将会丢失。 index.ts 使用模块级数组解决这个问题:
// index.ts — simplified
const eventQueue: QueuedEvent[] = []
let sink: AnalyticsSink | null = null
export function logEvent(eventName: string, metadata: LogEventMetadata): void {
if (sink === null) {
eventQueue.push({ eventName, metadata, async: false })
return
}
sink.logEvent(eventName, metadata)
}
export function attachAnalyticsSink(newSink: AnalyticsSink): void {
if (sink !== null) return // idempotent
sink = newSink
if (eventQueue.length > 0) {
const queuedEvents = [...eventQueue]
eventQueue.length = 0
queueMicrotask(() => {
for (const event of queuedEvents) { sink!.logEvent(event.eventName, event.metadata) }
})
}
}
queueMicrotask 而不是同步。 这避免了增加启动热路径的延迟——启动代码调用 attachAnalyticsSink 并返回; 积压工作将在下一个微任务检查点耗尽。
attachAnalyticsSink and initializeAnalyticsSink (在sink.ts中)是显式幂等的。 他们可以从 preAction hooks(子命令)和 from setup() (默认命令)无需协调或双重连接。
03 接收器路由——sink.ts
sink.ts 是交通警察。 对于每个事件,它在调度之前都会应用三个顺序检查:
// sink.ts — logEventImpl
function logEventImpl(eventName: string, metadata: LogEventMetadata): void {
const sampleResult = shouldSampleEvent(eventName)
if (sampleResult === 0) return // dropped by sampling
const metadataWithSampleRate =
sampleResult !== null
? { ...metadata, sample_rate: sampleResult }
: metadata
if (shouldTrackDatadog()) {
// strip _PROTO_* keys — PII tagged, Datadog is general-access
void trackDatadogEvent(eventName, stripProtoFields(metadataWithSampleRate))
}
// 1P exporter receives the full payload including _PROTO_* keys
logEventTo1P(eventName, metadataWithSampleRate)
}
_PROTO_ 通过 1P 导出器路由到特权 BigQuery 列。 stripProtoFields() 在 Datadog 扇出之前调用,因此这些值永远不会到达通用访问后端。 然后,出口商将它们提升到原型油田后,防御性地剥离它们,因此无法识别 _PROTO_foo 将来不能默默地登陆 BQ JSON blob。
04 数据狗传输
Datadog 收到一份精心策划的事件允许列表。 活动不在 DATADOG_ALLOWED_EVENTS 都被默默丢弃。 这使得 Datadog 专注于运营监控而不是产品分析。
// datadog.ts — batching + flush
const DEFAULT_FLUSH_INTERVAL_MS = 15000
const MAX_BATCH_SIZE = 100
const NETWORK_TIMEOUT_MS = 5000
function scheduleFlush(): void {
if (flushTimer) return
flushTimer = setTimeout(() => {
flushTimer = null
void flushLogs()
}, getFlushIntervalMs()).unref() // .unref() — never blocks process exit
}
// Flush immediately if batch is full, otherwise schedule
if (logBatch.length >= MAX_BATCH_SIZE) {
void flushLogs()
} else {
scheduleFlush()
}
基数减少
在构建 Datadog 标签之前,一些标准化可以减少度量基数:
型号名称
- 对于非 Anthropic 用户,模型名称将规范化为其基本名称。
- 未知型号被列为
"other"以避免标签爆炸。 - Ant(内部用户)保留完整的模型字符串以进行调试。
MCP 工具名称
mcp__slack__post_messagebecomes"mcp"在 Datadog 标签中。- 根据内部分类,用户配置的服务器名称是 PII 中型。
- 在metadata.ts 中允许通过的官方注册服务器。
版本字符串
- 开发构建:
2.0.53-dev.20251124.t173302.sha526cc6a→2.0.53-dev.20251124 - 时间戳和 SHA 被剥离以避免每次构建都有新标签。
用户桶
- 用户 ID 散列到 30 个存储桶中的 1 个(SHA-256,前 8 个十六进制字符 mod 30)。
- 启用近似唯一用户计数,无需记录实际 ID。
- 保护隐私,同时支持对“受影响的用户”发出警报。
05 元数据丰富——metadata.ts
每场活动都充满了丰富的内容 EventMetadata 组装的对象 getEventMetadata()。 这包括三个嵌套层:
EnvContext — 平台和运行时快照
每个进程构建一次并记住。 捕获:
EnvContext {
platform: 'darwin' | 'linux' | 'windows'
platformRaw: // raw process.platform (freebsd visible here, not bucketed)
arch: 'arm64' | 'x64'
nodeVersion: // runtime version string
terminal: // $TERM_PROGRAM
packageManagers: // comma-joined: 'npm,bun'
runtimes: // node, bun, deno detection
isCi: // $CI env var
isGithubAction: // $GITHUB_ACTIONS
isClaudeAiAuth: // OAuth vs API key auth
version: // full semver build string
wslVersion: // WSL 1/2 if applicable
vcs: // 'git' | 'hg' | ... detected VCS
// + GitHub Actions runner metadata, container IDs, remote session IDs
}
ProcessMetrics — 每个事件的内存和 CPU 增量
ProcessMetrics {
uptime: // process.uptime()
rss: // resident set size in bytes
heapTotal: // V8 heap allocated
heapUsed: // V8 heap in use
cpuPercent: // delta since last event (user+sys µs / wall-clock ms)
constrainedMemory: // process.constrainedMemory() if available
}
CPU 百分比是 delta: (userDeltaµs + sysDeltaµs) / (wallDeltaMs × 1000) × 100。 模块级变量 prevCpuUsage and prevWallTimeMs 在通话之间坚持。
代理识别——群体和子代理归因
EventMetadata {
agentId?: // CLAUDE_CODE_AGENT_ID or subagent UUID
parentSessionId?: // lead session for cross-session joining in BQ
agentType?: // 'teammate' | 'subagent' | 'standalone'
teamName?: // swarm team label
rh?: // first 16 chars of SHA-256(repo remote URL)
subscriptionType?: // 'max' | 'pro' | 'enterprise' | 'team'
kairosActive?: // ant-only KAIROS assistant flag
}
首先检查 AsyncLocalStorage(对于在同一进程中运行的子代理),然后检查环境变量(对于在单独进程中运行的 swarm 队友)。 这意味着归因是自动的——呼叫者不需要手动传递代理上下文。
06 metadata.ts 中的 PII 清理
类型系统本身强制要求在记录之前检查字符串。 事件元数据中的任何字符串值都必须转换为两种标记类型之一 - 两者都键入为 never,因此它们只能用作记录开发人员意图的转换:
// You cannot store a value in these types — they're "never".
// They exist solely as cast targets to document a review decision.
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED = never
// Usage example — sanitizing MCP tool names:
export function sanitizeToolNameForAnalytics(toolName: string) {
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
}
工具输入截断
When OTEL_LOG_TOOL_DETAILS=1 设置后,工具输入将针对 OTel 范围进行序列化。 超过 512 个字符的字符串将被截断为 128 个字符,并带有长度注释。 对象限制为深度 2 和 20 个项目。 总 JSON 的上限为 4 KB。
文件扩展名提取
对于 bash 工具调用,Claude Code 从已知命令中提取文件扩展名(cat, mv, cp, rm等)来跟踪正在操作的文件类型 - 而不记录实际的文件路径。 超过 10 个字符的扩展名将被编辑为 "other" 以防止基于哈希的文件名泄漏。
07 功能标志 — GrowthBook
GrowthBook 驱动器具有标志、A/B 实验、动态配置和终止开关。 Claude Code 使用其 远程评估 模式:服务器评估该用户的所有规则并返回预先计算的值,而不是将完整的规则树发送到客户端。
用于定位的用户属性
GrowthBookUserAttributes {
id: // stable device UUID
sessionId: // per-session ID
deviceID: // same as id
platform: // 'win32' | 'darwin' | 'linux'
organizationUUID?: // enterprise org targeting
accountUUID?: // account-level targeting
subscriptionType?: // 'max' | 'pro' | 'enterprise' | 'team'
email?: // ant-only: always included for internal targeting
appVersion?: // semver for version-range gates
github?: // GitHub Actions runner metadata
}
三级优先级
Env 被覆盖
CLAUDE_INTERNAL_FC_OVERRIDES JSON 对象。 仅限蚂蚁。 由评估线束用于确定性标志状态。
配置覆盖
设置通过 /config Gates 运行时选项卡。 存储在 ~/.claude.json。 仅限蚂蚁。
远程评估
取自 api.anthropic.com。 每次成功提取时都会缓存到磁盘,以便下一个会话以最后的已知值开始。
SDK 解决方法 — 远程评估格式
GrowthBook API 返回特征值 { "value": ... } 形状,但 SDK 期望 { "defaultValue": ... }。 Claude Code 通过在加载之前转换有效负载并维护其自己的负载来解决此问题 remoteEvalFeatureValues 绕过 SDK 本地重新评估的映射:
// growthbook.ts — processRemoteEvalPayload (simplified)
for (const [key, feature] of Object.entries(payload.features)) {
if ('value' in feature && !('defaultValue' in feature)) {
transformedFeatures[key] = { ...feature, defaultValue: feature.value }
}
// Cache evaluated value directly to avoid SDK re-evaluation
remoteEvalFeatureValues.set(key, feature.value ?? feature.defaultValue)
}
features 对象(瞬时错误或被截断的响应), processRemoteEvalPayload returns false 无需清除磁盘缓存。 这可以防止所有进程共享的完全标志中断 ~/.claude.json.
实验曝光重复数据删除
当 GrowthBook 将用户分配给实验变体时,Claude Code 会记录一个 曝光事件 到 1P 后端进行正确的实验分析。 模块级 loggedExposures 设置确保每个功能在每个会话中最多触发一次曝光事件 - 防止调用的热代码路径中出现重复条目 getFeatureValue 在每个渲染上。
08 终止开关
如果分析中的错误导致出现问题(流量过大、意外 PII 等),Anthropic 可以远程禁用各个接收器,而无需发布新版本:
// sinkKillswitch.ts
// Deliberately obfuscated config key to avoid easy discovery
const SINK_KILLSWITCH_CONFIG_NAME = 'tengu_frond_boric'
export function isSinkKilled(sink: 'datadog' | 'firstParty'): boolean {
const config = getDynamicConfig_CACHED_MAY_BE_STALE<
Partial<Record<'datadog' | 'firstParty', boolean>>
>('tengu_frond_boric', {})
return config?.[sink] === true
}
isSinkKilled returns false — 接收器保持启用状态。 这可以防止 GrowthBook 中断也导致分析沉默。 仅当配置显式设置时才会禁用接收器 { "datadog": true }.
09 当分析被禁用时
config.ts 定义了完全抑制分析的条件:
export function isAnalyticsDisabled(): boolean {
return (
process.env.NODE_ENV === 'test' // test environments
|| isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) // AWS Bedrock
|| isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) // GCP Vertex
|| isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) // Azure Foundry
|| isTelemetryDisabled() // user privacy setting
)
}
第三方云提供商(Bedrock、Vertex、Foundry)完全禁用分析 - 如果没有 Anthropic 身份验证上下文,事件数据将毫无意义,并且这些用户拥有自己的日志记录管道。 这 isTelemetryDisabled() 路径尊重用户的隐私级别设置。
isFeedbackSurveyDisabled() does not 检查第三方提供商。 调查提示是本地 UI,没有文字记录数据,因此它们在基岩/顶点上保持活动状态,而分析将被阻止。
10 事件采样
高频事件(例如 tengu_api_success 在繁忙的会话中)可以产生巨大的音量。 GrowthBook配置键 tengu_event_sampling_config 让 Anthropic 设置每个事件的采样率,无需更改代码:
// firstPartyEventLogger.ts — shouldSampleEvent
export function shouldSampleEvent(eventName: string): number | null {
const config = getEventSamplingConfig() // from GrowthBook cache
const eventConfig = config[eventName]
if (!eventConfig) return null // no config = 100% logging
const sampleRate = eventConfig.sample_rate
if (sampleRate <= 0) return 0 // 0 = drop everything
if (sampleRate >= 1) return null // 1 = keep everything (no annotation needed)
// Probabilistic: roll the dice
return Math.random() < sampleRate
? sampleRate // sampled — return rate so it can be added to event metadata
: 0 // not sampled — sink drops it
}
当对事件进行采样时, sample_rate 附加到其元数据中。 这使得分析师可以应用逆概率加权来重建未采样的总数。
11 文件参考
核心文件
- services/analytics/index.ts
- services/analytics/sink.ts
- services/analytics/config.ts
- services/analytics/sinkKillswitch.ts
后端和丰富
- services/analytics/datadog.ts
- services/analytics/metadata.ts
- services/analytics/growthbook.ts
- services/analytics/firstPartyEventLogger.ts
12 要点
零深度公共 API
任何模块都可以调用 logEvent() 无需创建导入周期。 接收器稍后在启动过程中接线。
类型强制清理
字符串需要显式转换为标记类型。 如果代码中没有开发人员签名注释,任何字符串都无法到达后端。
GrowthBook 作为控制平面
采样率、特征门、A/B 实验和紧急终止开关都流经单个远程评估端点。
随处失效打开
缺少配置、失败的获取和 GrowthBook 中断都默认保持分析运行 - 永远不会意外地静默数据。
从第一天起就具有群体意识
代理类型、团队名称和父会话 ID 通过 AsyncLocalStorage 自动附加到每个事件 — 无需手动传播。
优雅关机
shutdownDatadog() 之前被调用 process.exit() 刷新内存中的批处理,以便会话的最后事件永远不会丢失。