GrowthBook:特性开关与实验数据闭环
一个新结算页面可以对 10% 用户开放,但“没有报错”只说明发布风险暂时可控,不能说明新页面提高了支付成功率。GrowthBook 把这两个问题放在同一套 feature definition 里,却保留两条不同链:SDK 根据 attributes 在本地决定某个 subject 得到哪个 value;tracking callback 把曝光交给现有事件系统,GrowthBook 再从数据源查询曝光、转化和指标。前一条链断了会影响用户看到什么,后一条链断了会让实验结论失真。工程上最重要的第一步,就是不把二者混成一个“开关成功”。
从评估链看懂所有对象
GrowthBook 控制面保存 feature、环境规则、实验配置、数据源和指标定义。应用中的 SDK Connection 提供读取 feature definitions 的端点和客户端标识。SDK 启动后拉取 definitions 并缓存;每次评估使用调用方提供的 attributes,按规则顺序检查条件、强制值、比例放量或实验规则,在进程内完成哈希与选组。官方SDK 概览明确区分“拉取并缓存定义”和“本地评估”两项职责,因此本地评估不是永不联网:规则同步、刷新和冷启动仍依赖配置端点或预置 payload。
下面的部署与代码示例以 GrowthBook 控制面 4.4.0 和浏览器 JavaScript SDK 1.6.5 为基线。两条版本线独立演进:升级控制面不等于应用 SDK 已升级,SDK Connection 中声明的语言与版本也必须对应真实客户端。GrowthBook 主仓库不是单一 MIT 许可:packages/back-end/src/enterprise、packages/front-end/enterprise 和 packages/shared/src/enterprise 采用 GrowthBook Enterprise License,其余受仓库根许可证约束的代码采用 MIT Expat;Cloud 是托管服务,自托管也分 Open Source 与商业 Enterprise 能力。选型时应把镜像版本、SDK 版本、代码目录许可证和套餐能力分别登记,不能用“开源”二字推导审批、SSO、SCIM、可导出审计或 SLA 已经可用。GrowthBook 4.4.0 Release GrowthBook License
feature key 是代码契约,例如 checkout-layout;feature value 可以是 boolean、string、number 或 JSON。feature definition 是按环境发布的规则集合。attributes 是评估上下文,例如稳定 id、country、plan、appVersion。实验规则还包含 experiment key、variations、weights、coverage、hash attribute 等。SDK 对同一组输入使用确定性哈希,所以相同 subject、experiment key 与哈希属性应稳定落入同一变体。
不要把 attributes 当用户档案库。SDK 只需要当前评估所需字段;浏览器 attributes 对用户可见且可篡改,不能承担授权、价格资格或资源访问控制。服务端可以从可信会话构造属性,但仍应限制字段与日志。稳定 ID 最好是内部不透明标识,邮箱既是个人信息,也会因修改而破坏稳定分桶。
一次完整实验的因果链是:feature definition 发布;SDK 用 subject attributes 命中实验并返回 variation;只有实际参与实验时,tracking callback 发送 assignment/exposure;事件进入分析系统或仓库;用户后续行为形成 conversion;GrowthBook 通过数据源凭据执行 SQL,把实验 key、variation、subject identity、时间窗口与 metric 定义关联,统计结果才出现。任何一个 join key、时间类型、时区或去重口径不一致,都可能让 UI 看起来有数据却得出错误结论。
Cloud 与自托管先做责任选择
GrowthBook Cloud 适合希望快速使用控制面、托管能力和持续升级的团队。自托管适合数据驻留、隔离网络、定制集成或平台责任可被内部承担的团队。自托管 Open Source 并不自动获得 Enterprise 目录中的能力;Cloud 的 Starter、Pro、Enterprise 与自托管 Open Source、Enterprise 也不是一张可以按名称互换的能力表。例如 Feature Approval Workflows、SSO/SCIM、可导出审计和更细权限属于需要单独确认的商业边界。两种部署模式都不会替团队自动修复埋点、身份和指标口径;Cloud 也不意味着必须把原始业务数据搬进去,具体数据路径要按连接方式决定。GrowthBook Pricing and Plan Matrix
官方自托管说明把应用描述为 Next.js 前端、Express API 与 Python stats engine,打包在一个镜像中,并使用 MongoDB 或兼容数据库保存登录凭据、缓存实验结果和元数据。开发练习可用 Compose:
services:
mongo:
image: mongo:<validated-version>
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: <local-only-password>
volumes:
- mongodata:/data/db
growthbook:
image: growthbook/growthbook:4.4.0
ports:
- "3000:3000"
- "3100:3100"
depends_on:
- mongo
environment:
MONGODB_URI: mongodb://root:<local-only-password>@mongo:27017/growthbook?authSource=admin
NODE_ENV: production
JWT_SECRET: <local-random-signing-secret>
ENCRYPTION_KEY: <local-random-credential-encryption-key>
volumes:
- uploads:/usr/local/src/app/packages/back-end/uploads
volumes:
mongodata:
uploads:docker compose up -d
docker compose ps
curl --fail --show-error http://localhost:3000/
docker compose logs --tail=200 growthbook3000 是 Web 应用入口,3100 是 API 相关入口,实际暴露方式应以目标发行版配置为准。首次进入后创建组织、项目、Development 环境和 SDK Connection,再创建 boolean feature checkout-layout-enabled。练习环境可使用本地 Mongo;生产不应照搬 latest、明文密码和单副本数据库。官方说明 latest 会跟随主分支每次提交更新;需要受控升级时应固定稳定 release tag 或镜像摘要。NODE_ENV=production、JWT_SECRET 与 ENCRYPTION_KEY 是生产安全基线,修改 ENCRYPTION_KEY 前必须执行凭据迁移,不能直接换值后重启。官方的生产部署建议还要求处理反向代理、共享上传存储、Mongo 高可用、备份、扩缩容与 SDK payload 分发层。
生产恢复点不只有 Mongo。还要保存镜像版本、环境变量、加密密钥、上传卷、OAuth/邮件配置、数据源凭据注入方式和外部仓库中的事件 schema。只恢复 Mongo 但更换了加密材料,已保存的数据源凭据可能无法解密;只恢复控制面而仓库 exposure 表缺失,实验历史也无法重算。
清理本地练习环境时:
docker compose down
# 明确不再需要本地组织、配置和上传内容后执行
docker compose down --volumes删除卷不可恢复。Cloud 或外部仓库中的测试事件不会随本地容器清理,要按租户和事件保留策略单独删除。
SDK 本地评估的可复制接入
以浏览器 JavaScript SDK 为例,安装并从控制台取得 SDK Connection 的 client key 与 API host。client key 会出现在浏览器,不能把管理 API key 或数据源凭据塞进前端。下面的 apiHost 和 key 都是占位符:
npm install @growthbook/growthbook@1.6.5import { GrowthBook } from '@growthbook/growthbook';
const growthbook = new GrowthBook({
apiHost: 'https://growthbook.example.invalid',
clientKey: '<sdk-client-key>',
attributes: {
id: getStableAnonymousOrUserId(),
country: getCountryCode(),
plan: getPlan(),
},
trackingCallback: (experiment, result) => {
analytics.track('Experiment Viewed', {
experimentId: experiment.key,
variationId: result.key,
subjectId: getStableAnonymousOrUserId(),
});
},
});
const initResult = await growthbook.init({ timeout: 1500 });
if (!initResult.success) {
console.warn('GrowthBook payload unavailable', {
source: initResult.source,
message: initResult.error?.message,
});
}
const enabled = growthbook.getFeatureValue(
'checkout-layout-enabled',
false,
);默认值 false 是调用方的故障决策:feature 缺失或定义尚未加载时,旧结算路径继续工作。网络失败时 init() 不会靠抛异常表达失败,而会返回 success=false 以及 source、error;忽略返回对象会把冷启动失败伪装成正常默认值。类型错误不能只依赖 fallback:远端 string 或 JSON 值可能已经进入 JavaScript 运行时,消费方仍要做枚举或 schema 校验。对 JSON feature,应给出完整且经过 schema 校验的旧配置,而不是 {}。初始化是否阻塞首屏取决于业务风险:关键服务端决策可在短预算内等待;非关键 UI 可以先渲染旧体验,规则到达后更新,但要防止页面闪烁和重复曝光。JavaScript SDK Loading and Error Handling
trackingCallback 在 SDK 评估命中实验时上报 assignment;业务必须把 feature 的评估位置放在真正影响用户的路径上,不能把“已分桶”自动解释为“已看见”。不要在每次 getFeatureValue 后手写一条曝光,否则可能把强制值、普通 rollout 或未命中实验的人误标成实验参与者。JavaScript SDK 1.6.5 会在单个 GrowthBook 实例内按 experiment/result 去重回调,并吞掉回调抛出的异常以保护求值;这个去重不跨浏览器标签、进程、服务端与客户端,也不能替代仓库幂等键。回调仍应快速、非阻塞,并对失败率、队列深度、丢弃和跨端重复建立证据。JavaScript SDK Experiment Tracking SDK Tracking Source
服务端应用应复用 SDK 实例和已缓存 definitions,不要每个请求创建实例再拉一次配置。请求级 attributes 放在请求上下文,避免并发请求相互覆盖全局 attributes。进程退出时停止刷新和等待事件队列的有限 flush;超过预算则记录丢弃数量后退出。
正向实验:证明稳定分桶和事件闭环
创建 string feature checkout-layout,默认值 classic;增加 experiment rule,key 为 checkout-layout-test,variations 为 classic 与 compact,各 50%,hash attribute 选择稳定 id。固定 20 个 subject:subject-0001 到 subject-0020。
第一轮对每个 subject 连续评估十次。预期同一 subject 十次结果完全一致,群体中两种 variation 都出现;小样本不要求恰好 10:10。重启进程并重新加载相同 definitions 后再次评估,预期每个 subject 与第一轮一致。把 feature key、experiment key 或 hash attribute 改掉会重新分桶,因此它们是迁移契约,不是随手可改的展示文本。
测试代码可以把这一性质固定下来:
import assert from 'node:assert/strict';
const subjects = Array.from({ length: 20 }, (_, i) => `subject-${i + 1}`);
for (const id of subjects) {
growthbook.setAttributes({ id, country: 'CN', plan: 'free' });
const values = Array.from({ length: 10 }, () =>
growthbook.getFeatureValue('checkout-layout', 'classic')
);
assert.equal(new Set(values).size, 1, `unstable bucket for ${id}`);
}然后触发一次真实页面展示,由 tracking callback 发送:
{
"event": "Experiment Viewed",
"experimentId": "checkout-layout-test",
"variationId": "compact",
"subjectId": "subject-0007"
}再让同一 subject 完成支付,仓库生成 conversion 事件:
{
"event": "Purchase Completed",
"subjectId": "subject-0007",
"orderId": "order-demo-0001",
"amount": 12800,
"currency": "CNY"
}预期证据分三层:应用日志证明 SDK 返回 compact;事件调试器或仓库查询证明曝光与转化使用同一 subjectId;GrowthBook 查询预览证明 exposure SQL 与 metric SQL 能在转换窗口内关联。控制台出现一条实验记录不是闭环证据。
feature definition、attributes 与稳定分桶的深水区
规则按顺序求值,前面的强制规则可能遮住后面的 experiment rule。排查“实验无人进入”时,要查看最终发布到目标环境的 definition,而不是只看实验页面。开发、预发和生产的规则可以不同;SDK Connection 若绑定错误环境,代码完全正确也会得到错误值。
attributes 的名称、类型和缺失语义必须版本化。plan="pro" 与 plan=true 不是一回事;age=18 与 age="18" 可能让数值条件表现不同;缺失属性通常意味着条件不匹配,而不是自动使用用户库中的旧值。建立上下文 schema,并在应用边界做类型转换。浏览器能得到的 attributes 应假定对终端用户可见。
稳定分桶依赖稳定 hash attribute。匿名用户先用设备随机 ID,登录后改为账户 ID,会发生身份切换;跨设备则可能属于不同桶。可选策略包括始终使用账户 ID、接受匿名到登录时重新评估,或在登录后使用 sticky bucketing 保存既有 assignment。选择必须与实验单位一致:面向组织的 B2B 功能可能应按 companyId 分桶,按个人 ID 会让同一组织同时看到两套流程并互相影响。
GrowthBook 的Sticky Bucketing通过持久 assignment 降低规则变化、权重调整或身份属性变化造成的换组风险,产品能力属于 Pro/Enterprise;自托管 Open Source 不能因为 SDK 暴露了 sticky service 接口就推导控制面能力已经获得许可。它也不是免费的稳定性按钮:要选择 document store、处理读取延迟和失败、定义跨设备同步与删除策略,并确保实验重启或 key 复用不会错误继承旧 assignment。普通确定性哈希已能保证输入不变时稳定;只有输入、范围或实验配置会变化且换组不可接受时,才承担 sticky store 的复杂度。
扩大 coverage 时尽量保持 variation weights 不变。若从“总覆盖 20%、两组各半”增加到“总覆盖 50%、两组各半”,已入组 subject 通常保持原 variation,新 subject 进入;直接把两组权重从 80:20 改成 50:50,部分 subject 可能换组,污染归因。任何规则、权重、hash attribute 或 variation 顺序变更都应被当作实验协议变更并留审计。
普通放量与实验不是同一件事
普通 percentage rollout 的目标是限制风险:稳定地让一部分 subject 获得新值,观察错误率、延迟和支持反馈,然后逐步扩大。它可以没有 tracking callback,也不需要统计显著性。实验的目标是估计因果影响:需要明确假设、对照组、实验单位、曝光时刻、指标、转换窗口、样本量与停止规则。
同一个 feature 可以承载两者,但语义不能混。把 10% rollout 自动叫作 A/B 实验,会在没有曝光事件和指标设计时制造虚假科学感;把实验当放量开关频繁调权重,又会造成换组与选择偏差。发布止损看工程 guardrail,实验决策看预先定义的目标指标与护栏指标。
实验结束有三种动作:采用新变体,把 feature 固定为 winner;保留旧变体,恢复 control;结论不确定,停止并重新设计。无论哪种都要先停止新曝光、保留可重算的数据窗口,再清理规则与代码。不能为了“结果更好看”在看到中间结果后临时换主指标或过滤人群。
曝光、转化、指标与数据源链
GrowthBook 提供三种数据路径。官方数据路径说明列出 Managed Warehouse、Event Forwarder 与 Bring Your Own Warehouse:Managed Warehouse 使用平台托管的 ClickHouse 和内置事件采集;Event Forwarder 把曝光和自定义事件写入团队仓库,但当前属于 Pro/Enterprise 且只有明确列出的仓库可用;BYOW 使用团队已有采集链并由 GrowthBook 查询,所有计划可用,也是自托管的常规选择。一个组织可以并存多条数据路径,迁移时必须防止同一曝光被两条链重复摄取。能力、支持仓库与费用会变化,架构记录应保存所选路径、目标计划和数据归属,不能只写“已接 GrowthBook”。
BYOW 的核心优点是原始行为数据留在已有仓库,常规查询只需要只读凭据;启用 Pipeline Mode 是明确例外,GrowthBook 需要对指定专用 schema 写入临时分析表。不能一边启用 Pipeline Mode,一边把全库只读失败误判成网络故障;也不能为省事授予全库 DDL/DML。连接凭据应限制到必要 schema、查询与临时表动作,并通过网络边界、查询超时和审计约束。对生产大表先用分区与聚合表控制扫描量,给实验查询设置资源组、并发上限和成本告警。Self-hosted Data Source Permissions
曝光表至少要稳定表达 subject、experiment、variation、timestamp;转化事实要表达同一 subject、事件时间和去重键。Metric 或 fact table 再定义过滤条件、数值聚合、窗口和去重。官方Metrics and Fact Tables支持在事实模型之上复用指标,但复用不代表口径天然正确:订单创建与支付完成、订单金额与净收入、用户数与事件数会给出完全不同答案。
先在仓库直接运行三条审计查询:各 variation 的唯一曝光 subject 数;曝光记录中 subject 或 variation 为空的比例;曝光后转换窗口内可关联转化的 subject 数。再看 GrowthBook 生成 SQL 与扫描行数。若 SDK 回调有事件但 GrowthBook 结果为零,依次检查数据延迟、数据源连接、实验 key、variation key、identity type、时区和实验开始时间。
曝光不是“页面加载”,而是 subject 真正受到某个变体影响的时点。过早曝光会把未看到新体验的人纳入实验,稀释效果;过晚曝光会漏掉已经受影响的行为。复杂场景可以用 activation metric 进一步筛出真正触达的人,但它不能修复完全错误的曝光埋点。
反向实验:主动制造数据断链
第一组实验移除 trackingCallback。预期 feature 仍按稳定规则返回 variation,页面功能正常,但事件系统没有新的 exposure,GrowthBook 无法分析新增 subject。这证明“SDK 评估成功”和“实验可分析”是两项独立 SLO。监控应同时看 definition 刷新成功率、evaluation fallback 次数和 exposure 发送成功率。
第二组实验让 callback 抛异常或把分析端点指向不可达地址。JavaScript SDK 的安全调用会保护评估结果不被 callback 异常推翻,但 SDK 不会替业务凭空建立可靠事件队列;若 callback 只是调用一个不返回 Promise 的分析客户端,后续异步失败甚至不会被 SDK 看见。主请求应继续返回,由团队实现的有界队列记录失败、容量和丢弃;不能无限重试挤爆内存,也不能同步等待分析系统。恢复网络后只补发仍在保留窗口内且带幂等键的事件,避免重复曝光。
第三组实验把曝光里的 subjectId 写成字符串账户 ID,把转化里的 ID 写成数值数据库主键,或者匿名阶段与登录后使用不同 ID。预期两张表各自都有数据,但 join 命中率接近零。这是最危险的“绿灯故障”:采集链看起来健康,实验结果却系统性偏差。修复需要统一 identity contract,不能用模糊邮箱匹配补救。
第四组实验删除 plan attribute 或把类型改错。预期目标条件不命中,subject 得到 default 或后续规则结果,且不应被记为该 experiment 的曝光。若仍产生实验事件,检查手写埋点是否脱离 SDK 结果。
第五组实验在 SDK 成功初始化后阻断 feature endpoint。预期缓存 definitions 继续服务,配置 age 上升,新发布规则暂不出现;冷启动且无缓存时应使用代码默认值。恢复连接后观察 definitions 版本或更新时间变化。只验证“断网后还能返回值”不够,还要辨认返回的是 last-known-good 还是 default。
第六组实验故意重复调用评估并多次渲染组件。检查 tracking callback 与下游去重:同一 subject 在同一实验中的首次有效曝光口径应可解释。若事件数是唯一 subject 的几十倍,仓库成本和统计权重都会被扭曲。
故障退化与项目接入边界
在应用中建立 ExperimentDecision 适配器,统一 feature key、默认值、attributes schema 和可观测字段:
export function checkoutLayout(gb, subject, logger) {
if (!subject?.id) {
logger.warn({ feature: 'checkout-layout', reason: 'missing_subject' });
return 'classic';
}
gb.setAttributes({
id: String(subject.id),
country: subject.country ?? 'unknown',
plan: subject.plan ?? 'free',
});
const value = gb.getFeatureValue('checkout-layout', 'classic');
if (!['classic', 'compact'].includes(value)) {
logger.error({ feature: 'checkout-layout', reason: 'invalid_value' });
return 'classic';
}
return value;
}浏览器单例可以这样更新 attributes;并发服务端不要在共享实例上用可变全局属性,要使用该语言 SDK 提供的请求级上下文或不可变评估入口。适配器测试至少包含定义未加载、feature 缺失、属性缺失、强制值、普通 rollout、实验命中、非法 value 和 callback 失败。
默认值按业务损失方向设计。支付、写数据、权限相关路径通常回到旧行为;纯展示增强可以隐藏;运维 kill switch 可能需要默认开启保护。每个 feature 都单独决策,禁止全局规定“异常一律 false”。默认值还必须与当前部署版本兼容,旧版本不能解析新 JSON 时应拒绝而非猜测。
配置刷新失败告警至少包含连续失败次数、last successful refresh age 和 fallback 计数。事件链告警包含 callback error、队列丢弃、仓库摄取延迟、曝光空 ID 比例、variation 未知比例、join rate 和查询失败。低基数标签使用 feature/experiment key,不把 subject 放进指标标签。
回滚时先判断故障属于哪条链。功能故障就把 feature 固定到旧值或降低 coverage,验证各 SDK 在刷新窗口内恢复;分析故障则暂停实验决策、修复曝光或指标,但未必需要关闭用户功能。若数据已经污染,记录污染窗口并重跑查询,不能用控制台上改几个筛选条件假装历史未受影响。
隐私、安全与真实成本
GrowthBook SDK 本地评估减少了逐请求把 attributes 发往评估服务的需要,但前端 feature payload 可能暴露规则、内部 feature 名和目标条件。敏感规则应在服务端评估,或评估目标发行版的 remote evaluation 能力与泄露边界。无论哪种方式,客户端返回都不具备授权效力。
tracking callback 由团队实现,事件去哪里、保存多久、是否含个人信息都由该实现和数据路径决定。只发送实验分析所需的 subject、experiment、variation、timestamp 和必要维度;禁止把完整 attributes、邮箱、姓名、IP、请求体一股脑塞入事件。subject 删除请求需要贯穿事件系统、仓库、托管数据路径、缓存和 sticky assignment store。
自托管不等于没有外联。逐项审计镜像拉取、错误遥测、更新检查、OAuth、邮件、CDN、SDK endpoint、数据仓库与第三方分析。用出口防火墙和流量日志验证。Cloud/BYOW 则要确认查询结果、样本数据和凭据如何处理,并根据企业要求完成密钥轮换与审计。
成本有四层:控制面席位与能力套餐;feature payload 分发和代理流量;曝光事件摄取与保留;数据仓库查询扫描。实验越多、维度越高、重复曝光越严重,后两层增长越快。用项目 scope 缩小 SDK payload,用事件去重和采样控制非关键诊断事件,用事实表与增量模型降低扫描;但核心实验曝光不能随意采样,否则统计口径也要同步建模。
团队治理与退出路径
每个 feature 创建时登记 owner、环境、类型、默认值、hash attribute、风险方向、预计结束点和删除工单。experiment 还要登记假设、实验单位、目标指标、guardrail、最小样本、转换窗口、数据延迟与停止规则。指标定义变更必须版本化,不能静默覆盖正在运行的实验口径。
发布前由工程、产品和数据三方分别验收:工程确认 fallback、稳定 ID、回滚和兼容;产品确认受众与变体;数据确认曝光时刻、identity join、metric SQL 和成本。生产发布先用内部或 QA 条件验证,再开放实验流量。真实用户实验期间限制规则和 variation 顺序变更,所有例外留下审计。
实验结束后先停止分配并固化决策,保留足够的数据延迟窗口,归档查询与结论;随后把 winner 写成普通产品行为,删除 loser 分支、tracking 代码和 feature 引用;部署完成并确认旧版本退出后,再归档 feature 与实验。直接先删控制面对象会让尚未升级的实例走 default,产生第二次非计划实验。
长期盘点同时看代码引用、definitions 分发、实际评估、曝光事件和 owner。一个 feature 没有曝光不一定没用,它可能只是普通 rollout;一个 feature 有大量评估也不代表仍需保留,它可能已全量固定数月。清理判断要把代码和控制面证据合并。
选型落点也由责任决定:只需要低风险开关且已有分析平台,GrowthBook 可以只承担 definitions 与本地评估;需要可信实验结论,就必须投资 identity、曝光、指标和仓库链;选择 Cloud 减少控制面运维,选择自托管则接手 Mongo、应用密钥、升级、备份和可用性;选择托管数据路径降低起步门槛,BYOW 增加数据控制同时带来查询与管道成本。只有当用户得到的值、事件记录的曝光和仓库计算的指标能由同一个稳定 subject 串起来,渐进交付才真正升级成可验证的产品决策系统。
