Claude Code 源码分析第 24 课 · 第 07
第 24 课

成本分析

Claude Code 如何跟踪代币支出、通过双分析管道路由事件、应用 PII 控件以及控制 GrowthBook 背后的一切(从单个 API 响应到 BigQuery)。

01 双管道架构

Claude Code 运行 两个完全独立的可观察性堆栈 并联。 一个是内部第一方 (1P) 管道,由 OpenTelemetry 和发送到的原始序列化事件支持 /api/event_logging/batch。 另一个是用于运行监控的 Datadog HTTP 接收扇出。 一个薄薄的 sink 层位于中间,决定哪个管道接收每个事件 - 以及是否发送它。

flowchart TD API["API Response\n(token usage)"] --> CostTracker["cost-tracker.ts\naddToTotalSessionCost()"] CostTracker --> OTelCounter["OTel Counters\ngetCostCounter() / getTokenCounter()"] CostTracker --> LogEvent["logEvent()\nservices/analytics/index.ts"] LogEvent --> Queue["Event Queue\n(pre-sink buffer)"] Queue --> Sink["initializeAnalyticsSink()\nservices/analytics/sink.ts"] Sink --> SampleCheck{"shouldSampleEvent()\nGrowthBook rate"} SampleCheck -->|"rate=0 → drop"| Drop["Dropped"] SampleCheck -->|"rate>0 → keep"| DatadogGate{"tengu_log_datadog_events\nGrowthBook gate"} DatadogGate -->|"enabled"| StripProto["stripProtoFields()\nremove _PROTO_* keys"] StripProto --> Datadog["trackDatadogEvent()\nHTTP batch → Datadog"] DatadogGate -->|"any"| FirstParty["logEventTo1P()\nOTel BatchLogRecordProcessor"] FirstParty --> Exporter["FirstPartyEventLoggingExporter\n/api/event_logging/batch"] OTelCounter --> MeterProvider["OTel MeterProvider"] MeterProvider --> BigQuery["BigQueryMetricsExporter\n/api/claude_code/metrics"] MeterProvider --> CustomerOTLP["Customer OTLP Endpoint\n(opt-in via CLAUDE_CODE_ENABLE_TELEMETRY)"]
设计原理
事件在接收器连接之前排队(queueMicrotask 启动时耗尽),因此在应用程序初始化期间不会丢失任何内容。 1P 管道和 Datadog 管道是完全独立的——终止开关可以使一个管道静音而不影响另一个管道。
02 成本跟踪:cost-tracker.ts

每个 API 响应都带有一个 BetaUsage object. addToTotalSessionCost() 是风扇将数据同时发送到三个目的地的单一入口点:内存中累加器、OpenTelemetry 计数器和分析事件日志。

addToTotalSessionCost — 完整实施
// cost-tracker.ts
export function addToTotalSessionCost(
  cost: number,
  usage: Usage,
  model: string,
): number {
  // 1. Accumulate per-model token counters in memory
  const modelUsage = addToTotalModelUsage(cost, usage, model)
  addToTotalCostState(cost, modelUsage, model)

  // 2. Push to OTel counters (for BigQuery / customer OTLP)
  const attrs = isFastModeEnabled() && usage.speed === 'fast'
    ? { model, speed: 'fast' }
    : { model }

  getCostCounter()?.add(cost, attrs)
  getTokenCounter()?.add(usage.input_tokens,  { ...attrs, type: 'input' })
  getTokenCounter()?.add(usage.output_tokens, { ...attrs, type: 'output' })
  getTokenCounter()?.add(usage.cache_read_input_tokens ?? 0,
    { ...attrs, type: 'cacheRead' })
  getTokenCounter()?.add(usage.cache_creation_input_tokens ?? 0,
    { ...attrs, type: 'cacheCreation' })

  // 3. Log advisor sub-usage to 1P analytics
  let totalCost = cost
  for (const advisorUsage of getAdvisorUsage(usage)) {
    const advisorCost = calculateUSDCost(advisorUsage.model, advisorUsage)
    logEvent('tengu_advisor_tool_token_usage', {
      advisor_model: advisorUsage.model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
      input_tokens: advisorUsage.input_tokens,
      cost_usd_micros: Math.round(advisorCost * 1_000_000),
    })
    totalCost += addToTotalSessionCost(advisorCost, advisorUsage, advisorUsage.model)
  }
  return totalCost
}

成本存储在 microdollars 当作为分析元数据发送时(cost_usd_micros) 将值保留为整数。 原始浮点数用于 OTel 计数器。 这可以防止事件负载中出现浮点累积错误。

会话保持

当会话结束(或暂停)时 saveCurrentSessionCosts() 将完整快照写入项目配置 JSON。 简历上, restoreCostStateForSession() 从该快照中重新水化内存中的计数器 - 但仅当 sessionId 在配置中匹配当前会话。

会话保存/恢复模式
// Save on process exit via useCostSummary React hook
export function saveCurrentSessionCosts(fpsMetrics?: FpsMetrics): void {
  saveCurrentProjectConfig(current => ({
    ...current,
    lastCost:                          getTotalCostUSD(),
    lastAPIDuration:                   getTotalAPIDuration(),
    lastAPIDurationWithoutRetries:     getTotalAPIDurationWithoutRetries(),
    lastTotalInputTokens:              getTotalInputTokens(),
    lastTotalOutputTokens:             getTotalOutputTokens(),
    lastTotalCacheCreationInputTokens: getTotalCacheCreationInputTokens(),
    lastTotalCacheReadInputTokens:     getTotalCacheReadInputTokens(),
    lastTotalWebSearchRequests:        getTotalWebSearchRequests(),
    lastFpsAverage:                    fpsMetrics?.averageFps,
    lastModelUsage:                    Object.fromEntries(
      Object.entries(getModelUsage()).map(([model, u]) => [
        model,
        { inputTokens: u.inputTokens, outputTokens: u.outputTokens,
          cacheReadInputTokens: u.cacheReadInputTokens,
          cacheCreationInputTokens: u.cacheCreationInputTokens,
          costUSD: u.costUSD },
      ])
    ),
    lastSessionId: getSessionId(),
  }))
}

// Restore only when session IDs match
export function restoreCostStateForSession(sessionId: string): boolean {
  const data = getStoredSessionCosts(sessionId)
  if (!data) return false
  setCostStateForRestore(data)
  return true
}
03 costHook.ts React 桥

costHook.ts 看似很小(22 行),但它将成本节省连接到 React 生命周期中,以便 CLI 的退出处理程序始终捕获累积的成本状态,无论进程如何终止。

// costHook.ts — full file
import { useEffect } from 'react'
import { formatTotalCost, saveCurrentSessionCosts } from './cost-tracker.js'
import { hasConsoleBillingAccess } from './utils/billing.js'
import type { FpsMetrics } from './utils/fpsTracker.js'

export function useCostSummary(
  getFpsMetrics?: () => FpsMetrics | undefined,
): void {
  useEffect(() => {
    const f = () => {
      // Only print the summary if the user has console billing access
      if (hasConsoleBillingAccess()) {
        process.stdout.write('\n' + formatTotalCost() + '\n')
      }
      saveCurrentSessionCosts(getFpsMetrics?.())
    }
    process.on('exit', f)
    return () => { process.off('exit', f) }
  }, [])
}
为什么要使用 React 钩子?
Claude Code 的 TUI 是使用 Ink(针对终端的 React)构建的。 注册 process.on('exit') 里面的处理程序 useEffect 将处理程序生命周期与 React 组件树联系起来 - 如果根组件卸载,则处理程序将被删除,从而防止在许多会话顺序运行的测试环境中出现内存泄漏。
04 分析接收器和队列

分析模块(services/analytics/index.ts)故意有 零依赖 以防止导入循环。 它仅公开三个功能: logEvent, logEventAsync, 和 attachAnalyticsSink。 其他所有内容——Datadog、1P、GrowthBook——都位于该文件之外。

Queue-then-drain 启动模式(完整实现)
// services/analytics/index.ts
const eventQueue: QueuedEvent[] = []
let sink: AnalyticsSink | null = null

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) {
        event.async
          ? void sink!.logEventAsync(event.eventName, event.metadata)
          :       sink!.logEvent(event.eventName, event.metadata)
      }
    })
  }
}

export function logEvent(eventName: string, metadata: LogEventMetadata): void {
  if (sink === null) {
    eventQueue.push({ eventName, metadata, async: false })
    return
  }
  sink.logEvent(eventName, metadata)
}

接收器实现(services/analytics/sink.ts)通过采样门路由每个事件,然后扇出到 Datadog(剥离后 _PROTO_* 键)和 1P 管道(具有完整的有效负载)。

// services/analytics/sink.ts — core routing
function logEventImpl(eventName: string, metadata: LogEventMetadata): void {
  const sampleResult = shouldSampleEvent(eventName)
  if (sampleResult === 0) return  // dropped

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

  if (shouldTrackDatadog()) {
    // Strip _PROTO_* keys — Datadog is general-access, PII must stay out
    void trackDatadogEvent(eventName, stripProtoFields(metadataWithSampleRate))
  }
  // 1P receives full payload; exporter handles PII routing internally
  logEventTo1P(eventName, metadataWithSampleRate)
}
05 数据狗管道

Datadog 收到一份包含约 40 个事件的精选许可名单。 活动不在 DATADOG_ALLOWED_EVENTS 都被默默地丢弃了。 此白名单的存在是为了保持 Datadog 索引较小并防止大量内部事件的意外泄漏。

Batching

带 15 秒计时器的 HTTP 批处理

事件累积于 logBatch[]。 每 15 秒或当批次达到 100 个条目时刷新一次,以先到者为准。

基数减少

模型和工具名称规范化

外部用户模型名称映射到规范短名称或“其他”。 MCP 工具名称折叠为“mcp”以防止每个工具基数爆炸。

用户桶

SHA-256 → 30 个桶

用户 ID 被散列并存储到 30 个槽中的 1 个槽中。 仪表板对唯一存储桶进行计数以近似唯一用户,而无需跟踪个人 ID。

Routing

仅限第一方 API

Bedrock、Vertex 和 Foundry 提供商的 Datadog 事件被跳过 — getAPIProvider() !== 'firstParty' 是一个硬退出。

标签与字段

可查询 Dim 的 ddtags

高基数维度(型号、版本、平台)落地 ddtags 因此可以通过 Datadog 聚合 API 查询它们。 这 message 字段是保留字段,不可查询。

Shutdown

退出时优雅冲洗

shutdownDatadog() 取消刷新计时器并在之前发布任何剩余批次 process.exit().

用户存储桶实现——隐私保护基数
const NUM_USER_BUCKETS = 30

const getUserBucket = memoize((): number => {
  const userId = getOrCreateUserID()
  const hash = createHash('sha256').update(userId).digest('hex')
  return parseInt(hash.slice(0, 8), 16) % NUM_USER_BUCKETS
})

哈希值截断为 8 个十六进制字符(32 位),以保持在 JavaScript 安全整数范围内。 模确定性地映射到 0-29。 这使得警报仪表板在唯一用户计数上的误差范围约为 3.3%,同时使个人重新识别变得不切实际。

06 第一方管道和 OpenTelemetry

1P管道建立在 OpenTelemetry 日志 SDK。 一个专门的 LoggerProvider - 与面向客户的遥测提供商不同 - 推动 BatchLogRecordProcessor 缓冲事件并将它们批量导出到 Anthropic 的内部端点。

sequenceDiagram participant App participant Logger as 1P Logger
(OTel Logger) participant Batch as BatchLogRecordProcessor participant Exporter as FirstPartyEventLoggingExporter participant Disk as Failed Events
(JSONL on disk) participant API as /api/event_logging/batch App->>Logger: emit({ body: eventName, attributes }) Logger->>Batch: onEmit(log record) Note over Batch: Buffer until scheduledDelayMillis
or maxExportBatchSize (200) Batch->>Exporter: export(logs[]) Exporter->>Exporter: transformLogsToEvents()
hoist _PROTO_* keys Exporter->>API: POST /api/event_logging/batch alt success API-->>Exporter: 200 OK Exporter->>Disk: delete failed events file else failure API-->>Exporter: 5xx / timeout Exporter->>Disk: appendEventsToFile()
JSONL append (atomic) Exporter->>Exporter: scheduleBackoffRetry()
quadratic: base * attempts² end

弹性:磁盘支持的重试

当 POST 失败时,失败的事件将附加到每个会话的 JSONL 文件中,位置为 ~/.claude/telemetry/1p_failed_events.<sessionId>.<BATCH_UUID>.json。 下次启动时, retryPreviousBatches() 扫描同一会话中的任何剩余文件并重试。 这意味着事件可以在进程崩溃和临时网络中断的情况下幸存下来。

二次退避调度器
private scheduleBackoffRetry(): void {
  if (this.cancelBackoff || this.isRetrying || this.isShutdown) return

  // Quadratic backoff: base * attempts²  (matches Statsig SDK)
  const delay = Math.min(
    this.baseBackoffDelayMs * this.attempts * this.attempts,
    this.maxBackoffDelayMs,  // caps at 30 000 ms
  )

  this.cancelBackoff = this.schedule(async () => {
    this.cancelBackoff = null
    await this.retryFailedEvents()
  }, delay)
}

尝试 1:500 毫秒延迟。 尝试 2:2 000 毫秒。 尝试 3:4 500 毫秒。 尝试 4:8 000 毫秒。 尝试 5:12 500 毫秒。 从尝试 8 开始,上限为 30 000 毫秒。 在该批次被永久丢弃之前,总共最多尝试 8 次。

OTel LoggerProvider 分离

1P LoggerProvider is 从未在全球注册过 via logs.setGlobalLoggerProvider()。 面向客户的 OTLP 遥测使用全球提供商。 内部事件日志记录使用模块本地提供程序,因此客户 OTLP 端点永远不会看到内部 Anthropic 事件,反之亦然。

// firstPartyEventLogger.ts — two providers, never mixed
// The 1P provider is kept module-local:
let firstPartyEventLoggerProvider: LoggerProvider | null = null
let firstPartyEventLogger: Logger | null = null

// Customer telemetry uses the global provider in instrumentation.ts:
// logs.setGlobalLoggerProvider(loggerProvider)  ← customer endpoint
// setEventLogger(logs.getLogger(...))           ← customer event logger
//
// 1P provider is obtained from the local instance, NOT from logs.getLogger():
firstPartyEventLogger = firstPartyEventLoggerProvider.getLogger(
  'com.anthropic.claude_code.events',
  MACRO.VERSION,
)
07 PII 隔离和 _PROTO_ 模式

Claude Code 使用 TypeScript 幻像类型系统在编译时强制实施 PII 卫生,并使用运行时键命名约定将敏感值仅路由到特权存储。

编译时防护:从不键入的标记类型

// services/analytics/index.ts

// Type is `never` — cannot hold a value, only used for casting.
// Any string passed to logEvent() must be explicitly cast to this type,
// forcing developers to verify it contains no code/file paths.
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never

// Second marker for PII-tagged values destined for privileged BQ columns.
// These values travel with the event but are stripped before Datadog.
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED = never
never 类型如何强制审查
Because never 是 TypeScript 中的底层类型,无法合法地为其赋值。 将字符串传递为 AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 需要明确的 as 投掷。 该转换向代码审阅者发出了一个信号:“作者有意识地确认该字符串可以安全记录。” 类型系统无法验证声明,但它强制执行可见的意图声明。

运行时防护:_PROTO_* 键约定

属于 PII 但在特权 BigQuery 列中可接受的值(例如技能名称、插件名称)将添加到前缀为键下的事件负载中 _PROTO_。 水槽呼叫 stripProtoFields() 在Datadog扇出之前。 已知的 1P 出口商葫芦 _PROTO_ 键到专用原型字段,然后调用 stripProtoFields() 再次作为防守包罗万象。

// services/analytics/index.ts
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  // same reference when no _PROTO_ keys present
}

// In the 1P exporter — hoist then strip:
const { _PROTO_skill_name, _PROTO_plugin_name, _PROTO_marketplace_name, ...rest } = formatted.additional
const additionalMetadata = stripProtoFields(rest)  // catches future unknown _PROTO_* keys

events.push({
  event_type: 'ClaudeCodeInternalEvent',
  event_data: ClaudeCodeInternalEvent.toJSON({
    skill_name: typeof _PROTO_skill_name === 'string' ? _PROTO_skill_name : undefined,
    additional_metadata: Buffer.from(jsonStringify(additionalMetadata)).toString('base64'),
  }),
})
08 GrowthBook:动态配置和采样

GrowthBook 在分析堆栈中具有三个不同的用途:代码行为的功能标志、管道调整的动态配置以及实验分配跟踪。

特征门

tengu_log_datadog_events

启用或禁用每个用户/组织/推出的整个 Datadog 管道。 期间检查过一次 initializeAnalyticsGates() 并为会话缓存。

采样配置

tengu_event_sampling_config

JSON 配置将事件名称映射到 { sample_rate: 0–1 }。 事件不在 100% 的配置日志中。 速率 0 = 全部丢弃。 用途 getDynamicConfig_CACHED_MAY_BE_STALE.

批量配置

tengu_1p_event_batch_config

控制 OTel BatchLogRecordProcessor: scheduledDelayMillis, maxExportBatchSize, maxQueueSize, 端点, skipAuth。 可实时重新加载通过 reinitialize1PEventLoggingIfConfigChanged().

Killswitch

tengu_frond_boric

每个接收器分析终止开关的损坏名称。 形状: { datadog?: boolean, firstParty?: boolean }。 价值 true 立即停止所有发送到该接收器的操作。

shouldSampleEvent — GrowthBook 驱动的采样实现
// firstPartyEventLogger.ts
export function shouldSampleEvent(eventName: string): number | null {
  const config = getEventSamplingConfig()   // GrowthBook dynamic config
  const eventConfig = config[eventName]

  if (!eventConfig) return null            // not configured → log 100%

  const sampleRate = eventConfig.sample_rate
  if (typeof sampleRate !== 'number' || sampleRate < 0 || sampleRate > 1)
    return null                              // invalid → fail-open (log 100%)
  if (sampleRate >= 1) return null           // rate=1 → log 100%, no metadata overhead
  if (sampleRate <= 0) return 0             // rate=0 → drop

  return Math.random() < sampleRate ? sampleRate : 0
}

// Caller in sink.ts:
// null   → event passes through, no sample_rate metadata added
// 0      → event dropped
// 0–1   → event passes, sample_rate added to metadata for downstream correction

实时管道重新初始化

长时间运行的会话会自动获取批量配置更改。 当GrowthBook刷新时, reinitialize1PEventLoggingIfConfigChanged() 将新配置与 lastBatchConfig。 如果不同,它将清空记录器(因此并发发出看不到记录器并干净地保释),将旧缓冲区强制刷新到磁盘,交换新的提供程序,然后在后台关闭旧的提供程序。

09 面向客户的开放遥测

企业可以通过设置选择在自己的 OTLP 端点接收遥测数据 CLAUDE_CODE_ENABLE_TELEMETRY=1 and OTEL_EXPORTER_OTLP_ENDPOINT。 该路径使用 global OTel 提供商,完全独立于 1P 分析管道。

Signal 协议选项 Notes
Metrics otlp/grpc、otlp/http+json、otlp/http+proto、普罗米修斯、控制台 默认临时性:delta。 BigQuery 读取器的间隔为 60 秒,可供客户配置。
Logs otlp/grpc、otlp/http+json、otlp/http+proto、控制台 默认导出间隔5s。 除非 OTEL_LOG_USER_PROMPTS=1,否则用户提示已被编辑。
Traces otlp/grpc、otlp/http+json、otlp/http+proto、控制台 需要 CLAUDE_CODE_ENABLE_TELEMETRY + isEnhancedTelemetryEnabled()。 批处理跨度处理器。

BigQuery 指标导出器

API 客户、C4E(Claude for Enterprise)和 Teams 用户还可以获得 BigQueryMetricsExporter 发布到 /api/claude_code/metrics 每 5 分钟一次。 这与客户 OTLP 是分开的 — 无论是否运行,它都会运行 CLAUDE_CODE_ENABLE_TELEMETRY 已设置。

客户 OTLP 的遥测属性 — 基数控制
// utils/telemetryAttributes.ts
const METRICS_CARDINALITY_DEFAULTS = {
  OTEL_METRICS_INCLUDE_SESSION_ID:   true,   // included by default
  OTEL_METRICS_INCLUDE_VERSION:      false,  // excluded by default
  OTEL_METRICS_INCLUDE_ACCOUNT_UUID: true,   // included by default
}

export function getTelemetryAttributes(): Attributes {
  const attributes: Attributes = { 'user.id': getOrCreateUserID() }

  if (shouldIncludeAttribute('OTEL_METRICS_INCLUDE_SESSION_ID'))
    attributes['session.id'] = getSessionId()

  if (shouldIncludeAttribute('OTEL_METRICS_INCLUDE_VERSION'))
    attributes['app.version'] = MACRO.VERSION

  // OAuth account data only when actively using OAuth
  const oauthAccount = getOauthAccountInfo()
  if (oauthAccount) {
    if (oauthAccount.organizationUuid) attributes['organization.id'] = oauthAccount.organizationUuid
    if (oauthAccount.email)            attributes['user.email']    = oauthAccount.email
  }
  return attributes
}

默认情况下排除版本,以防止客户仪表板中出现高基数时间序列爆炸。 企业用户可以使用以下命令重新启用它 OTEL_METRICS_INCLUDE_VERSION=1.

10 政策限制和禁用条件

分析在四种情况下被禁用,已签入 isAnalyticsDisabled():

// services/analytics/config.ts
export function isAnalyticsDisabled(): boolean {
  return (
    process.env.NODE_ENV === 'test'                           ||
    isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)           ||  // AWS Bedrock
    isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX)            ||  // Google Vertex
    isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)           ||  // Azure Foundry
    isTelemetryDisabled()                                          // privacy setting
  )
}
3P 提供商的区别
反馈调查使用单独的检查, isFeedbackSurveyDisabled(),这确实 not 阻止 3P 提供商。 该调查是本地 UI 提示,没有文字记录数据; 企业客户通过 OTEL 而不是 Anthropic 的内部管道捕获响应。

Auth-Aware 事件调度

1P 导出器在每次 POST 之前探测身份验证状态。 如果 OAuth 令牌已过期,则缺少 user:profile 范围,或者信任对话框尚未被接受,它会回退到发送没有身份验证标头的事件。 401 响应还会触发无需身份验证的自动重试。 事件永远不会仅仅因为身份验证不可用而被丢弃——它们会降级为未经身份验证的传递。

Condition Behaviour Recovery
不接受信任对话框 跳过身份验证标头 接受信任后自动处理下一个事件
OAuth 令牌已过期 跳过身份验证标头 令牌刷新; 下一批使用新的标头
没有用户:配置文件范围 跳过身份验证标头 服务密钥会话 - 预计未经身份验证
401响应 无需授权立即重试 对调用者透明
5xx/超时 磁盘队列+二次退避 最多尝试 8 次; 此后事件下降
终止开关已激活 扔; 所有批次都排队到磁盘 当 GrowthBook 缓存清除标志时恢复

要点

  • 每个代币使用事件都会流经 addToTotalSessionCost() 它在单个同步调用中扇出到三个目的地:内存累加器、OTel 计数器和分析事件日志。
  • 分析接收器是无依赖性的,并使用队列然后排出模式,因此在启动过程中不会丢失任何事件,无论 GrowthBook 或 Datadog 初始化需要多长时间。
  • Datadog 和 1P 管道接收不同的有效负载:Datadog 获取 _PROTO_* 发货前钥匙已被拔除; 1P 导出器接收完整负载并将 PII 标记值路由到特权 BigQuery 列。
  • The never-键入的标记类型(AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS)创建编译时书面记录 - 没有运行时强制执行,但每个日志记录调用都需要代码审阅者必须批准的显式转换。
  • GrowthBook 控制三个独立的维度:哪些事件到达 Datadog(功能门)、每种事件类型的采样率(动态配置)以及是否完全终止任何接收器(每个接收器终止开关)。
  • 1P LoggerProvider 从未在全球注册 - 客户 OTLP 端点和内部 Anthropic 分析在提供商级别完全隔离。
  • 失败的事件将作为仅附加 JSONL 文件持久保留在磁盘上,通过二次退避重试,并在进程崩溃时幸存下来。 永久丢弃之前最多尝试 8 次。
  • 会话成本状态在每次退出时都会序列化为项目配置 JSON,并在恢复时重新水合 — 成本在整个过程中正确累积 /resume 会话数不重复计算。

知识检查

Q1. When logEvent() 之前被调用 attachAnalyticsSink() 已经运行了,事件会发生什么?

Q2. 什么是 shouldSampleEvent() 当 GrowthBook 配置没有事件名称条目时返回?

Q3. 为什么是 AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 类型定义为 never?

Q4. 下列哪项正确描述了 1P OTel 之间的关系 LoggerProvider 以及面向客户的 LoggerProvider?

Q5. 当事件发生时会发生什么 firstParty 接收器终止开关(tengu_frond_boric)被设置为 true?

Q6. 会话成本状态在退出时保存并在恢复时恢复。 什么可以防止从不同的会话恢复成本?

Q7. The getUserBucket() datadog.ts 中的函数有何用途?