# 遥测与分析系统完全解析

本章解析 Claude Code 中最敏感的子系统之一——遥测系统如何收集用户行为数据、进行 PII 脱敏处理，并通过多级采样和动态配置平衡产品分析需求与用户隐私保护。

---

> **🌍 行业背景**：遥测系统是所有商业开发者工具的标配，但各家在透明度和隐私保护力度上差异显著。**VS Code** 的遥测系统是行业标杆——完整的 [telemetry 文档](https://code.visualstudio.com/docs/getstarted/telemetry)详细列出了收集的每类数据，提供 GUI 开关（`telemetry.telemetryLevel`），并且源码完全开源可审计。**GitHub Copilot** 在 2023 年因遥测问题遭受质疑后，增加了组织级策略控制（`copilot_telemetry_disabled`）。**Cursor** 使用 PostHog 进行产品分析，通过 Sentry 收集错误日志，但缺乏公开的遥测数据目录。**Aider** 默认发送匿名使用统计到 MixPanel，可通过 `--no-analytics` 完全关闭。**Codex（OpenAI）** 较为简洁，遥测数据主要通过 API 调用日志收集。Claude Code 的遥测设计在技术层面（类型级 PII 防护、双管线隔离、远程采样控制）处于行业领先水平，但在用户透明度方面（缺乏面向用户的遥测数据目录、默认 Opt-Out 而非 Opt-In）落后于 VS Code。

---

## 本章导读

遥测与分析系统是 Claude Code 2.1.88 中最敏感的子系统之一。它负责收集用户行为数据、工具使用指标、错误日志等信息，经过脱敏处理后发送到 Anthropic 的服务端进行分析。这个系统直接关系到用户隐私，因此既需要理解其技术实现，也需要审视其隐私保护措施。

**技术比喻（OS 视角）**：遥测系统像操作系统中的**审计子系统（auditd / ETW）**——在应用运行时默默记录事件（event），经过过滤管线（PII 脱敏 + 采样），最终输出到多个接收端（Datadog + 第一方日志）。GrowthBook 则相当于远程策略服务器（Group Policy），控制哪些特性开启、哪些事件采集。

> 💡 **通俗理解**：遥测系统像**汽车的行车记录仪**——默默记录使用数据（事件），经过脱敏处理（隐私过滤，比如模糊车牌号），传回总部分析（上报到 Datadog 和 Anthropic）。记录仪有开关（隐私级别设置），也有远程更新（GrowthBook 动态配置）。

## 文件结构

`src/services/analytics/` 目录共 9 个文件，**4,040 行代码**（`wc -l` 精确核实 · 下表为每文件精确行数）：

| 文件 | 行数 | 职责 |
|------|------|------|
| `index.ts` | 173 | 公共 API——logEvent/logEventAsync + 启动前事件队列 |
| `sink.ts` | 114 | 事件路由——将事件分发到 Datadog 和 1P 两条管线 |
| `metadata.ts` | 973 | 元数据丰富——给每个事件附加设备/用户/环境信息 |
| `firstPartyEventLogger.ts` | 449 | 第一方事件记录——OpenTelemetry Logger + 采样 |
| `firstPartyEventLoggingExporter.ts` | 806 | 第一方事件导出器——批量发送 + 重试 + 离线缓存 |
| `datadog.ts` | 307 | Datadog RUM 日志——事件白名单 + 批量发送 |
| `growthbook.ts` | 1,155 | GrowthBook 集成——特性门控 + A/B 实验 + 远程配置 |
| `config.ts` | 38 | 全局禁用判断——测试/第三方提供者/隐私级别 |
| `sinkKillswitch.ts` | 25 | 紧急关闭开关——独立于 config 的远程 kill-switch |
| **合计** | **4,040** | |

## 1. 事件生命周期

### 1.1 三通道架构

Claude Code 的数据上报并非只有 `logEvent()` 这一条路——实际存在**三个独立通道**，互为备份：

```
┌───────────────────────────────────────────────────────────────┐
│                    三通道遥测架构                               │
├──────────────┬──────────────────┬────────────────────────────┤
│ ① API Header │ ② 1P Event Log   │ ③ Datadog RUM              │
│ 嵌入系统提示词 │ OpenTelemetry →  │ 客户端 Token 硬编码         │
│ 无法单独关闭   │ BigQuery         │ 白名单过滤 30+ 事件类型      │
│ (attribution) │ (protobuf 格式)  │ (pubbbf48e6d...)           │
└──────────────┴──────────────────┴────────────────────────────┘
```

- **通道 ①（API Header）**：每次 API 请求都携带 attribution header，嵌入在系统提示词上下文中。默认开启但不是不可关闭：可通过环境变量 `CLAUDE_CODE_ATTRIBUTION_HEADER=false` 或远程 gate `tengu_attribution_header` 关闭（源码 `src/constants/system.ts:53`）。这是最基本的使用追踪。
- **通道 ② + ③**：可通过隐私级别设置关闭。用户侧实际入口是两个环境变量（`privacyLevel.ts:14-24`）：
  - `DISABLE_TELEMETRY=1` → 隐私级别设为 `no-telemetry`，完全关闭
  - `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` → 隐私级别设为 `essential-traffic`，保留关键流量
  - `isTelemetryDisabled()`（`privacyLevel.ts:42`）只关闭 Datadog/1P/GrowthBook 相关管线，不影响通道 ①——因此"完全关闭遥测"需要分别处理两条路径。

事件在通道 ② 和 ③ 中的处理经过三层管线：

```
┌──────────────┐    ┌──────────────┐    ┌──────────────────────────┐
│ 事件产生      │ →  │ Sink 路由     │ →  │ 两条管线                  │
│ logEvent()   │    │ 采样+分发     │    │ ② 1P Exporter (OpenTel)  │
│ logEventAsync│    │              │    │ ③ Datadog (白名单过滤)    │
└──────────────┘    └──────────────┘    └──────────────────────────┘
       ↑                   ↑                        ↑
   index.ts           sink.ts           datadog.ts + exporter.ts
```

> **标识符追踪链**：即使用户更换 IP、清除 cookie，以下标识符的组合仍然可以关联同一用户：`deviceId`（随机 32 字节 hex，首次启动时由 `getOrCreateUserID()` 生成，写入全局配置文件 `~/.claude/config.json`——**不在 keychain**，删除配置文件即重建；源码 `src/utils/config.ts:1757`）+ `accountUuid`（账号级）+ 仓库指纹（repo remote URL 归一化后 SHA256 取前 16 位，**与 attribution fingerprint 分开**）+ 环境指纹（20+ 种终端类型、30+ 种云平台检测）。了解这一点对做出知情的隐私选择很重要。

### 1.2 启动前事件队列

`index.ts` 中最关键的设计是**启动前事件队列**——在 analytics sink 初始化之前产生的事件不会丢失：

```typescript
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  // 幂等
  sink = newSink

  if (eventQueue.length > 0) {
    const queuedEvents = [...eventQueue]
    eventQueue.length = 0

    // 异步排空，避免阻塞启动路径
    queueMicrotask(() => {
      for (const event of queuedEvents) {
        if (event.async) {
          void sink!.logEventAsync(event.eventName, event.metadata)  // 原 logEventAsync 入队的走异步路径
        } else {
          sink!.logEvent(event.eventName, event.metadata)             // 原 logEvent 入队的走同步路径
        }
      }
    })
  }
}
```

使用 `queueMicrotask` 而非直接同步排空——这确保了排空操作不会阻塞 `attachAnalyticsSink()` 的返回，让调用者可以立即继续执行后续的启动步骤。

**⚠️ 精确理解 `queueMicrotask` 的语义**：`queueMicrotask` 调度的任务在**当前事件循环 tick 的微任务阶段**执行，而非下一个 tick。这意味着它不会阻塞当前调用栈的返回（`attachAnalyticsSink` 立即返回），但排空操作仍然会在当前 tick 内完成，延迟后续的宏任务（如 I/O 回调、`setTimeout` 回调）。如果启动前积压了大量事件，微任务排空可能仍会产生可观的延迟——它只是将阻塞从"同步阻塞调用者"变成了"微任务阻塞后续宏任务"。若要真正将排空操作推迟到下一个事件循环 tick（完全不影响当前 tick 的其余任务），需要使用 `setTimeout(fn, 0)` 或 Node.js 的 `setImmediate`。在 Claude Code 的实际场景中，启动前队列通常只有少量事件，因此 `queueMicrotask` 的选择在实践中是合理的——但在理论上它并非"零延迟"。

### 1.3 类型级 PII 防护

`index.ts` 定义了两个标记类型来防止意外记录敏感数据：

```typescript
// 通用脱敏标记——用于确认字符串不包含代码或文件路径
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never

// PII 标签标记——用于需要进入特权列的数据
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED = never
```

这两个类型都是 `never`，意味着你不能直接赋值，必须显式 `as` 转换：

```typescript
// 错误用法（编译报错）
logEvent('test', { path: someFilePath })

// 正确用法（需要开发者确认）
logEvent('test', { 
  path: someFilePath as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 
})
```

这个冗长的类型名是**有意为之**——每次写 `as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS` 时，开发者被迫阅读名字中的提醒，形成一种"类型级 code review"。

> **📚 课程关联**：这种利用类型系统强制安全审查的技巧，是《编程语言》和《编译原理》课程中**类型安全（Type Safety）**理念的应用。`never` 类型在类型论中是**底类型（Bottom Type）**——没有值可以赋给它，因此只能通过 `as` 类型断言绕过，而每次 `as` 都是一个显式的"我知道我在做什么"声明。

**⚠️ 重要限制：这是流程层防护，不是技术层防护。** 需要明确的是，`as` 断言在 TypeScript 中是最轻量级的类型逃逸手段——它在 JavaScript 运行时**完全消失**，提供**零运行时保护**。与 Rust 的 `unsafe` 块相比，差距是本质性的：

| 维度 | Rust `unsafe` | TypeScript `as` |
|------|---------------|-----------------|
| 编译/运行时效果 | **放宽编译器检查**（允许裸指针解引用、跨 FFI 调用、修改可变静态等），本身不改运行时行为；但写错的 unsafe 代码会在运行时真实触发未定义行为 | 编译后完全消失，零运行时语义 |
| 审查可见性 | `unsafe` 关键字在代码库中罕见，grep 即可定位所有使用点 | `as` 在 TypeScript 代码库中无处不在，审查者容易产生"断言疲劳" |
| 工具链支持 | clippy 可统计和追踪所有 `unsafe` 使用点 | 没有标准工具自动审计所有 `as AnalyticsMetadata_*` 的使用是否真的经过了人工验证 |
| 绕过难度 | 需要在代码中显式写 `unsafe {}`，在 PR 审查中高度醒目 | 一个 `as` 就能绕过，比 `// eslint-disable-next-line` 还不引人注目 |

因此，这套机制的实际效果完全取决于团队的 code review 纪律——如果审查者不逐一检查 `as AnalyticsMetadata_*` 的使用场景，类型系统的"提醒"就形同虚设。更进一步，如果有人从纯 JavaScript 层面（而非 TypeScript）调用 `logEvent`，类型防护完全失效。

真正利用类型系统做安全防护的工业级实现是 **branded types + 工厂函数**模式（如 Google 的 Trusted Types、Meta 的 Opaque Types）——它们确保只有经过验证逻辑的值才能通过类型检查，不允许 `as` 绕过。Claude Code 的 `never` + `as` 方案更像是一个**轻量级开发者提醒机制**，在有严格审查文化的团队中有效，但不具备类型系统层面的数学保证。

### 1.4 _PROTO_ 字段隔离

```typescript
export function stripProtoFields<V>(
  metadata: Record<string, V>,
): Record<string, V> {
  let result: Record<string, V> | undefined
  for (const key in metadata) {
    if (key.startsWith('_PROTO_')) {
      if (result === undefined) {
        result = { ...metadata }
      }
      delete result[key]
    }
  }
  return result ?? metadata
}
```

以 `_PROTO_` 为前缀的字段只允许进入第一方特权列（PII-tagged proto columns），所有其他目标（Datadog）在路由前都会被剥离。这是"单点剥离"设计——只需要在 `sink.ts` 中调用一次 `stripProtoFields`，就能确保 PII 数据不会泄露到非特权存储。

## 2. 事件路由（Sink）

### 2.1 路由逻辑

`sink.ts` 是事件路由的核心：

```typescript
function logEventImpl(eventName: string, metadata: LogEventMetadata): void {
  // 1. 采样决策
  const sampleResult = shouldSampleEvent(eventName)
  if (sampleResult === 0) return  // 被采样丢弃

  const metadataWithSampleRate = sampleResult !== null
    ? { ...metadata, sample_rate: sampleResult }
    : metadata

  // 2. Datadog 管线（脱敏后）
  if (shouldTrackDatadog()) {
    void trackDatadogEvent(eventName, stripProtoFields(metadataWithSampleRate))
  }

  // 3. 第一方管线（完整数据，含 _PROTO_ 字段）
  logEventTo1P(eventName, metadataWithSampleRate)
}
```

注意第 2 步和第 3 步的差异：
- Datadog 收到的是 `stripProtoFields()` 处理后的数据——**不包含** PII 标签字段
- 第一方日志收到的是完整数据——**包含** `_PROTO_*` 字段，由 exporter 负责将它们路由到特权列

### 2.2 Datadog 开关

```typescript
function shouldTrackDatadog(): boolean {
  if (isSinkKilled('datadog')) return false  // 紧急关闭
  if (isDatadogGateEnabled !== undefined) return isDatadogGateEnabled
  try {
    return checkStatsigFeatureGate_CACHED_MAY_BE_STALE(DATADOG_GATE_NAME)
  } catch {
    return false  // 异常时默认关闭
  }
}
```

三层开关：kill-switch → 启动时初始化的值 → GrowthBook 缓存值。失败时默认**关闭** Datadog 追踪——这是正确的安全默认。

> **命名注记**：函数名 `checkStatsigFeatureGate_CACHED_MAY_BE_STALE` 是遗留命名，Claude Code 当前使用的是 GrowthBook（而非 Statsig）。该命名保留是出于旧 API 兼容，内部实现已指向 GrowthBook 的特性门控查询。

## 3. Datadog 日志系统

### 3.1 事件白名单

`datadog.ts` 第 19-64 行定义了一个严格的事件白名单：

```typescript
const DATADOG_ALLOWED_EVENTS = new Set([
  'tengu_api_error',
  'tengu_api_success',
  'tengu_cancel',
  'tengu_exit',
  'tengu_init',
  'tengu_started',
  'tengu_tool_use_error',
  'tengu_tool_use_success',
  'tengu_voice_recording_started',
  'tengu_voice_toggled',
  // ... 共 37 个事件（`datadog.ts:19-64`，`grep -c "'tengu_"` 精确核实）
])
```

只有白名单中的事件才会发送到 Datadog——这是一个"许可列表"策略，默认拒绝。

### 3.2 Tag 字段提取

```typescript
const TAG_FIELDS = [
  'arch', 'clientType', 'errorType', 'http_status_range',
  'http_status', 'kairosActive', 'model', 'platform', 'provider',
  'skillMode', 'subscriptionType', 'toolName', 'userBucket', 'userType',
  'version', 'versionBase',
]
```

（共 16 个字段，源码 `datadog.ts:66-83`）

这些字段会被提取为 Datadog tags（用于过滤和聚合），其余字段保留在日志 body 中。

### 3.3 批量发送

```typescript
const DEFAULT_FLUSH_INTERVAL_MS = 15000  // 15 秒刷新
const MAX_BATCH_SIZE = 100               // 最大批量 100 条
const NETWORK_TIMEOUT_MS = 5000          // 5 秒超时

const DATADOG_LOGS_ENDPOINT = 'https://http-intake.logs.us5.datadoghq.com/api/v2/logs'
const DATADOG_CLIENT_TOKEN = 'pubbbf48e6d78dae54bceaa4acf463299bf'
```

注意：Datadog Client Token 是**公开的**（`pub...` 前缀表示这是一个只能写入的客户端 Token），但它硬编码在源码中，这意味着任何人都可以向这个 Datadog 账户写入虚假日志。

## 4. 第一方事件日志系统

### 4.1 OpenTelemetry 集成

`firstPartyEventLogger.ts` 使用 OpenTelemetry 的 LoggerProvider + BatchLogRecordProcessor：

```typescript
import { BatchLogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs'

const BATCH_CONFIG_NAME = 'tengu_1p_event_batch_config'
type BatchConfig = {
  scheduledDelayMillis?: number   // 批量间隔
  maxExportBatchSize?: number     // 单批最大事件数
  maxQueueSize?: number           // 队列最大容量
  skipAuth?: boolean              // 跳过认证（测试用）
  maxAttempts?: number            // 最大重试次数
  path?: string                   // API 路径
  baseUrl?: string                // API 基地址
}
```

所有批量参数都通过 GrowthBook 远程配置（`tengu_1p_event_batch_config`），可以不发版就调整发送策略。

### 4.2 事件采样

```typescript
export function shouldSampleEvent(eventName: string): number | null {
  const config = getEventSamplingConfig()
  const eventConfig = config[eventName]

  if (!eventConfig) return null       // 未配置 = 100% 记录
  const sampleRate = eventConfig.sample_rate

  // 类型与范围校验：非 number、负数、> 1 都视为无效配置，回退到 100% 记录
  if (typeof sampleRate !== 'number' || sampleRate < 0 || sampleRate > 1) {
    return null
  }

  if (sampleRate >= 1) return null    // 100% 记录
  if (sampleRate <= 0) return 0       // 0% 记录（丢弃）

  // 随机采样
  return Math.random() < sampleRate ? sampleRate : 0
}
```

采样配置也是远程的（`tengu_event_sampling_config`），允许对高频事件（如 `tengu_tool_use_success`）进行降采样以减少数据量和成本。被采样保留的事件会附加 `sample_rate` 字段，分析时可以根据此字段进行加权还原（`count / sample_rate` 还原真实事件量，即霍维茨-汤普森估计量原理）。

**⚠️ 朴素 Bernoulli 采样的局限性**：`Math.random() < sampleRate` 是**逐事件独立采样**（Bernoulli sampling）——每个事件独立抛硬币决定去留。这意味着同一用户在同一会话中的事件可能**部分被保留、部分被丢弃**，导致分析时无法重建完整的用户行为序列。例如，一个包含 `tool_use_start → tool_use_success → api_call` 三个事件的操作链，在 50% 采样率下可能只保留了第一个和第三个事件，中间的因果关系丢失。

业界更成熟的做法是**确定性哈希采样**（如 Datadog APM 的 trace sampling）——基于会话 ID 或 trace ID 的哈希值做采样决策，确保同一会话/trace 的所有事件要么全部保留、要么全部丢弃。这样分析时可以完整重建任意被采样保留的用户行为序列。Claude Code 选择 Bernoulli 采样的原因可能是实现简单且对聚合统计（"昨天有多少次 tool_use_success？"）足够准确，但它牺牲了单用户行为分析的能力——这对于调试特定用户报告的问题是一个实际缺陷。

### 4.3 Exporter 弹性设计

`firstPartyEventLoggingExporter.ts` 实现了多项弹性保障：

```typescript
export class FirstPartyEventLoggingExporter implements LogRecordExporter {
  // 追加式日志——并发安全
  // 二次方退避重试
  // 超过 maxAttempts 后丢弃
  // 任何 export 成功时立即重试队列中的失败事件（说明端点恢复了）
  // 大事件集分块发送
  // 401 时尝试无认证回退
}
```

离线缓存使用 JSONL 文件存储在 `~/.claude/telemetry/` 目录：

```typescript
const BATCH_UUID = randomUUID()       // 进程唯一 ID，隔离不同运行的失败文件
const FILE_PREFIX = '1p_failed_events.'

function getStorageDir(): string {
  return path.join(getClaudeConfigHomeDir(), 'telemetry')
}
```

## 5. GrowthBook 特性门控系统

`growthbook.ts` 是整个 analytics 目录中最大的单文件（~1000 行，占目录代码量的 25%），它不仅仅是一个特性开关——它是 Anthropic 对 Claude Code 进行**远程行为控制**的核心基础设施。通过 GrowthBook，Anthropic 可以在不发版的情况下：启用/禁用功能、进行 A/B 实验分桶、动态调整运行参数、紧急关闭出问题的子系统。理解这个模块，就是理解 "Anthropic 对 Claude Code 的运营控制力边界"。

### 5.1 架构概览：Remote Eval 模式

Claude Code 使用 GrowthBook 的 `remoteEval: true` 模式——所有特性值由服务端预计算，客户端不做本地规则评估：

```typescript
const thisClient = new GrowthBook({
  apiHost: baseUrl,
  clientKey,
  attributes,
  remoteEval: true,
  cacheKeyAttributes: ['id', 'organizationUUID'],
  // 带认证头请求（企业代理场景需要）
  ...(authHeaders.error ? {} : { apiHostRequestHeaders: authHeaders.headers }),
})
```

这个架构选择有明确的 trade-off：
- **优势**：服务端可以使用任意复杂的定位规则（包括需要保密的分桶逻辑），客户端不需要知道规则细节
- **代价**：每次初始化和刷新都需要网络请求；需要大量本地缓存和降级逻辑来应对离线场景

### 5.2 用户属性与定位维度

`growthbook.ts` 向 GrowthBook 发送以下用户属性用于目标定位：

```typescript
export type GrowthBookUserAttributes = {
  id: string                    // 用户 ID（实际使用 deviceId）
  sessionId: string             // 会话 ID
  deviceID: string              // 设备 ID
  platform: 'win32' | 'darwin' | 'linux'
  apiBaseUrlHost?: string       // API 主机（企业代理场景）
  organizationUUID?: string     // 组织 UUID
  accountUUID?: string          // 账户 UUID
  userType?: string             // 用户类型（ant/external）
  subscriptionType?: string     // 订阅类型
  rateLimitTier?: string        // 限流等级
  firstTokenTime?: number       // 首次使用时间
  email?: string                // 邮箱
  appVersion?: string           // 应用版本
  github?: GitHubActionsMetadata // GitHub Actions 信息
}
```

这些属性支撑了多维度定位策略：
- **内部灰度**：基于 `userType === 'ant'` 先对 Anthropic 内部员工发布新功能
- **A/B 实验分桶**：基于 `id`（设备 ID）进行确定性分桶，同一设备始终看到相同变体
- **订阅分层**：基于 `subscriptionType` 和 `rateLimitTier` 对不同付费层级提供差异化功能
- **企业代理适配**：`apiBaseUrlHost` 让非 Anthropic 直连的企业代理部署也能被精确定位
- **平台特定配置**：基于 `platform` 提供平台差异化行为

注意 `id` 字段实际使用的是 `deviceId` 而非用户账户 ID——这意味着分桶是**设备级别**的，同一用户在不同设备上可能看到不同的实验变体。

### 5.3 特性门控的完整列表

通过源码搜索，可以找到 Claude Code 中通过 GrowthBook 远程控制的所有特性门控。以下是从源码提取的主要 gate/config 名称：

| 门控/配置名 | 用途 | 调用方式 |
|------------|------|---------|
| `tengu_log_datadog_events` | Datadog 遥测开关 | `CACHED_MAY_BE_STALE` |
| `tengu_frond_boric` | Sink 紧急关闭开关 | `CACHED_MAY_BE_STALE` |
| `tengu_event_sampling_config` | 事件采样率配置 | `CACHED_MAY_BE_STALE` |
| `tengu_1p_event_batch_config` | 1P 事件批量发送配置 | `CACHED_MAY_BE_STALE` |
| `tengu_amber_quartz_disabled` | 语音模式禁用开关 | `CACHED_MAY_BE_STALE` |
| `tengu_scratch` | Coordinator 模式开关 | `CACHED_MAY_BE_STALE` |
| `tengu_session_memory` | 会话记忆功能开关 | `CACHED_MAY_BE_STALE` |
| `tengu_lodestone_enabled` | Deep Link 注册开关 | `CACHED_MAY_BE_STALE` |
| `tengu_surreal_dali` | 定时远程 Agent 功能 | `CACHED_MAY_BE_STALE` |
| `tengu_birch_trellis` | Bash 权限控制 | `CACHED_MAY_BE_STALE` |
| `tengu_slate_prism` | CLI 打印行为 | `CACHED_MAY_BE_STALE` |
| `tengu_otk_slot_v1` | OTK Slot 功能 | `CACHED_MAY_BE_STALE` |
| `tengu_max_version_config` | 最大版本控制（强制更新） | `BLOCKS_ON_INIT` |

所有混淆名称（如 `tengu_amber_quartz`、`tengu_frond_boric`）都使用了"项目代号 + 两个随机词"的命名约定，这使得外部观察者无法从配置名推断功能用途——这是一种安全措施，但也增加了审计难度。

### 5.4 四级 API 与一致性保证

GrowthBook 模块暴露了四个不同一致性保证级别的读取 API，供不同场景选用：

```
┌───────────────────────────────────┐  一致性：强 ←──────────────→ 弱
│ getDynamicConfig_BLOCKS_ON_INIT   │  阻塞等待初始化完成，保证最新值
│ checkGate_CACHED_OR_BLOCKING      │  缓存 true 则快速返回，否则阻塞
│ checkSecurityRestrictionGate      │  等待重新初始化，然后读缓存
│ *_CACHED_MAY_BE_STALE             │  纯读缓存，可能返回旧值
└───────────────────────────────────┘  性能：差 ←──────────────→ 好
```

这个设计揭示了一个经典的**可用性 vs 一致性**权衡：

- **`_CACHED_MAY_BE_STALE`**：所有启动关键路径和同步上下文使用此 API。它从磁盘缓存（`~/.claude.json` 中的 `cachedGrowthBookFeatures`）或内存中的 `remoteEvalFeatureValues` Map 立即返回，不发网络请求。值可能是上一个进程写入的旧值。
- **`_BLOCKS_ON_INIT`**：会等待 GrowthBook 客户端完成初始化（最多 5 秒超时），确保获取最新值。用于"必须获取最新配置才能正确运行"的场景（如最大版本控制）。
- **`checkGate_CACHED_OR_BLOCKING`**：巧妙的混合策略——如果磁盘缓存已经是 `true`，立即返回（信任缓存的正面结果）；如果是 `false` 或缺失，则走阻塞路径获取最新值。适用于"宁可多等也不要错误拒绝"的权限类门控。
- **`checkSecurityRestrictionGate`**：安全门控专用——如果正在重新初始化（如登录切换后），等待初始化完成再读取，避免使用旧用户的权限值。

**竞态条件值得注意**：`_CACHED_MAY_BE_STALE` 不等待 `reinitializingPromise`——这意味着在登录切换的短暂窗口期内，通过此 API 读取的值可能属于**旧用户**。后缀 `_MAY_BE_STALE` 正是对这一风险的诚实标注。对于非安全关键的功能（如 UI 配置），这个 trade-off 是可接受的；但对于权限相关的决策，必须使用 `checkSecurityRestrictionGate`。

### 5.5 实验曝光日志与去重

GrowthBook 在检查特性值时会记录"曝光"（exposure）——用户看到了哪个实验变体：

```typescript
type StoredExperimentData = {
  experimentId: string
  variationId: number
  inExperiment?: boolean
  hashAttribute?: string
  hashValue?: string
}
const experimentDataByFeature = new Map<string, StoredExperimentData>()
```

曝光日志的处理有两个关键设计：

1. **会话级去重**：`loggedExposures` Set 确保同一特性在同一会话中只记录一次曝光，避免高频调用路径（如渲染循环中的 `isAutoMemoryEnabled`）产生大量重复曝光事件
2. **延迟曝光**：如果特性在 GrowthBook 初始化完成之前被访问（通过 `_CACHED_MAY_BE_STALE`），其 feature key 被加入 `pendingExposures` Set，待初始化完成后补发曝光日志

```typescript
// 初始化后补发所有待处理曝光
if (hadFeatures) {
  for (const feature of pendingExposures) {
    logExposureForFeature(feature)
  }
  pendingExposures.clear()
}
```

### 5.6 五级解析与降级策略

GrowthBook 的值解析遵循严格的降级链：

```
环境变量覆盖（CLAUDE_INTERNAL_FC_OVERRIDES，其中 FC = Feature Config，仅 ant）
    ↓ 未命中
本地配置覆盖（~/.claude.json growthBookOverrides，仅 ant）
    ↓ 未命中
内存缓存（remoteEvalFeatureValues Map，本进程 init 后填充）
    ↓ 未命中
磁盘缓存（~/.claude.json cachedGrowthBookFeatures，跨进程持久化）
    ↓ 未命中
默认值（调用者提供的 defaultValue）
```

磁盘缓存通过 `syncRemoteEvalToDisk()` 维护——每次成功处理 payload 后，完整覆写（非合并）磁盘缓存，确保服务端删除的特性也从磁盘中消失。

**环境变量覆盖**是为内部测试和 eval 框架设计的：
```bash
# 测试时强制启用/禁用特定特性
CLAUDE_INTERNAL_FC_OVERRIDES='{"tengu_session_memory": true}' claude
```

### 5.7 重新初始化与认证变更

当用户登录/登出时，GrowthBook 需要完全销毁和重建客户端（因为 `apiHostRequestHeaders` 在客户端创建后无法更新）：

```typescript
export function refreshGrowthBookAfterAuthChange(): void {
  resetGrowthBook()       // 销毁旧客户端，清空所有缓存
  refreshed.emit()        // 立即通知订阅者重新读取（降级到磁盘缓存）
  reinitializingPromise = initializeGrowthBook()
    .catch(error => { logError(toError(error)); return null })
    .finally(() => { reinitializingPromise = null })
}
```

`reinitializingPromise` 是保护安全门控的关键——`checkSecurityRestrictionGate` 会 `await` 这个 Promise，确保在认证切换完成前不会返回旧用户的权限值。

### 5.8 周期刷新

对于长时间运行的会话，GrowthBook 设置了周期性刷新：

```typescript
const GROWTHBOOK_REFRESH_INTERVAL_MS =
  process.env.USER_TYPE !== 'ant'
    ? 6 * 60 * 60 * 1000   // 外部用户：6 小时
    : 20 * 60 * 1000        // 内部员工：20 分钟
```

内部员工的刷新频率是外部用户的 18 倍——这使得新特性可以在 20 分钟内对内部用户生效，而不需要重启 Claude Code。刷新使用"轻量刷新"（`refreshGrowthBookFeatures`）而非销毁重建——保留客户端状态，只重新拉取特性值。

`refreshInterval.unref?.()` 确保这个定时器不会阻止 Node.js 进程自然退出。

### 5.9 GrowthBook 集成的架构问题

**供应商锁定风险**：1000 行的适配代码中，大量是在处理 GrowthBook SDK 的问题：
- API 返回 `value` 而非 `defaultValue` 的格式不匹配（需要 `processRemoteEvalPayload` 中的 transform workaround）
- SDK 的 `evalFeature()` 在 `remoteEval` 模式下不能正确使用预评估值（需要自建 `remoteEvalFeatureValues` 缓存）
- SDK 的 `setForcedFeatures` 在 `remoteEval` 模式下不可靠

这些 workaround 暗示 GrowthBook SDK 对 `remoteEval` 模式的支持并不成熟，而 Claude Code 对此模式有深度依赖。如果 GrowthBook 未来的 SDK 更新改变了这些行为，这些 workaround 可能会失效。

**为什么选 GrowthBook 而非 LaunchDarkly/Unleash？** 源码中没有直接回答这个问题，但可以推断：GrowthBook 是开源的（MIT 协议），这对 Anthropic 意味着可审计性和不被锁定于闭源供应商。LaunchDarkly 的 SDK 更成熟但闭源且昂贵；Unleash 是另一个开源选项但 remoteEval 支持更弱。选择开源方案的代价就是需要自己填补 SDK 的不足——这正是这 1000 行代码的本质。

**fail-closed 的运维代价**：如果 GrowthBook 服务本身不可达（如 CDN 故障），所有使用 `_BLOCKS_ON_INIT` 的门控会在 5 秒超时后回退到磁盘缓存或默认值。对于 Datadog 开关（`tengu_log_datadog_events`），默认值是 `false`——这意味着恰恰在最需要遥测数据来诊断问题时，遥测管道自己先关闭了。这是 fail-closed（安全优先）vs fail-open（可观测性优先）的经典张力。

## 6. 元数据丰富

### 6.1 MCP 工具名脱敏

`metadata.ts` 第 70-77 行对 MCP 工具名进行脱敏：

```typescript
export function sanitizeToolNameForAnalytics(toolName: string) {
  if (toolName.startsWith('mcp__')) {
    return 'mcp_tool'  // 所有 MCP 工具统一为 'mcp_tool'
  }
  return toolName      // 内置工具保留原名
}
```

MCP 工具名格式为 `mcp__<server>__<tool>`，其中 server 名称可能暴露用户配置（如 `mcp__my-company-api__query`），属于中等 PII。脱敏后统一为 `mcp_tool`。

但有例外——某些受信任的来源可以保留完整名称：

```typescript
export function isAnalyticsToolDetailsLoggingEnabled(
  mcpServerType: string | undefined,
  mcpServerBaseUrl: string | undefined,
): boolean {
  if (process.env.CLAUDE_CODE_ENTRYPOINT === 'local-agent') return true  // Cowork
  if (mcpServerType === 'claudeai-proxy') return true   // claude.ai 官方代理
  if (mcpServerBaseUrl && isOfficialMcpUrl(mcpServerBaseUrl)) return true  // 官方 MCP
  return false
}
```

## 7. 全局禁用条件

`config.ts` 第 19-27 行定义了全局禁用条件：

```typescript
export function isAnalyticsDisabled(): boolean {
  return (
    process.env.NODE_ENV === 'test' ||
    isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
    isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
    isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
    isTelemetryDisabled()
  )
}
```

禁用场景：
1. **测试环境**：`NODE_ENV === 'test'`
2. **第三方云提供者**：Bedrock / Vertex / Foundry 用户不向 Anthropic 发送遥测
3. **隐私级别**：`no-telemetry` 或 `essential-traffic` 设置

### 7.1 Sink Kill-Switch

```typescript
// 混淆名称：tengu_frond_boric = 分析管线紧急开关
const SINK_KILLSWITCH_CONFIG_NAME = 'tengu_frond_boric'

export function isSinkKilled(sink: SinkName): boolean {
  const config = getDynamicConfig_CACHED_MAY_BE_STALE<
    Partial<Record<SinkName, boolean>>
  >(SINK_KILLSWITCH_CONFIG_NAME, {})
  return config?.[sink] === true
}
```

这是一个独立于 `isAnalyticsDisabled()` 的远程紧急开关。如果 Datadog 出现问题（如费用异常），可以通过 GrowthBook 远程关闭 Datadog sink 而不影响第一方日志——或者反过来。

## 8. 隐私影响深度分析

### 8.1 收集了什么

基于源码分析，以下数据会被收集：

**设备信息**：平台、架构、WSL 版本、Linux 发行版、VCS 类型
**账户信息**：用户 ID（UUID）、设备 ID、组织 UUID、账户 UUID、订阅类型、邮箱
**使用行为**：工具调用（名称+结果）、API 调用（模型+状态码）、会话时长、命令使用
**仓库信息**：`getRepoRemoteHash()` 获取的仓库远程地址哈希（注意是哈希，非明文）
**实验数据**：GrowthBook 特性值、实验变体、曝光事件

### 8.2 脱敏措施

1. **类型级防护**：`AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS` 强制开发者确认每个字符串字段不包含敏感数据
2. **MCP 工具名脱敏**：默认将 `mcp__<server>__<tool>` 替换为 `mcp_tool`
3. **仓库地址哈希**：使用 `getRepoRemoteHash()` 而非明文 URL
4. **`_PROTO_` 字段隔离**：PII 数据只进入特权存储列，不会泄露到 Datadog
5. **第三方提供者排除**：Bedrock/Vertex/Foundry 用户完全不发送遥测
6. **全局关闭选项**：`isTelemetryDisabled()` 关闭 Datadog/1P/GrowthBook 管线（即通道 ② + ③）。通道 ①（API attribution header）需要独立设置 `CLAUDE_CODE_ATTRIBUTION_HEADER=false`（或管理员远程 gate）才会关闭；"完全关闭遥测"需要两条路径都处理

### 8.3 隐私风险点

**硬编码 Datadog Token**：`pubbbf48e6d78dae54bceaa4acf463299bf` 硬编码在源码中。虽然这是一个只写 Token（无法读取历史数据），但任何人都可以用它向 Anthropic 的 Datadog 账户注入虚假日志，可能影响内部分析结果。

**邮箱字段**：GrowthBookUserAttributes 中的 `email` 字段是 PII，虽然只发送到 GrowthBook（不进入 Datadog/1P 事件），但如果 GrowthBook 的数据被泄露，用户邮箱就会暴露。

**混淆名称的双刃剑**：`tengu_amber_quartz`（语音）、`tengu_frond_boric`（kill-switch）等混淆名称虽然防止了外部猜测功能含义，但也妨碍了安全审计——审计人员无法从配置名推断其用途。

**"Essential Traffic Only" 的边界含义**：`isEssentialTrafficOnly()` 在源码中定义明确（`privacyLevel.ts:34-36`）——当隐私级别为 `essential-traffic` 时返回 true。注释说明 `essential-traffic` 等级是"ALL nonessential network traffic disabled"（所有非关键网络流量都禁用）。但"non-essential 流量"的完整清单在源码中未集中列出，各子系统各自用此判断决定是否发起请求（如 GrowthBook 刷新、Datadog 上报等）。用户在最高隐私级别下，某些特性门控拉取会退回磁盘缓存或默认值——这在实践中意味着可能看到过时的 feature flag 行为，但不会"无法工作"。

## 批判性分析

### 优点

1. **类型级 PII 防护**：`never` 类型标记是一个实用的开发者提醒机制——冗长的类型名迫使开发者在 `as` 转换时停下来思考。但其实际防护效果取决于团队的 code review 纪律，而非类型系统的强制保证（`as` 断言在运行时完全消失，绕过成本极低）
2. **双管线隔离**：Datadog（脱敏）和 1P（特权列）的分离确保了"最小权限"原则——大多数分析人员只能看到脱敏数据
3. **远程可控**：采样率、批量配置、kill-switch 全部通过 GrowthBook 远程配置，无需发版就能应对突发情况
4. **启动前队列**：避免了"初始化时序"这个遥测系统的经典难题

### 不足

1. **硬编码凭证**：Datadog Client Token 和 API 端点硬编码在源码中，虽然是只写 Token，但仍然是凭证泄露
2. **Opt-Out 而非 Opt-In**：遥测默认开启（除非用户主动设置 `no-telemetry`），这在 GDPR 合规性上存在争议——虽然 CLI 工具不像网站那样受严格约束，但"默认收集"的做法在隐私敏感的开发者社区中可能引起反感
3. **GrowthBook 本身的隐私问题**：向 GrowthBook 发送用户属性（包括邮箱、组织 UUID）本身就是一种数据外传——GrowthBook 作为第三方服务，其数据安全性取决于 GrowthBook 公司的安全实践
4. **离线缓存的潜在风险**：失败事件缓存在 `~/.claude/telemetry/` 的 JSONL 文件中，如果这些文件被其他程序读取（如恶意软件或文件同步工具），可能泄露用户行为数据
5. **缺少数据保留期限声明**：源码中没有发现关于收集数据保留多久、何时删除的逻辑——这应该在隐私政策中明确说明
6. **`tengu_` 前缀的遍布**：几乎所有遥测事件都以 `tengu_` 为前缀（tengu 是 Claude Code 的内部代号），这种命名在源码进入公开渠道后失去了隐蔽性，反而增加了代码理解难度

### 与行业实践的对比

Claude Code 的遥测设计在技术层面是成熟的——类型级 PII 防护、双管线隔离、远程配置都是业界常见的最佳实践组合，OpenTelemetry + Datadog 的双管线架构在 SaaS 产品中已广泛使用。但在透明度方面，它落后于一些竞品：VS Code 的遥测系统有完整的文档说明收集了什么、为什么收集、如何关闭，并通过 `vscode-telemetry-extractor` 工具自动生成遥测数据目录；GitHub Copilot 允许在组织策略级别完全禁用遥测；Aider 提供了简洁的 `--no-analytics` 一键关闭。Claude Code 的遥测配置虽然可以通过环境变量和隐私级别控制，但缺少一个面向用户的、可读的遥测数据目录——这在开源/透明度日益重要的开发者工具市场中是一个需要改进的方向。
