为 QA 强制指定某个 A/B 测试或功能开关变体
A/B 测试和功能开关对产品团队很好用,对 QA 却很折磨。你被随机分桶一次,之后就再也看不到另一个变体了。本文介绍如何夺回控制权:找到网站用来给你分桶的那个值,在页面读取它之前覆盖掉它,再用一条 JustZix 规则按 URL 模式固定某个变体。
网站如何给你分桶
几乎所有客户端实验工具 —— Optimizely、GrowthBook、LaunchDarkly、Statsig、Split,以及自研方案 —— 工作原理都一样。你第一次访问时它掷一次骰子,把结果写到某个持久化的地方,之后每次访问都从同一个地方读取,让你的体验保持稳定。这个「某个持久化的地方」几乎总是以下三者之一:
- 一个 cookie(例如
ab_variant=B,或一个会被哈希成分桶的匿名用户 ID)。 - 一个 localStorage 键(GrowthBook 和 Statsig 的 SDK 常用)。
- 一个 服务端决策,固化在 HTML 里或由某个 API 调用返回。
前两者你可以直接覆盖。第三种更难 —— 下文细说。
找到分桶用的键
打开 DevTools,进入 Application 标签。在 Storage 下,Cookies 和 Local Storage 并排展示。重新加载页面,扫一眼有没有像实验的东西:含有 ab、exp、variant、flag、bucket、test、feature 的键,或者像 optimizely、growthbook 这样的厂商名。
如果没有明显的东西,那么决策是通过网络送达的。打开 Network 标签,筛选到 Fetch/XHR,重新加载,找像 /api/flags、/decide 或 config.json 这样的请求。响应 JSON 会告诉你变体名 —— 关键的是 —— SDK 把它们存在哪个键下。
在脚本读取之前覆盖 localStorage
时机就是一切。实验 SDK 很早就读取它的键,通常在 DOMContentLoaded 之前。你的 JustZix JS 规则必须在那次读取之前运行。把规则设为在 document_start 运行,并立即写入值:
// Pin the GrowthBook-style variant before the SDK initialises
const KEY = 'gb_anonymous_id'; // the bucketing key you found
const FORCED = 'qa-pinned-variant-b';
// Overwrite whatever value the SDK would have rolled
localStorage.setItem(KEY, FORCED);
// Some SDKs cache a whole decision object — pin that too
localStorage.setItem('growthbook_features', JSON.stringify({
'new-checkout': { defaultValue: true },
'pricing-table-v2': { defaultValue: 'variant-b' }
}));
对匿名 ID 这种方式,挑一个稳定、非随机的 ID:大多数 SDK 会把那个 ID 哈希成分桶,所以同一个字符串总会落到同一个变体。试几个 ID,记下哪一个给你变体 B,然后永远复用它。
覆盖一个 cookie
Cookie 更简单 —— 中间没有 SDK,只是一个字符串。在 document_start 写入它,让页面自己的脚本看到你的值:
// Force the experiment cookie for this domain
function setCookie(name, value) {
document.cookie =
name + '=' + value + '; path=/; max-age=31536000; SameSite=Lax';
}
setCookie('ab_variant', 'B');
setCookie('feature_new_nav', 'on');
如果 cookie 是 HttpOnly 的,你根本无法从 JavaScript 触碰它 —— 那是一个服务端开关,下面会讲。你可以在 Application 标签里确认这一点:HttpOnly cookie 会在那一列显示一个勾。
处理服务端开关
当变体由服务端决定时,你收到的 HTML 已经是那个变体了。没有客户端的值可以翻转。你有两个诚实的选项:
- 发送一个服务端尊重的提示。许多方案会读取一个非 HttpOnly 的覆盖 cookie,比如
x-force-variant,或一个查询参数?ab_force=B,专门让 QA 能测试分支。问问你的团队 —— 这些覆盖通常存在,只是没有文档。 - 给你已有的变体重新换皮。如果你拿不到真正的变体 B 标记,有时可以用一条 CSS 规则近似模拟可见的差异以便截图 —— 但那是个模型,不是真正的测试。只在设计评审时用,绝不用于功能 QA。
查询参数覆盖是最干净的路径。如果你的站点支持 ?ff_override=new-checkout:true,你甚至不需要 JavaScript —— 把那个 URL 加书签即可。
按 URL 模式固定一个变体
JustZix 真正的胜出之处在于作用域控制。你不希望每个站点都被同样分桶 —— 你希望这个预发布环境固定到变体 B,而那个保持不动。创建一条带紧凑 URL 模式的规则:
URL pattern: https://staging.example.com/*
JS (document_start):
localStorage.setItem('exp_checkout', 'variant-b');
document.cookie = 'ab_force=B; path=/';
每个变体做一条规则 —— 「固定结账 A」、「固定结账 B」、「固定对照组」 —— 然后从动作栏切换它们。把全部三条分支跑一遍 QA 就变成三次点击,而不是三个隐身窗口外加大量运气。
验证变体确实生效了
不要因为页面看起来不同了就相信覆盖生效了。要确认它。大多数 SDK 会在某处暴露当前的分配 —— 从 Output Console 把它打印出来:
// Read back what the SDK decided, after it initialised
window.addEventListener('load', () => {
// GrowthBook example
if (window.growthbook) {
console.log('Active features:', window.growthbook.getFeatures());
}
// Generic: dump every storage key for the audit trail
console.log('Forced storage:', { ...localStorage });
});
把它和 Output Console 窗口搭配使用,这样在你点击走完流程时,分配在标签页内是可见的。如果 SDK 报告的是你强制的变体,你的测试就是真实的。如果它报告的是别的东西,说明你的规则跑得太晚 —— 把它推到 document_start。
常见陷阱
- 跑得太晚。一条在
document_idle的规则会输掉这场赛跑。SDK 已经读过它的键了。分桶覆盖永远用document_start。 - 忘了服务端那一半。你固定了客户端变体,但服务端仍然渲染对照组。找一个覆盖 cookie 或参数。
- 污染分析数据。被强制的会话会扭曲实验结果。用预发布环境,或者让你的数据团队排除 QA 流量。
- 陈旧缓存。有些 SDK 会把决策缓存数小时。先清掉相关的 storage 键,再设置你强制的值。
另见
- 通过拦截 fetch 模拟 API 响应 —— 伪造开关接口,而不是 storage 键。
- 基于时间的规则 —— 按日期或小时安排固定哪个变体。
- JustZix 应用场景 —— 更多 QA 与调试工作流。
别再依赖隐身窗口和好运掷骰了。安装 JustZix,为每个变体建一条规则,按需演练每个实验的每条分支。
为这篇文章评分
暂无评分 — 成为第一个。