Subotiz A/B 实验前端 SDK。在浏览器中拉取实验分组数据、支持按需上报转化事件。
- 浏览器优先,不依赖任何 UI 框架(React / Vue / 原生页面均可)
- 原生 TypeScript 支持,类型完整
通过 CDN 加载
可通过 CDN 引入 SDK。
CDN 模式调用语法:abSubotiz(方法名, ...参数)
<!-- 1. 同步定义全局代理与方法队列 -->
<script>
window.abSubotizQueue = window.abSubotizQueue || [];
window.abSubotiz = window.abSubotiz || function () {
window.abSubotizQueue.push([].slice.call(arguments));
};
// 2. 立即开始排队调用
abSubotiz('init', {
storeId: 'your_store_id',
});
abSubotiz('onReady', function (experiments) {
var hero = experiments['homepage_hero_test'];
if (hero && hero.variantKey === 'variant_b') {
document.documentElement.dataset.heroVariant = 'b';
}
});
</script>
<!-- 3. 异步加载真实 SDK,加载完成后会自动回放队列 -->
<script async src="https://cdn.subotiz.com/ab-sdk-subotiz/v1/ab-sdk-subotiz.min.js"></script>上报事件
业务事件通过 track(eventName, properties?) 上报,常用于实验转化分析(注册完成、按钮点击、自有收入 / LTV 等平台无法自动采集的指标):
abSubotiz.track('signup_complete'); // 计数 / 去重类指标
abSubotiz.track('add_to_cart', { source: 'cta' }); // 额外标签随事件透传
abSubotiz.track('ltv', { value: 99.9 }); // 数值类指标(求和 / 均值)
abSubotiz.track('checkout_success', { value: 99, currency: 'USD' }); // value + 额外标签特性:
- 永远上报、永不丢弃——SDK 不在客户端做白名单拦截,任意
eventName都会发往后端。是否计入某个实验由后端归因,无需前端关心 - 永远成功返回,不抛出异常
- 网络失败 / 5xx → 入
localStorage离线队列,回到在线状态时自动重发 - 4xx → 静默丢弃(事件本身不合法)
init未完成时调用 → 静默跳过
不要在事件参数中传用户隐私数据(手机号、邮箱、身份证、地址等)。SDK 不做过滤,由调用方自行约束。
数值指标:properties.value
properties.value需要求和 / 平均值类聚合的指标(如收入、LTV、停留时长),把数值放在 properties.value 上:
abSubotiz.track('ltv', { value: 99.9, tier: 'pro' });value必须是有限数字(typeof === 'number'且非NaN/Infinity)才会被后端纳入数值聚合。- 非法的
value(字符串'99.9'、NaN、null等)不会污染数值统计,会作为普通标签原样透传,由后端按"非数值"降级处理。 - 计数 / 去重类指标(如
signup_complete)无需传value,直接track('signup_complete')即可。 properties中除value外的其他字段都作为附加标签随事件上报,字段名自定。
调试:指标白名单提示
后台为实验声明的指标 key 会随分桶结果下发,SDK 在 debug: true 时据此做拼写提示:当 track 的 eventName 不在任何运行中实验声明的指标里,控制台会 warn(提示可能拼写错误 / 大小写不符,并打印当前白名单)。
这只是调试提示——事件依然正常上报。生产模式(debug: false)下完全无此开销,不影响上报行为。
曝光事件
实验首次命中时 SDK 会自动上报曝光事件,不需要手动调用。粘性缓存中已存在的实验不重复上报。
Pricing 链接自动注入
实验对象类型为 price_list 时,SDK 会在返回的价格表链接中,追加以下 query 参数。业务方拿到的链接已经是注入完毕的:
| 参数 | 值 |
|---|---|
ab_sbt_sid | 当前商家 id |
ab_sbt_uid | 当前用户 id |
ab_sbt_utype | 'user' 或 'anonymous' |
ab_sbt_sch | 来源渠道(仅当 init 传入了合法 sourceChannel 时才追加,见下文) |
// → https://checkout.subotiz.com/pricing-v2?ab_sbt_sid=store_123&ab_sbt_uid=user_456&ab_sbt_utype=user
const exp = experiments['pricing_redirect_test'];
if (exp && exp.objectType === 'price_list') {
window.location.href = exp.config!.link as string;
}来源渠道定向 (sourceChannel)
init 时可传入 sourceChannel,用于让后端按流量来源做实验定向分流(例如"仅对来自 facebook 的流量开启某实验")。其余定向维度(设备、语言、国家、IP)由后端从 HTTP 请求头自动解析,SDK 不采集任何浏览器指纹 / UA / 地理位置信息。
abSubotiz.init({
storeId: 'your_store_id',
sourceChannel: 'facebook',
});传入后,SDK 会在两处携带该值:
- 分桶请求:写入
POST /sdk/v1/ab/assign请求体的attributes.source_channel,供后端定向规则匹配。 - Pricing 链接:对
price_list类型实验的链接追加ab_sbt_schquery 参数(与上面的值同源)。
取值与归一化规则
SDK 不做枚举校验、不做大小写转换,取值口径(如 facebook / google / tiktok)由你与产品侧自行约定。携带前仅做如下处理:
| 传入值 | 处理结果 |
|---|---|
| 正常字符串 | trim 后携带(保留原始大小写) |
undefined / null / 非字符串 | 视为未传,不携带 source_channel 字段(不是空串占位) |
| 空字符串 / 纯空白 | 视为未传,不携带 |
| trim 后长度 ≥ 50 | 视为未传,不携带(debug: true 时控制台 warn 提示超长) |
稳定性:
sourceChannel在单次会话内被视为稳定值,仅init时确定一次。后续不监听变化、不会因渠道变化重新拉取分桶。未携带优于截断:超长 / 非法值一律不携带,避免后端拿到被静默改写的值后与你的埋点配置产生偏差。
API 参考
init(config)
init(config)初始化 SDK。在每个 SDK 实例上只能调用一次,重复调用会被忽略。立即返回,不阻塞主线程。
- 参数
config: SubotizSDKConfig - 返回
void
配置项
init(config) 接受一个 SubotizSDKConfig 对象:
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
storeId | string | — | 必填。商家 ID(Subotiz 后台店铺 access no) |
sourceChannel | string | — | 可选。来源渠道,用于后端流量定向。详见 来源渠道定向 |
timeout | number | 30000 | 分桶请求超时毫秒数 |
debug | boolean | false | 打开调试日志,并允许 forceVariant 生效。生产环境保持关闭 |
onReady(onSuccess, onError?)
onReady(onSuccess, onError?)注册就绪回调。SDK 已就绪时同步触发,未就绪时异步触发。可重复注册多个。
abSubotiz.onReady(
(experiments) => { /* 实验数据已就绪 */ },
(error) => { /* 网络异常且无缓存 */ },
);- 参数
onSuccess: (experiments: SubotizExperimentMap) => voidonError?: (error: ABTestError) => void
- 返回
void
waitReady()
waitReady()onReady 的 Promise 版本。
const experiments = await abSubotiz.waitReady();- 返回
Promise<SubotizExperimentMap> - 失败时:抛出
ABTestError(与onReady的onError等价)
isReady()
isReady()同步检查 SDK 是否已就绪。
- 返回
boolean
getExperiments()
getExperiments()同步获取当前所有命中实验。
- 返回
SubotizExperimentMap | nullnull—— 未初始化、未就绪、或当前没有任何命中实验Record<string, SubotizExperiment>—— 实验映射表,键为experimentId
track(eventName, properties?)
track(eventName, properties?)上报自定义业务事件。任意事件都会上报,是否计入实验由后端归因。
abSubotiz.track('signup_complete'); // 计数 / 去重
abSubotiz.track('ltv', { value: 99.9, tier: 'pro' }); // 求和 / 均值,value 必须是有限数字- 参数
eventName: string—— 事件名properties?: Record<string, unknown>—— 事件附加属性。其中value(有限数字)用于求和 / 均值类指标;其余字段作为附加标签透传
- 返回
void(永不抛错) - 行为
- 永远上报,不在客户端做白名单丢弃
debug: true且eventName不在后台声明的指标白名单中 → 控制台warn拼写提示(仅提示,不影响上报)init未完成时调用 → 静默跳过
forceVariant(experimentId, variantKey)
forceVariant(experimentId, variantKey)仅 debug: true 时生效。本地强制覆盖某实验的变体,便于开发期验证不同变体的 UI。
abSubotiz.forceVariant('homepage_hero_test', 'variant_b');- 参数
experimentId: string、variantKey: string - 返回
void
错误处理
onReady(_, onError) 与 waitReady() 失败时返回 ABTestError:
interface ABTestError {
code: ABTestErrorCode;
message: string;
cause?: unknown;
}错误码:
| 错误码 | 含义 |
|---|---|
NETWORK_TIMEOUT | 请求超时(弱网) |
NETWORK_ERROR | 网络请求失败(离线、跨域被拦截) |
HTTP_ERROR | 后端返回非 2xx |
INVALID_RESPONSE | 响应格式异常 |
NOT_INITIALIZED | init 校验失败(如缺少 storeId) |
abSubotiz.onReady(
(experiments) => render(experiments),
(error) => {
if (error.code === "NOT_INITIALIZED") {
console.error('init 配置非法', error.message);
}
// do something
},
);降级策略
SDK 内置降级策略,业务方只需关心三件事:
- 首次渲染走对照组 ——
await waitReady()在网络异常时最长会等timeout秒,不要用它阻塞首屏 onReady/waitReady中切换变体 —— 用户视觉冲击最小onError时保持对照组 —— 不展示空白或异常 UI
| 场景 | SDK 行为 |
|---|---|
| 二次访问 + 网络异常 | 自动用 localStorage 缓存进入就绪态 |
| 首次访问 + 网络异常 | 触发 onError |
track 时 5xx / 离线 | 入离线队列,window.online 时自动重发 |
track 时 4xx | 静默丢弃 |
调试
打开 debug: true 后,SDK 会在控制台输出请求与队列状态:
abSubotiz.init({ storeId: 'store_123', debug: true });| 日志 | 含义 |
|---|---|
[ab-sdk] reportExposure: sending N event(s) | 曝光上报中 |
[ab-sdk] track: sending N event(s) | 自定义事件上报中 |
[ab-sdk] track event queued (offline) | 事件入离线队列 |
[ab-sdk] 4xx error, track event dropped | 4xx 静默丢弃 |
[ab-sdk] MetricKeyRegistry rebuilt: N keys | 分桶成功后重建指标白名单(含 key 预览) |
[ab-sdk] track('xxx') did not match the metric_keys ... | track 事件名未命中白名单的拼写提示(仍会上报) |
[ab-sdk-subotiz] sourceChannel length N >= 50, omitted ... | sourceChannel 超长被忽略 |
[ab-sdk] Forced variant: xxx → yyy | forceVariant 调用 |
重置 SDK 状态
Object.keys(localStorage)
.filter((k) => k.startsWith('_ab_sdk_') || k.startsWith('ab_event_queue_'))
.forEach((k) => localStorage.removeItem(k));本地存储键
SDK 在 localStorage 中使用以下键:
| 键名 | 内容 |
|---|---|
_ab_sdk_uuid | 匿名用户 UUID(跨租户共享) |
_ab_sdk_exp_{storeId}_USERID | 实验分配快照(有效期 60 天) |
ab_event_queue_{storeId}_USERID_track | 离线事件上报队列 |
ab_event_queue_{storeId}_USERID_exposure | 离线曝光事件队列 |
常见问题
SDK 加载之前业务代码已经渲染了,会拿不到实验数据?
会。让业务代码先渲染对照组,再在 onReady / waitReady 中切换到变体。粘性缓存保证用户刷新后稳定看到自己的变体(首次访问会有"对照组 → 变体"的瞬时切换)。
track 调用了,后端没收到怎么排查?
track 调用了,后端没收到怎么排查?按顺序检查:
abSubotiz.isReady()是否为true?未就绪时track静默跳过。- 打开
debug: true,看控制台是否有track: sending日志。 - 看浏览器网络面板有没有
POST /sdk/v1/ab/events请求。 - 响应是 4xx 还是 5xx?4xx 会被丢弃,5xx 会入队等待重发。
- 看
localStorage.ab_event_queue_*_track是否积压事件。
我传了 sourceChannel,但实验定向没生效
sourceChannel,但实验定向没生效按顺序检查:
- 打开
debug: true,看分桶请求体attributes.source_channel是否带上了你的值。 - 确认值合法:trim 后非空、长度 < 50。超长会被忽略(控制台有
warn)。 - 确认值与后台定向规则完全一致——SDK 不做大小写转换,
Facebook≠facebook。 - 定向规则匹配在后端完成;命中与否可用
getExperiments()看是否拿到该实验。
track 的 value 没有进入求和 / 均值统计
track 的 value 没有进入求和 / 均值统计value 必须是有限数字(typeof === 'number' 且非 NaN / Infinity)才会被纳入数值聚合。常见坑是传了字符串:track('ltv', { value: '99.9' })(字符串)会被当成普通标签,不参与 sum / average。改成 { value: 99.9 }。
同一个实验,刷新前后变体不一致
SDK 粘性缓存保证 60 天内变体稳定。出现不一致通常是:
localStorage被清除(隐私模式 / 用户主动清空)- 后台开启了用户强制分配,会覆盖粘性缓存
实验在生产没生效,怎么排查
按顺序检查:
storeId是否正确(错误的 storeId 不报错,只是拿不到任何实验)- 后台实验是否处于"运行中"状态(暂停 / 草稿状态不会下发)
- 用户是否命中实验定向规则(设备 / 国家由后端从 HTTP 请求头解析)
- 用
getExperiments()看返回结果,确认是不是变体决策本身就是对照组
我想让某个用户完全不参与实验
SDK 不提供"退出实验"接口。变通方案:业务代码自行判断(如登录态 / 角色),命中条件时不读 experiments 直接走默认渲染。
getExperiments() 为什么返回 null 而不是空对象
getExperiments() 为什么返回 null 而不是空对象null 表示"未就绪"或"无任何命中实验",便于做存在性判断:
const all = ab.getExperiments();
if (!all) return renderDefault();避免写 Object.keys(all).length === 0。
React 服务端组件 / Edge Runtime 能用吗
不能。SDK 依赖 localStorage / document.cookie / window 等浏览器接口。
浏览器兼容性
依赖原生 fetch / Promise / localStorage。支持:
- Chrome / Edge ≥ 最近 2 个大版本
- Safari ≥ 最近 2 个大版本
- Firefox ≥ 最近 2 个大版本
不支持 IE。SDK 不内置任何兼容垫片。