XUNL EXPERIMENTS / JS SDK · WORDPRESS · SERVER API

A/B 测试接入说明

任何网站都能做 A/B 测试,询盘转化也算得准。

公开说明 · 免登录阅读
想在自己的网站上直接跑 A/B 测试?

XunL A/B 实验:可视化改页面、按访客稳定分组、成功询盘由服务器确认,报告给出置信区间和是否可以下结论。

进入 A/B 实验后台 →

选择接入方式

两种方式用同一个站点连接、同一套实验和报告,区别只在安装和询盘确认。

JS SDKWordPress 插件
适用网站任何网站:Shopify、Webflow、Wix、Squarespace、跟踪代码管理器、自建站WordPress 6.2 及以上
安装把一行脚本放进公共 head上传插件 ZIP,填入连接信息
页面、点击、自定义事件目标支持支持
成功询盘目标你的服务器签名确认(附 Node.js、Python、PHP 示例)自动确认:Contact Form 7、Elementor Pro、WPForms、Gravity Forms

任何网站:安装 JS SDK

  1. 登录迅龙后台,在「站点管理」添加网站,再到「A/B 实验 → 站点与安装」创建连接。
  2. 复制后台给出的脚本,放进所有页面共用的 head 里,越靠前越好。下面的示例中 SITE_PUBLIC_KEY 要换成你自己站点的公钥。
  3. 接入访客同意(见下文),然后在后台点「检查接入」。
HTML
<script defer src="https://www.xunl.net/js/xunl-experiments-sdk.js" data-xunl-key="SITE_PUBLIC_KEY" data-xunl-origin="https://www.xunl.net" data-xunl-consent="pending"></script>

脚本只读取公开的实验配置,连接密钥永远不要放进网页。单页应用用 history.pushState 切换路由后,调用一次 XunLExperiments.refresh()。

各建站平台放在哪里

Shopify
在线商店 → 模板 → 编辑代码 → layout/theme.liquid,粘贴到 head 结束标签之前。结账页无法加脚本(Shopify Plus 除外),实验目标请放在商品页和购物车之前的页面。
Webflow
Site settings → Custom code → Head code,粘贴后发布网站。需要付费的网站套餐。
Wix
设置 → 高级 → 自定义代码 → 添加自定义代码,选择“所有页面”和“Head”。需要付费套餐并绑定域名。
Squarespace
设置 → 高级 → 代码注入 → 页眉。需要商务版或更高套餐。
Google 跟踪代码管理器
新建「自定义 HTML」代码,粘贴脚本,触发条件选“所有网页”或“初始化 - 所有网页”。通过跟踪代码管理器加载会比直接写在 head 里晚一些,可视化改动更容易闪一下。
自建站、React、Next.js
放进 HTML 模板的 head,例如 Next.js 的根布局或 _document,Vue 和 React 项目的 index.html。

默认 data-xunl-consent="pending":同意之前 SDK 不读写浏览器存储、不生成访客身份、不发送事件。在你的 Cookie 同意弹窗回调里调用下面任一种写法。

JavaScript
// After the visitor accepts analytics cookies in your consent banner
XunLExperiments.consent(true);

// If they decline or withdraw consent
XunLExperiments.consent(false);

// Or, without touching the SDK directly (also works before it has started)
window.dispatchEvent(new CustomEvent('xunl:consent', { detail: { granted: true } }));

如果网站在你的地区不需要征求同意,可以把脚本里的 data-xunl-consent 改成 granted。是否需要征求同意请按网站所在地和访客所在地的法规判断。

设置实验目标

每个实验设一个主要目标,开测前定好,报告的结论只看主要目标;其他目标作为参考。

目标什么时候算转化示例
页面访客到达某个网址,例如感谢页https://www.ssevening.com/thank-you/
点击点击某个元素,用 CSS 选择器指定#quote-button
自定义事件网页代码里调用 track 上报XunLExperiments.track('quote_started')
成功询盘服务器确认询盘已保存或发出,见下一节—

统计成功询盘:服务端确认

非 WordPress 网站按下面三步统计成功询盘。只在询盘真正保存或发出后发送,按钮点击、校验失败和垃圾提交都不要发。

第一步:表单带上实验上下文

给脚本加上 data-xunl-forms="all"(也可以填 CSS 选择器,只处理询盘表单)。提交时表单会多一个隐藏字段 xunl_context。

HTML
<script defer src="https://www.xunl.net/js/xunl-experiments-sdk.js" data-xunl-key="SITE_PUBLIC_KEY" data-xunl-origin="https://www.xunl.net" data-xunl-consent="pending" data-xunl-forms="all"></script>

用 fetch 或 AJAX 提交的表单,自己把上下文放进请求:

JavaScript
const body = new FormData(form);
const ctx = window.XunLExperiments && XunLExperiments.inquiryContext();
if (ctx) body.set('xunl_context', JSON.stringify(ctx));
await fetch('/api/inquiry', { method: 'POST', body });

第二步:服务器保存询盘后签名发送

连接密钥在后台创建连接时只显示一次,保存到服务器环境变量 XUNL_CONNECTOR_SECRET。submissionId 用你自己的询盘编号,同一个询盘重发不会重复计数。

Node.js
// Node 18+. Call after your server has saved or sent the inquiry.
const crypto = require('node:crypto');
const XUNL = 'https://www.xunl.net', KEY = 'SITE_PUBLIC_KEY', SECRET = process.env.XUNL_CONNECTOR_SECRET;

async function confirmInquiry(xunlContext, submissionId) {
  let ctx; try { ctx = JSON.parse(xunlContext || 'null'); } catch { return false; }
  if (!ctx || ctx.preview || !ctx.visitorId || !(ctx.assignments || []).length) return false;
  const body = JSON.stringify({ eventId: crypto.randomUUID(), submissionId, visitorId: ctx.visitorId,
    acceptedAt: new Date().toISOString(), assignments: ctx.assignments, status: 'accepted' });
  const path = '/x/ab/v1/sites/' + KEY + '/conversions';
  for (let attempt = 0; attempt < 3; attempt++) { // retries keep the body, sign again
    const ts = String(Math.floor(Date.now() / 1000)), nonce = crypto.randomUUID();
    const digest = crypto.createHash('sha256').update(body).digest('hex');
    const sig = crypto.createHmac('sha256', SECRET).update(`v1\nPOST\n${path}\n${ts}\n${nonce}\n${digest}`).digest('hex');
    const res = await fetch(XUNL + path, { method: 'POST', body, redirect: 'error', headers: { 'Content-Type': 'application/json',
      'X-XunL-Timestamp': ts, 'X-XunL-Nonce': nonce, 'X-XunL-Signature': 'v1=' + sig } }).catch(() => null);
    if (res && res.ok) return true;
    if (res && res.status !== 409 && res.status < 500) return false;
    await new Promise(r => setTimeout(r, 2000 * (attempt + 1)));
  }
  return false;
}
module.exports = { confirmInquiry };
Python
# Python 3.8+. Call after your server has saved or sent the inquiry.
import hashlib, hmac, json, os, time, uuid, urllib.error, urllib.request
from datetime import datetime, timezone
XUNL, KEY, SECRET = 'https://www.xunl.net', 'SITE_PUBLIC_KEY', os.environ['XUNL_CONNECTOR_SECRET']

def confirm_inquiry(xunl_context, submission_id):
    try:
        ctx = json.loads(xunl_context or 'null')
    except ValueError:
        return False
    if not ctx or ctx.get('preview') or not ctx.get('visitorId') or not ctx.get('assignments'):
        return False
    body = json.dumps({'eventId': str(uuid.uuid4()), 'submissionId': submission_id, 'visitorId': ctx['visitorId'],
        'acceptedAt': datetime.now(timezone.utc).isoformat(timespec='milliseconds').replace('+00:00', 'Z'),
        'assignments': ctx['assignments'], 'status': 'accepted'}).encode()
    path = f'/x/ab/v1/sites/{KEY}/conversions'
    for attempt in range(3):  # retries keep the body, sign again
        ts, nonce = str(int(time.time())), str(uuid.uuid4())
        canonical = f'v1\nPOST\n{path}\n{ts}\n{nonce}\n{hashlib.sha256(body).hexdigest()}'
        sig = hmac.new(SECRET.encode(), canonical.encode(), hashlib.sha256).hexdigest()
        req = urllib.request.Request(XUNL + path, data=body, method='POST', headers={'Content-Type': 'application/json',
            'X-XunL-Timestamp': ts, 'X-XunL-Nonce': nonce, 'X-XunL-Signature': 'v1=' + sig})
        try:
            with urllib.request.urlopen(req, timeout=10):
                return True
        except urllib.error.HTTPError as e:
            if e.code != 409 and e.code < 500:
                return False
        except OSError:
            pass
        time.sleep(2 * (attempt + 1))
    return False
PHP
<?php // PHP 7.4+ with cURL. Call after your server has saved or sent the inquiry.
function xunl_uuid(): string { $b = random_bytes(16); $b[6] = chr(ord($b[6]) & 0x0f | 0x40); $b[8] = chr(ord($b[8]) & 0x3f | 0x80);
    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4)); }

function xunl_confirm_inquiry(string $xunlContext, string $submissionId): bool {
    $xunl = 'https://www.xunl.net'; $key = 'SITE_PUBLIC_KEY'; $secret = getenv('XUNL_CONNECTOR_SECRET');
    $ctx = json_decode($xunlContext, true);
    if (!is_array($ctx) || !empty($ctx['preview']) || empty($ctx['visitorId']) || empty($ctx['assignments'])) return false;
    $body = json_encode(['eventId' => xunl_uuid(), 'submissionId' => $submissionId, 'visitorId' => $ctx['visitorId'],
        'acceptedAt' => (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s.v\Z'),
        'assignments' => $ctx['assignments'], 'status' => 'accepted'], JSON_UNESCAPED_SLASHES);
    $path = '/x/ab/v1/sites/' . $key . '/conversions';
    for ($attempt = 0; $attempt < 3; $attempt++) { // retries keep the body, sign again
        $ts = (string) time(); $nonce = xunl_uuid();
        $sig = hash_hmac('sha256', "v1\nPOST\n$path\n$ts\n$nonce\n" . hash('sha256', $body), $secret);
        $ch = curl_init($xunl . $path);
        curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10,
            CURLOPT_HTTPHEADER => ['Content-Type: application/json', "X-XunL-Timestamp: $ts", "X-XunL-Nonce: $nonce", "X-XunL-Signature: v1=$sig"]]);
        curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
        if ($code >= 200 && $code < 300) return true;
        if ($code > 0 && $code !== 409 && $code < 500) return false;
        sleep(2 * ($attempt + 1));
    }
    return false;
}

第三步:接口约定

地址
POST https://www.xunl.net/x/ab/v1/sites/{SITE_PUBLIC_KEY}/conversions
请求头
Content-Type: application/json · X-XunL-Timestamp · X-XunL-Nonce · X-XunL-Signature: v1=<hex>
签名原文
v1\nPOST\n/x/ab/v1/sites/{key}/conversions\n{timestamp}\n{nonce}\n{sha256(body)}
签名算法
HMAC-SHA256(connector secret, string to sign) → lowercase hex
请求体
{eventId, submissionId, visitorId, acceptedAt, assignments, status: "accepted"}
时间与 nonce
时间戳为 Unix 秒,误差不超过 300 秒;nonce 是小写 UUID,每次请求都换新的。
返回
200 已接收或重复;401 签名或时间不对;409 浏览器分组尚未到达,稍后用同一个请求体重新签名发送;其他 4xx 不要重试。

WordPress 插件

  1. 在后台「A/B 实验 → 站点与安装」下载插件 ZIP,在 WordPress → 插件 → 安装插件 → 上传插件,然后启用。
  2. 在 WordPress → 设置 → XunL Experiments 填入 XunL 地址、站点公钥和连接密钥。
  3. 按“表单类型:表单 ID”勾选要统计的询盘表单,例如 cf7:123。没选的表单不会采集。
  4. 接入访客同意,然后在后台检查接入。插件已经包含 SDK,不要再手动加一份脚本。

检查接入是否成功

  1. 在网站上同意 Cookie 后,打开浏览器开发者工具的控制台,运行下面的命令,consentStatus 应为 granted。
  2. 回到后台点「检查接入」,看接收事件和访客分配是否增加。
  3. 用无痕窗口多打开几次实验页面,确认原版和新版本都能正常显示和提交。
JavaScript
XunLExperiments.diagnostics()
// { version, consentStatus, visitorId, assignments, pendingEvents, lastError, ... }

常见问题

不是 WordPress 网站能用吗?

能。把 JS SDK 脚本放进网站公共 head 即可运行可视化实验和页面分流,统计页面、点击和自定义事件目标。成功询盘由你自己的服务器签名确认后发给 XunL,WordPress 插件只是把这一步做成了现成的。

为什么询盘转化要服务器确认?

浏览器能看到的只是“点了提交”,表单校验失败、垃圾提交和重复提交都会混进来。由服务器在询盘真正保存或发出后签名确认,报告里的询盘数才和销售收到的一致。

会不会让页面变慢或闪一下?

脚本异步加载,读取实验配置失败或超时会保留原页面。可视化改动在配置返回后才应用,所以脚本放得越靠前、改动越小,闪烁越少;整页改版建议用页面分流,直接跳到新页面。

访客没同意 Cookie 时会记录吗?

不会。默认等待同意:在调用 XunLExperiments.consent(true) 或触发 xunl:consent 事件之前,SDK 不读写浏览器存储、不生成访客身份、不发送任何实验事件。访客撤回同意时会清除本地记录。

单页应用(React、Vue)路由切换后怎么办?

浏览器前进后退会自动重新匹配。用 history.pushState 切换路由后,调用一次 XunLExperiments.refresh(),SDK 会按新网址重新判断哪些实验生效。

按需连接下一步

从一次网站检查,走向日常监控

注册公司账号,集中管理网站与通知。Pro 支持 5 个站点、5 分钟级可用性监控,深度诊断按套餐与权限开放。

新公司享 14 天 Pro 试用,无需绑卡;试用不含付费月度积分。

已有公司账号?打开对应工作台 →

A/B 测试接入说明实用指南与常见问题

什么是A/B 测试接入说明?

覆盖两种接入方式的对比、各建站平台放脚本的位置、Cookie 同意接入、四类实验目标、单页应用刷新,以及 Node.js、Python、PHP 三种服务端确认询盘的完整示例和签名接口约定。

适合谁用,什么时候用?

想在 Shopify、Webflow、Wix、自建站或 WordPress 上做 A/B 测试,并把成功询盘算进实验结果的网站负责人和开发者。

如何使用A/B 测试接入说明?

按目录选接入方式:任何网站把 JS SDK 放进 head,WordPress 装插件;接入访客同意,设好目标,非 WordPress 网站用服务端示例确认询盘。

如何理解A/B 测试接入说明的结果?

接入需要迅龙账号,A/B 实验是付费功能;示例代码要按你的表单和服务器调整,连接密钥只能放在服务器上。

内容在哪里处理,会提交到哪些服务?

这是公开说明页,阅读和复制示例代码都不需要登录,页面不会收集你输入的任何内容。