本章总览
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。
建议学习步骤
- 阅读源码块 A:logEvent 与 attachAnalyticsSink
- 阅读源码块 B:stripProtoFields 与类型标记
- 阅读源码块 C:sink 路由与 Datadog gate
- 阅读源码块 D:getFeatureValue_CACHED_MAY_BE_STALE
- 阅读源码块 E:shouldSampleEvent
- 阅读源码块 F:trackDatadogEvent allowlist
- 在源码树打开 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_ 键时返回原对象引用零分配。用于:
- sink.ts Datadog fanout 前
- 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 流程:
shouldSampleEvent→ 返回 0 则丢弃;返回 (0,1] 则写入sample_ratemetadatashouldTrackDatadog():sinkKillswitch +tengu_log_datadog_eventsgate(initializeAnalyticsGates 刷新,未 init 时读 disk cache)- Datadog:
trackDatadogEvent(event, stripProtoFields(metadata)) - 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 读取顺序:
- env overrides(eval harness)
- config overrides(本地调试)
isGrowthBookEnabled()false → defaultValue- 记录 experiment exposure(pendingExposures 若 init 未完成)
remoteEvalFeatureValues内存(init 后权威)- 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)。
动手练习
- 在 ant 环境启动 CLI,检查 analytics_sink_attached 的 queued_event_count
- 给 tengu_event_sampling_config 设某事件 sample_rate=0.1,验证 sample_rate metadata
- 对比 logEvent 带 _PROTO_email 时 Datadog payload 是否无该键
- 读 initializeAnalyticsGates 调用栈(main.tsx setupBackend)确认与 GrowthBook init 顺序
本章小结与延伸
analytics = 可观测性与动态配置。读 api-claude / compact 章中的 logEvent 调用时可回到本章理解采样与 kill switch。 继续学习: