Claude Code 源码分析Claude Code 源码分析
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
  • 概览

    • Claude Code 源码分析
    • 源码统计
    • CodeGraph 图谱
  • 架构

    • 系统架构
    • UML 图表索引
    • 查询引擎
    • 核心流程
    • 消息系统
    • 状态管理
  • 功能模块

    • 工具系统
    • 斜杠命令
    • 服务层
    • MCP 协议
    • Skills 技能
    • 子代理系统
  • 分层深度

    • 入口层
    • UI / Ink 层
    • utils 基础设施
    • 桥接 / 远程
    • 上下文压缩
  • 原理与安全

    • 底层原理
    • 技术难点
    • 权限与安全
    • 内部机制
    • 遥测与分析
  • 深度专题

    • Hooks 系统
    • 插件系统
    • 记忆系统
    • API 通信层
    • Ink 终端 UI
    • 认证系统
    • 构建与发布
    • 术语表
  • 调用分析

    • 调用链分析
    • 核心文件索引
  • 模块详解

    • utils

      • 模块: utils
      • messages · 消息工厂与规范化
      • session-storage · JSONL 会话持久化
      • permissions · 工具权限决策
      • shell-hooks · 用户 Shell Hook 系统
    • components

      • 模块: components
      • REPL · 主屏编排
      • messages · 消息行渲染
      • PermissionRequest · 权限弹窗
      • PromptInput · 底部输入
    • services

      • 模块: services
      • api-claude · Anthropic API 流式与重试
      • mcp-client · MCP 连接与工具调用
      • compact · 上下文压缩与自动触发
      • analytics · GrowthBook、Datadog 与 1P 事件
    • tools

      • 模块: tools
      • tool-interface · Tool 契约与注册表
      • bash-tool · Shell 执行与权限
      • streaming-executor · 流式工具并发调度
      • agent-tool · 子 Agent 委派
    • commands

      • 模块: commands
      • command-registry · commands.ts 注册与分派
      • model-command · /model 模型选择
      • mcp-commands · /mcp 服务器管理
      • compact-memory-commands · /compact 与 /memory
    • ink

      • 模块: ink
      • Ink 渲染管线 · Screen 与终端输出
      • 终端事件 · resize、paste、stdin
      • Ink Hooks · 输入、搜索、终端状态
      • Ink 组件 · Box、Text、ScrollBox 原语
    • hooks

      • 模块: hooks
      • useCanUseTool · 权限 UI 接缝
      • 输入与快捷键 Hook
      • 合并态 Hook(MCP + 本地)
      • notifs 通知 Hook
    • bridge

      • 模块: bridge
      • repl-bridge · REPL 桥初始化与传输
      • bridge-messaging · 桥消息路由与入站处理
      • remote-bridge-core · env-less 核心与守护主循环
      • bridge-permissions-ui · 权限、API 与 TUI
    • cli

      • 模块: cli
      • Structured IO · NDJSON SDK 协议
      • CLI Transports · Session Ingress 传输层
      • CLI Handlers · 子命令懒加载实现
      • Update & Upload · 自更新与串行上传原语
    • screens

      • 模块: screens
      • REPL 屏 · Screen 类型与顶层路由
      • ResumeConversation · 会话恢复选择器
      • Doctor · 安装诊断全屏
    • entrypoints

      • 模块: entrypoints
      • cli-entrypoint · Bootstrap 与快路径
      • sdk-types · core / control / runtime 类型体系
      • mcp-entrypoint · MCP stdio 服务器
      • sandbox-types · 沙箱配置单一真相源
    • skills

      • 模块: skills
      • skills-loading · 磁盘加载与 bundled 注册表
      • bundled-skills · 内置 skill 与 initBundledSkills
      • mcp-skills · MCP prompt 转 skill
      • skill-tool-integration · SkillTool 与命令注册
    • types

      • 模块: types
      • message-types · Message 联合与 content blocks
      • tool-permission-types · Tool、Permission、Command 类型
      • api-sdk-types · API 与 Hooks 协议类型
      • misc-types · ids、plugin、generated 与其余类型
    • tasks

      • 模块: tasks
      • local-agent-task · 本地 Agent 与主会话后台化
      • remote-agent-task · 远程 CCR 与 In-Process Teammate
      • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
      • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybindings

      • 模块: keybindings
      • keybinding-registry · 注册、Provider 与 useKeybinding
      • default-bindings · 默认键位表与平台差异
      • command-bindings · command:* 动态斜杠命令绑定
      • vim-bindings · Vim 模式与 keybindings 边界
    • memdir

      • 模块: memdir
      • memdir-core · 路径、加载与 MEMORY.md
      • memory-extraction · extractMemories 与 SessionMemory
      • memdir-commands · /memory、/remember 与命令集成
    • state

      • 模块: state
      • app-state-core · store、AppState 类型与 Provider
      • app-state-selectors · selectors 与 onChangeAppState
      • teammate-state · 队友视图与 swarm 状态
      • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query

      • 模块: query
      • query config 与 deps · 配置快照与依赖注入
      • query tokenBudget · +500k 自动续跑
      • query transitions · Continue / Terminal 状态机
      • query stopHooks · Stop 事件与 turn 结束编排
  • 模块详解(扩展)

    • messages · 消息工厂与规范化
    • session-storage · JSONL 会话持久化
    • permissions · 工具权限决策
    • shell-hooks · 用户 Shell Hook 系统
    • REPL · 主屏编排
    • messages · 消息行渲染
    • PermissionRequest · 权限弹窗
    • PromptInput · 底部输入
    • api-claude · Anthropic API 流式与重试
    • mcp-client · MCP 连接与工具调用
    • compact · 上下文压缩与自动触发
    • analytics · GrowthBook、Datadog 与 1P 事件
    • tool-interface · Tool 契约与注册表
    • bash-tool · Shell 执行与权限
    • streaming-executor · 流式工具并发调度
    • agent-tool · 子 Agent 委派
    • command-registry · commands.ts 注册与分派
    • model-command · /model 模型选择
    • mcp-commands · /mcp 服务器管理
    • compact-memory-commands · /compact 与 /memory
    • Ink 渲染管线 · Screen 与终端输出
    • 终端事件 · resize、paste、stdin
    • Ink Hooks · 输入、搜索、终端状态
    • Ink 组件 · Box、Text、ScrollBox 原语
    • useCanUseTool · 权限 UI 接缝
    • 输入与快捷键 Hook
    • 合并态 Hook(MCP + 本地)
    • notifs 通知 Hook
    • repl-bridge · REPL 桥初始化与传输
    • bridge-messaging · 桥消息路由与入站处理
    • remote-bridge-core · env-less 核心与守护主循环
    • bridge-permissions-ui · 权限、API 与 TUI
    • Structured IO · NDJSON SDK 协议
    • CLI Transports · Session Ingress 传输层
    • CLI Handlers · 子命令懒加载实现
    • Update & Upload · 自更新与串行上传原语
    • REPL 屏 · Screen 类型与顶层路由
    • ResumeConversation · 会话恢复选择器
    • Doctor · 安装诊断全屏
    • cli-entrypoint · Bootstrap 与快路径
    • sdk-types · core / control / runtime 类型体系
    • mcp-entrypoint · MCP stdio 服务器
    • sandbox-types · 沙箱配置单一真相源
    • skills-loading · 磁盘加载与 bundled 注册表
    • bundled-skills · 内置 skill 与 initBundledSkills
    • mcp-skills · MCP prompt 转 skill
    • skill-tool-integration · SkillTool 与命令注册
    • message-types · Message 联合与 content blocks
    • tool-permission-types · Tool、Permission、Command 类型
    • api-sdk-types · API 与 Hooks 协议类型
    • misc-types · ids、plugin、generated 与其余类型
    • local-agent-task · 本地 Agent 与主会话后台化
    • remote-agent-task · 远程 CCR 与 In-Process Teammate
    • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
    • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybinding-registry · 注册、Provider 与 useKeybinding
    • default-bindings · 默认键位表与平台差异
    • command-bindings · command:* 动态斜杠命令绑定
    • vim-bindings · Vim 模式与 keybindings 边界
    • memdir-core · 路径、加载与 MEMORY.md
    • memory-extraction · extractMemories 与 SessionMemory
    • memdir-commands · /memory、/remember 与命令集成
    • app-state-core · store、AppState 类型与 Provider
    • app-state-selectors · selectors 与 onChangeAppState
    • teammate-state · 队友视图与 swarm 状态
    • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query config 与 deps · 配置快照与依赖注入
    • query tokenBudget · +500k 自动续跑
    • query transitions · Continue / Terminal 状态机
    • query stopHooks · Stop 事件与 turn 结束编排
  • 工具详解

    • tool-interface · Tool 契约与注册表
    • tool-permission-types · Tool、Permission、Command 类型
    • 工具: Bash
    • 工具: PowerShell
    • 工具: Agent
    • 工具: LSP
    • 工具: FileEdit
    • 工具: FileRead
    • 工具: Skill
    • 工具: WebFetch
    • 工具: MCP
    • 工具: SendMessage
    • 工具: FileWrite
    • 工具: Config
    • 工具: Grep
    • 工具: Brief
    • 工具: ExitPlanMode
    • 工具: ToolSearch
    • 工具: NotebookEdit
    • 工具: TaskOutput
    • 工具: WebSearch
    • 工具: ScheduleCron

本章总览

services/analytics/ 提供 Claude Code 的遥测与 feature flag 基础设施:index.ts 无依赖 logEvent 入口 + 启动前队列;sink.ts 路由到 Datadog 与 1P OpenTelemetry;growthbook.ts 拉取 remote eval 并缓存到 disk。**本章要求你能从任意 logEvent("tengu_*") 调用反查到 sink 采样、Datadog allowlist 与 _PROTO_ PII 分离策略。

学完本章你应该能

  • 说明 attachAnalyticsSink 队列 drain 与 idempotent 语义
  • 解释 stripProtoFields 为何在 Datadog 前剥离 PROTO*
  • 描述 getFeatureValue_CACHED_MAY_BE_STALE 的缓存层级
  • 理解 shouldSampleEvent 与 tengu_event_sampling_config
  • 能在启动路径定位 initializeAnalyticsSink 与 initializeAnalyticsGates

核心概念(先读懂这些)

index.ts 故意零依赖

analytics/index.ts 不 import sink 或 growthbook,避免 import cycle。业务模块只 logEvent;main.tsx setupBackend 时 initializeAnalyticsSink attach 真实后端。启动前事件进 eventQueue,attach 后 queueMicrotask 异步 drain,不阻塞 CLI 冷启动。

双 sink:Datadog 是子集,1P 是全量

Datadog 仅 production + firstParty provider + allowlist 内事件名;且 metadata 经 stripProtoFields 去掉 PROTO(PII 特权列)。1P exporter 保留完整 payload,把 PROTO hoist 到 proto 字段。一条 logEvent 可能只到 1P、或两者皆有,取决于 gate 与 allowlist。

GrowthBook CACHED_MAY_BE_STALE 的契约

热路径(render loop、autocompact 判断)禁止 blocking network。getFeatureValue_CACHED_MAY_BE_STALE 读 env override → config override → 内存 remoteEval map → disk cachedGrowthBookFeatures。值可能跨进程陈旧;安全门控用 checkGate_CACHED_OR_BLOCKING 或 checkSecurityRestrictionGate 阻塞等待 init。

建议学习步骤

  1. 阅读源码块 A:logEvent 与 attachAnalyticsSink
  2. 阅读源码块 B:stripProtoFields 与类型标记
  3. 阅读源码块 C:sink 路由与 Datadog gate
  4. 阅读源码块 D:getFeatureValue_CACHED_MAY_BE_STALE
  5. 阅读源码块 E:shouldSampleEvent
  6. 阅读源码块 F:trackDatadogEvent allowlist
  7. 在源码树打开 services/analytics/ 对照行号

常见误区

注意

LogEventMetadata 故意不含 string,防 filepath 泄漏;需用 AnalyticsMetadata_I_VERIFIED 断言

注意

checkStatsigFeatureGate_CACHED_MAY_BE_STALE 仅迁移遗留 gate,新代码用 getFeatureValue

注意

Datadog dev 环境直接 return,本地看不到 tengu_* 上传

在架构中的位置

Analytics 初始化与数据流:

模块任意处 logEvent(name, { numeric metadata })
  → index.ts:sink 未 attach 则入队
  → setupBackend:initializeAnalyticsGates + initializeAnalyticsSink
  → sink.logEventImpl
  → shouldSampleEvent(GrowthBook tengu_event_sampling_config)
  → [可选] trackDatadogEvent(stripProtoFields)
  → logEventTo1P(OpenTelemetry BatchLogRecordProcessor)

growthbook.ts 并行在 init 时 refreshGrowthBookFeatures,供 compact cache、Datadog gate、1P batch config 等读取。onGrowthBookRefresh 订阅者可重建 long-lived LoggerProvider。

logEvent 入口与启动队列

index.ts 设计目标:无环 + 早启动友好。

attachAnalyticsSink:

  • 已 attach 则 no-op(preAction hook 与 setup() 可重复调用)
  • drain 时用 spread 复制队列再清空,microtask 异步发送
  • ant 用户打 analytics_sink_attached 含 queued_event_count 调试 init 时序

logEvent / logEventAsync 在 sink 为 null 时 push { eventName, metadata, async }。

_resetForTesting 仅测试用,清空 sink 与队列。

类型 AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 强制开发者确认字符串不含代码路径;AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED 标记进 BQ 特权列的 _PROTO_ 键。

源码引用: src/services/analytics/index.ts · 第 11–58 行(共 174 行)

  11| /**
  12|  * Marker type for verifying analytics metadata doesn't contain sensitive data
  13|  *
  14|  * This type forces explicit verification that string values being logged
  15|  * don't contain code snippets, file paths, or other sensitive information.
  16|  *
  17|  * Usage: `myString as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS`
  18|  */
  19| export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never
  20| 
  21| /**
  22|  * Marker type for values routed to PII-tagged proto columns via `_PROTO_*`
  23|  * payload keys. The destination BQ column has privileged access controls,
  24|  * so unredacted values are acceptable — unlike general-access backends.
  25|  *
  26|  * sink.ts strips `_PROTO_*` keys before Datadog fanout; only the 1P
  27|  * exporter (firstPartyEventLoggingExporter) sees them and hoists them to the
  28|  * top-level proto field. A single stripProtoFields call guards all non-1P
  29|  * sinks — no per-sink filtering to forget.
  30|  *
  31|  * Usage: `rawName as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED`
  32|  */
  33| export type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED = never
  34| 
  35| /**
  36|  * Strip `_PROTO_*` keys from a payload destined for general-access storage.
  37|  * Used by:
  38|  *   - sink.ts: before Datadog fanout (never sees PII-tagged values)
  39|  *   - firstPartyEventLoggingExporter: defensive strip of additional_metadata
  40|  *     after hoisting known _PROTO_* keys to proto fields — prevents a future
  41|  *     unrecognized _PROTO_foo from silently landing in the BQ JSON blob.
  42|  *
  43|  * Returns the input unchanged (same reference) when no _PROTO_ keys present.
  44|  */
  45| export function stripProtoFields<V>(
  46|   metadata: Record<string, V>,
  47| ): Record<string, V> {
  48|   let result: Record<string, V> | undefined
  49|   for (const key in metadata) {
  50|     if (key.startsWith('_PROTO_')) {
  51|       if (result === undefined) {
  52|         result = { ...metadata }
  53|       }
  54|       delete result[key]
  55|     }
  56|   }
  57|   return result ?? metadata
  58| }

源码引用: src/services/analytics/index.ts · 第 95–164 行(共 174 行)

  95| export function attachAnalyticsSink(newSink: AnalyticsSink): void {
  96|   if (sink !== null) {
  97|     return
  98|   }
  99|   sink = newSink
 100| 
 101|   // Drain the queue asynchronously to avoid blocking startup
 102|   if (eventQueue.length > 0) {
 103|     const queuedEvents = [...eventQueue]
 104|     eventQueue.length = 0
 105| 
 106|     // Log queue size for ants to help debug analytics initialization timing
 107|     if (process.env.USER_TYPE === 'ant') {
 108|       sink.logEvent('analytics_sink_attached', {
 109|         queued_event_count: queuedEvents.length,
 110|       })
 111|     }
 112| 
 113|     queueMicrotask(() => {
 114|       for (const event of queuedEvents) {
 115|         if (event.async) {
 116|           void sink!.logEventAsync(event.eventName, event.metadata)
 117|         } else {
 118|           sink!.logEvent(event.eventName, event.metadata)
 119|         }
 120|       }
 121|     })
 122|   }
 123| }
 124| 
 125| /**
 126|  * Log an event to analytics backends (synchronous)
 127|  *
 128|  * Events may be sampled based on the 'tengu_event_sampling_config' dynamic config.
 129|  * When sampled, the sample_rate is added to the event metadata.
 130|  *
 131|  * If no sink is attached, events are queued and drained when the sink attaches.
 132|  */
 133| export function logEvent(
 134|   eventName: string,
 135|   // intentionally no strings unless AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 136|   // to avoid accidentally logging code/filepaths
 137|   metadata: LogEventMetadata,
 138| ): void {
 139|   if (sink === null) {
 140|     eventQueue.push({ eventName, metadata, async: false })
 141|     return
 142|   }
 143|   sink.logEvent(eventName, metadata)
 144| }
 145| 
 146| /**
 147|  * Log an event to analytics backends (asynchronous)
 148|  *
 149|  * Events may be sampled based on the 'tengu_event_sampling_config' dynamic config.
 150|  * When sampled, the sample_rate is added to the event metadata.
 151|  *
 152|  * If no sink is attached, events are queued and drained when the sink attaches.
 153|  */
 154| export async function logEventAsync(
 155|   eventName: string,
 156|   // intentionally no strings, to avoid accidentally logging code/filepaths
 157|   metadata: LogEventMetadata,
 158| ): Promise<void> {
 159|   if (sink === null) {
 160|     eventQueue.push({ eventName, metadata, async: true })
 161|     return
 162|   }
 163|   await sink.logEventAsync(eventName, metadata)
 164| }

stripProtoFields:PII 与 general-access 分离

_PROTO_ 前缀键进入 1P proto 列(privileged BQ ACL),绝不能进 Datadog 等 general-access 后端。

stripProtoFields 惰性拷贝:无 _PROTO_ 键时返回原对象引用零分配。用于:

  1. sink.ts Datadog fanout 前
  2. firstPartyEventLoggingExporter hoist 已知 proto 后对 additional_metadata 防御性 strip

注释说明:单点 strip 优于 per-sink 过滤,避免新增 sink 忘记脱敏。

读 GDPR/PII 相关 code review 时,确认新字段要么走 numeric metadata,要么显式 _PROTO_ + PII 断言。

源码引用: src/services/analytics/index.ts · 第 35–58 行(共 174 行)

  35| /**
  36|  * Strip `_PROTO_*` keys from a payload destined for general-access storage.
  37|  * Used by:
  38|  *   - sink.ts: before Datadog fanout (never sees PII-tagged values)
  39|  *   - firstPartyEventLoggingExporter: defensive strip of additional_metadata
  40|  *     after hoisting known _PROTO_* keys to proto fields — prevents a future
  41|  *     unrecognized _PROTO_foo from silently landing in the BQ JSON blob.
  42|  *
  43|  * Returns the input unchanged (same reference) when no _PROTO_ keys present.
  44|  */
  45| export function stripProtoFields<V>(
  46|   metadata: Record<string, V>,
  47| ): Record<string, V> {
  48|   let result: Record<string, V> | undefined
  49|   for (const key in metadata) {
  50|     if (key.startsWith('_PROTO_')) {
  51|       if (result === undefined) {
  52|         result = { ...metadata }
  53|       }
  54|       delete result[key]
  55|     }
  56|   }
  57|   return result ?? metadata
  58| }

sink.ts:采样、Datadog gate、1P

initializeAnalyticsSink 调用 attachAnalyticsSink({ logEvent: logEventImpl, logEventAsync })。

logEventImpl 流程:

  1. shouldSampleEvent → 返回 0 则丢弃;返回 (0,1] 则写入 sample_rate metadata
  2. shouldTrackDatadog():sinkKillswitch + tengu_log_datadog_events gate(initializeAnalyticsGates 刷新,未 init 时读 disk cache)
  3. Datadog:trackDatadogEvent(event, stripProtoFields(metadata))
  4. 1P:logEventTo1P(event, full metadata)

Segment 移除后 async 路径仅 wrap sync,保留接口兼容。

isSinkKilled 支持按 backend 名 kill(运维熔断)。

源码引用: src/services/analytics/sink.ts · 第 20–72 行(共 115 行)

  20| const DATADOG_GATE_NAME = 'tengu_log_datadog_events'
  21| 
  22| // Module-level gate state - starts undefined, initialized during startup
  23| let isDatadogGateEnabled: boolean | undefined = undefined
  24| 
  25| /**
  26|  * Check if Datadog tracking is enabled.
  27|  * Falls back to cached value from previous session if not yet initialized.
  28|  */
  29| function shouldTrackDatadog(): boolean {
  30|   if (isSinkKilled('datadog')) {
  31|     return false
  32|   }
  33|   if (isDatadogGateEnabled !== undefined) {
  34|     return isDatadogGateEnabled
  35|   }
  36| 
  37|   // Fallback to cached value from previous session
  38|   try {
  39|     return checkStatsigFeatureGate_CACHED_MAY_BE_STALE(DATADOG_GATE_NAME)
  40|   } catch {
  41|     return false
  42|   }
  43| }
  44| 
  45| /**
  46|  * Log an event (synchronous implementation)
  47|  */
  48| function logEventImpl(eventName: string, metadata: LogEventMetadata): void {
  49|   // Check if this event should be sampled
  50|   const sampleResult = shouldSampleEvent(eventName)
  51| 
  52|   // If sample result is 0, the event was not selected for logging
  53|   if (sampleResult === 0) {
  54|     return
  55|   }
  56| 
  57|   // If sample result is a positive number, add it to metadata
  58|   const metadataWithSampleRate =
  59|     sampleResult !== null
  60|       ? { ...metadata, sample_rate: sampleResult }
  61|       : metadata
  62| 
  63|   if (shouldTrackDatadog()) {
  64|     // Datadog is a general-access backend — strip _PROTO_* keys
  65|     // (unredacted PII-tagged values meant only for the 1P privileged column).
  66|     void trackDatadogEvent(eventName, stripProtoFields(metadataWithSampleRate))
  67|   }
  68| 
  69|   // 1P receives the full payload including _PROTO_* — the exporter
  70|   // destructures and routes those keys to proto fields itself.
  71|   logEventTo1P(eventName, metadataWithSampleRate)
  72| }

源码引用: src/services/analytics/sink.ts · 第 96–114 行(共 115 行)

  96| export function initializeAnalyticsGates(): void {
  97|   isDatadogGateEnabled =
  98|     checkStatsigFeatureGate_CACHED_MAY_BE_STALE(DATADOG_GATE_NAME)
  99| }
 100| 
 101| /**
 102|  * Initialize the analytics sink.
 103|  *
 104|  * Call this during app startup to attach the analytics backend.
 105|  * Any events logged before this is called will be queued and drained.
 106|  *
 107|  * Idempotent: safe to call multiple times (subsequent calls are no-ops).
 108|  */
 109| export function initializeAnalyticsSink(): void {
 110|   attachAnalyticsSink({
 111|     logEvent: logEventImpl,
 112|     logEventAsync: logEventAsyncImpl,
 113|   })
 114| }

GrowthBook:getFeatureValue_CACHED_MAY_BE_STALE

growthbook.ts 维护 GrowthBook client、remoteEval 内存 map、disk cachedGrowthBookFeatures、experiment exposure dedup。

getFeatureValue_CACHED_MAY_BE_STALE 读取顺序:

  1. env overrides(eval harness)
  2. config overrides(本地调试)
  3. isGrowthBookEnabled() false → defaultValue
  4. 记录 experiment exposure(pendingExposures 若 init 未完成)
  5. remoteEvalFeatureValues 内存(init 后权威)
  6. disk cache(跨进程)

热路径函数包括:compact 的 tengu_compact_cache_prefix、api 的 tengu_prompt_cache_1h_config、analytics 的 tengu_event_sampling_config。

onGrowthBookRefresh 在 init/periodic refresh 后通知订阅者;注册时若 map 已有值则 microtask catch-up(修复 REPL mount race #20951)。

安全敏感 gate 用 checkSecurityRestrictionGate 阻塞等待 reinitializingPromise。

源码引用: src/services/analytics/growthbook.ts · 第 734–775 行(共 1156 行)

 734| export function getFeatureValue_CACHED_MAY_BE_STALE<T>(
 735|   feature: string,
 736|   defaultValue: T,
 737| ): T {
 738|   // Check env var overrides first (for eval harnesses)
 739|   const overrides = getEnvOverrides()
 740|   if (overrides && feature in overrides) {
 741|     return overrides[feature] as T
 742|   }
 743|   const configOverrides = getConfigOverrides()
 744|   if (configOverrides && feature in configOverrides) {
 745|     return configOverrides[feature] as T
 746|   }
 747| 
 748|   if (!isGrowthBookEnabled()) {
 749|     return defaultValue
 750|   }
 751| 
 752|   // Log experiment exposure if data is available, otherwise defer until after init
 753|   if (experimentDataByFeature.has(feature)) {
 754|     logExposureForFeature(feature)
 755|   } else {
 756|     pendingExposures.add(feature)
 757|   }
 758| 
 759|   // In-memory payload is authoritative once processRemoteEvalPayload has run.
 760|   // Disk is also fresh by then (syncRemoteEvalToDisk runs synchronously inside
 761|   // init), so this is correctness-equivalent to the disk read below — but it
 762|   // skips the config JSON parse and is what onGrowthBookRefresh subscribers
 763|   // depend on to read fresh values the instant they're notified.
 764|   if (remoteEvalFeatureValues.has(feature)) {
 765|     return remoteEvalFeatureValues.get(feature) as T
 766|   }
 767| 
 768|   // Fall back to disk cache (survives across process restarts)
 769|   try {
 770|     const cached = getGlobalConfig().cachedGrowthBookFeatures?.[feature]
 771|     return cached !== undefined ? (cached as T) : defaultValue
 772|   } catch {
 773|     return defaultValue
 774|   }
 775| }

源码引用: src/services/analytics/growthbook.ts · 第 139–150 行(共 1156 行)

 139| export function onGrowthBookRefresh(
 140|   listener: GrowthBookRefreshListener,
 141| ): () => void {
 142|   let subscribed = true
 143|   const unsubscribe = refreshed.subscribe(() => callSafe(listener))
 144|   if (remoteEvalFeatureValues.size > 0) {
 145|     queueMicrotask(() => {
 146|       // Re-check: listener may have been removed, or resetGrowthBook may have
 147|       // cleared the Map, between registration and this microtask running.
 148|       if (subscribed && remoteEvalFeatureValues.size > 0) {
 149|         callSafe(listener)
 150|       }

事件采样:firstPartyEventLogger

shouldSampleEvent 读 GrowthBook dynamic config tengu_event_sampling_config:

  • 无配置 → null(100% 记录,不写 sample_rate)
  • sample_rate 非法 → null
  • rate >= 1 → null;rate <= 0 → 0(丢弃)
  • 否则 Math.random 决定,选中则返回 rate 供 metadata

getEventSamplingConfig 与 getBatchConfig(tengu_1p_event_batch_config)均用 getDynamicConfig_CACHED_MAY_BE_STALE。

1P logger 用 OpenTelemetry BatchLogRecordProcessor;GrowthBook refresh 时可 reinitialize1PEventLoggingIfConfigChanged 重建 provider(batch size、delay、endpoint)。

shutdown 路径:shutdown1PEventLogging 在 process exit 前 flush。

源码引用: src/services/analytics/firstPartyEventLogger.ts · 第 57–85 行(共 450 行)

  57| export function shouldSampleEvent(eventName: string): number | null {
  58|   const config = getEventSamplingConfig()
  59|   const eventConfig = config[eventName]
  60| 
  61|   // If no config for this event, log at 100% rate (no sampling)
  62|   if (!eventConfig) {
  63|     return null
  64|   }
  65| 
  66|   const sampleRate = eventConfig.sample_rate
  67| 
  68|   // Validate sample rate is in valid range
  69|   if (typeof sampleRate !== 'number' || sampleRate < 0 || sampleRate > 1) {
  70|     return null
  71|   }
  72| 
  73|   // Sample rate of 1 means log everything (no need to add metadata)
  74|   if (sampleRate >= 1) {
  75|     return null
  76|   }
  77| 
  78|   // Sample rate of 0 means drop everything
  79|   if (sampleRate <= 0) {
  80|     return 0
  81|   }
  82| 
  83|   // Randomly decide whether to sample this event
  84|   return Math.random() < sampleRate ? sampleRate : 0
  85| }

源码引用: src/services/analytics/firstPartyEventLogger.ts · 第 87–102 行(共 450 行)

  87| const BATCH_CONFIG_NAME = 'tengu_1p_event_batch_config'
  88| type BatchConfig = {
  89|   scheduledDelayMillis?: number
  90|   maxExportBatchSize?: number
  91|   maxQueueSize?: number
  92|   skipAuth?: boolean
  93|   maxAttempts?: number
  94|   path?: string
  95|   baseUrl?: string
  96| }
  97| function getBatchConfig(): BatchConfig {
  98|   return getDynamicConfig_CACHED_MAY_BE_STALE<BatchConfig>(
  99|     BATCH_CONFIG_NAME,
 100|     {},
 101|   )
 102| }

Datadog:allowlist 与 cardinality 控制

trackDatadogEvent 约束:

  • 非 production 直接 return
  • 非 firstParty provider(Bedrock/Vertex 等)return
  • 事件名必须在 DATADOG_ALLOWED_EVENTS Set(tengu_api_error、tengu_compact_failed、tengu_tool_use_success 等 ~40 个)
  • 合并 getEventMetadata(model、betas、envContext)
  • MCP toolName mcp__* 规范化为 mcp 降 cardinality
  • 外部用户 model 名映射 canonical / other
  • dev version 字符串截断去 timestamp.sha

批量:15s flush 或 100 条;shutdownDatadog 在 gracefulShutdown 显式 flush(forceExit 不触发 beforeExit)。

client token 内嵌 pub key(Datadog browser/log intake 模式)。

源码引用: src/services/analytics/datadog.ts · 第 19–64 行(共 308 行)

  19| const DATADOG_ALLOWED_EVENTS = new Set([
  20|   'chrome_bridge_connection_succeeded',
  21|   'chrome_bridge_connection_failed',
  22|   'chrome_bridge_disconnected',
  23|   'chrome_bridge_tool_call_completed',
  24|   'chrome_bridge_tool_call_error',
  25|   'chrome_bridge_tool_call_started',
  26|   'chrome_bridge_tool_call_timeout',
  27|   'tengu_api_error',
  28|   'tengu_api_success',
  29|   'tengu_brief_mode_enabled',
  30|   'tengu_brief_mode_toggled',
  31|   'tengu_brief_send',
  32|   'tengu_cancel',
  33|   'tengu_compact_failed',
  34|   'tengu_exit',
  35|   'tengu_flicker',
  36|   'tengu_init',
  37|   'tengu_model_fallback_triggered',
  38|   'tengu_oauth_error',
  39|   'tengu_oauth_success',
  40|   'tengu_oauth_token_refresh_failure',
  41|   'tengu_oauth_token_refresh_success',
  42|   'tengu_oauth_token_refresh_lock_acquiring',
  43|   'tengu_oauth_token_refresh_lock_acquired',
  44|   'tengu_oauth_token_refresh_starting',
  45|   'tengu_oauth_token_refresh_completed',
  46|   'tengu_oauth_token_refresh_lock_releasing',
  47|   'tengu_oauth_token_refresh_lock_released',
  48|   'tengu_query_error',
  49|   'tengu_session_file_read',
  50|   'tengu_started',
  51|   'tengu_tool_use_error',
  52|   'tengu_tool_use_granted_in_prompt_permanent',
  53|   'tengu_tool_use_granted_in_prompt_temporary',
  54|   'tengu_tool_use_rejected_in_prompt',
  55|   'tengu_tool_use_success',
  56|   'tengu_uncaught_exception',
  57|   'tengu_unhandled_rejection',
  58|   'tengu_voice_recording_started',
  59|   'tengu_voice_toggled',
  60|   'tengu_team_mem_sync_pull',
  61|   'tengu_team_mem_sync_push',
  62|   'tengu_team_mem_sync_started',
  63|   'tengu_team_mem_entries_capped',
  64| ])

源码引用: src/services/analytics/datadog.ts · 第 160–218 行(共 308 行)

 160| export async function trackDatadogEvent(
 161|   eventName: string,
 162|   properties: { [key: string]: boolean | number | undefined },
 163| ): Promise<void> {
 164|   if (process.env.NODE_ENV !== 'production') {
 165|     return
 166|   }
 167| 
 168|   // Don't send events for 3P providers (Bedrock, Vertex, Foundry)
 169|   if (getAPIProvider() !== 'firstParty') {
 170|     return
 171|   }
 172| 
 173|   // Fast path: use cached result if available to avoid await overhead
 174|   let initialized = datadogInitialized
 175|   if (initialized === null) {
 176|     initialized = await initializeDatadog()
 177|   }
 178|   if (!initialized || !DATADOG_ALLOWED_EVENTS.has(eventName)) {
 179|     return
 180|   }
 181| 
 182|   try {
 183|     const metadata = await getEventMetadata({
 184|       model: properties.model,
 185|       betas: properties.betas,
 186|     })
 187|     // Destructure to avoid duplicate envContext (once nested, once flattened)
 188|     const { envContext, ...restMetadata } = metadata
 189|     const allData: Record<string, unknown> = {
 190|       ...restMetadata,
 191|       ...envContext,
 192|       ...properties,
 193|       userBucket: getUserBucket(),
 194|     }
 195| 
 196|     // Normalize MCP tool names to "mcp" for cardinality reduction
 197|     if (
 198|       typeof allData.toolName === 'string' &&
 199|       allData.toolName.startsWith('mcp__')
 200|     ) {
 201|       allData.toolName = 'mcp'
 202|     }
 203| 
 204|     // Normalize model names for cardinality reduction (external users only)
 205|     if (process.env.USER_TYPE !== 'ant' && typeof allData.model === 'string') {
 206|       const shortName = getCanonicalName(allData.model.replace(/\[1m]$/i, ''))
 207|       allData.model = shortName in MODEL_COSTS ? shortName : 'other'
 208|     }
 209| 
 210|     // Truncate dev version to base + date (remove timestamp and sha for cardinality reduction)
 211|     // e.g. "2.0.53-dev.20251124.t173302.sha526cc6a" -> "2.0.53-dev.20251124"
 212|     if (typeof allData.version === 'string') {
 213|       allData.version = allData.version.replace(
 214|         /^(\d+\.\d+\.\d+-dev\.\d{8})\.t\d+\.sha[a-f0-9]+$/,
 215|         '$1',
 216|       )
 217|     }
 218| 

源码目录与关联文件

强关联:services/analytics/metadata.ts(enriched EventMetadata)、services/analytics/firstPartyEventLoggingExporter.ts、services/analytics/sinkKillswitch.ts、services/analytics/config.ts(isAnalyticsDisabled)。

动手练习

  1. 在 ant 环境启动 CLI,检查 analytics_sink_attached 的 queued_event_count
  2. 给 tengu_event_sampling_config 设某事件 sample_rate=0.1,验证 sample_rate metadata
  3. 对比 logEvent 带 _PROTO_email 时 Datadog payload 是否无该键
  4. 读 initializeAnalyticsGates 调用栈(main.tsx setupBackend)确认与 GrowthBook init 顺序

本章小结与延伸

analytics = 可观测性与动态配置。读 api-claude / compact 章中的 logEvent 调用时可回到本章理解采样与 kill switch。 继续学习:

  • api-claude
  • services 总览
Prev
compact · 上下文压缩与自动触发
Next
tool-interface · Tool 契约与注册表