A/B 测试:GrowthBook 控制 A/B 测试集
你的连锁餐厅上新菜时,不会一次在所有门店同时推出——先只在几家试点门店的菜单上出现,观察反馈没问题后再全量铺开(功能开关)。同时,为了测试新点餐界面,把会员随机分成两组,一组用界面 A,一组用界面 B,一个月后看哪组下单率更高(A/B 实验)。菜单显示什么由总部配置决定,门店服务员(代码)只需查一下"这道菜今天在我这开了吗",不需要自己判断。
这两种能力在 Claude Code 里由 GrowthBook 提供。从代码上看,src/services/analytics/growthbook.ts 在初始化时把用户属性上传,用于决定"这个用户该看到哪个版本":
// src/services/analytics/growthbook.ts
export type GrowthBookUserAttributes = {
id: string
sessionId: string
deviceID: string
platform: 'win32' | 'darwin' | 'linux'
organizationUUID?: string
accountUUID?: string
userType?: string
subscriptionType?: string // 订阅类型(免费/Pro/Max)——相当于会员等级
rateLimitTier?: string // API 速率限制等级
email?: string
appVersion?: string
github?: GitHubActionsMetadata
}
subscriptionType 和 rateLimitTier 是产品做差异化体验的关键字段——比如 Pro 用户提前看到某个实验性功能,就像餐厅金卡会员才能看到隐藏菜单。这些判断全在 GrowthBook 的配置里,代码里不需要写 if (user.isPro) { ... }。
缓存可能过时
这份配置不是实时拉取的。就像菜单每天早上印好发给各门店,白天服务员查的都是这份纸质版,如果某道菜上午卖完了,菜单上还显示着,下午才来的顾客点了之后才发现没有。GrowthBook 定期从服务器拉取最新配置,但两次拉取之间,代码读到的是上次缓存的值。这正是函数名里刻意加入 _CACHED_MAY_BE_STALE 后缀的原因——提醒调用方"你拿到的不一定是最新的"。对功能开关而言这完全可以接受:配置变更不需要毫秒级生效,下次会话或下次拉取时生效就够了。
实验曝光去重
你的餐厅做"新菜满意度调研",规定同一位会员只需统计一次"参与过调研",不管他来了多少次。如果每次扫码点餐都重复记录,一个常客可能被计入几十次,让调研数据完全失真。
loggedExposures 做的是同样的事——记录"哪些实验已经计过曝光",确保同一个实验在一次会话里只记录一次"进入实验组":
// src/services/analytics/growthbook.ts
const loggedExposures = new Set<string>()