Skip to content

AB Designer

Demo

Demo experiment · cookie-free A/B at the edge, scored for significance on the week's exposures. Join the waitlist →

Edge A/B tests apply variants via HTML rewrite at the edge. Zero flash, no client JS.PRO
New experiment
Experiments
CTA button copyactive
/ · 50% to variant B · .hero-cta · trial_started
VariantExposuresConversionsRateLift
Control
1,830532.9%
bp < 0.05
1,831764.2%+43%
Recomputed on every load, so a result can appear and vanish. Decide a sample size before you read it.
Pricing headlineactive
/pricing · 50% to variant B · h1 · purchase
VariantExposuresConversionsRateLift
Control
893242.7%
b
892313.5%+29%
Hero layoutpaused
/ · 50% to variant B · .hero
VariantExposuresConversionsRateLift
Control
1,83000.0%
b
1,82900.0%
Set a goal event on this experiment to measure conversions.

Edge integration

Deploy this Cloudflare Worker on your zone to apply variants at the edge. Set VERO_INGEST_SECRET as a Worker secret: the variant HTML is not public, so this uses your ingest secret and not the site token.
export default {
  async fetch(req, env, ctx) {
    const res = await fetch(req);
    if (!(res.headers.get('content-type') || '').includes('text/html')) return res;

    const url = new URL(req.url);
    // YOUR_INGEST_SECRET, not the public site token: this response carries the
    // HTML of every running variant, and the token ships in your page source.
    // Generate it in Site settings > Connect revenue, and keep it in a Worker secret.
    const cfg = await fetch(
      'https://api.getvero.dev/api/experiments/config',
      { headers: { authorization: 'Bearer ' + env.VERO_INGEST_SECRET }, cf: { cacheTtl: 60 } },
    ).then((r) => r.json()).catch(() => ({ experiments: [] }));

    const exp = cfg.experiments.find((e) => e.path === url.pathname);
    if (!exp) return res;

    // cookieless sticky bucket from IP + UA
    const id = (req.headers.get('cf-connecting-ip') || '') + (req.headers.get('user-agent') || '');
    const bucket = [...id].reduce((a, c) => (a * 31 + c.charCodeAt(0)) % 100, 7);
    const upTo = (i) => exp.variants.slice(0, i + 1).reduce((a, v) => a + v.weight, 0);
    const variant = exp.variants.find((v, i) => upTo(i) > bucket) || exp.variants[0];

    // Apply the variant AND stamp a marker so the Vero tracker's experiments
    // module can record the exposure that matches what was actually served.
    const rw = variant.changes.reduce(
      (r, ch) => r.on(ch.selector, {
        element(el) { el.setInnerContent(ch.html, { html: true }); },
      }),
      new HTMLRewriter().on('head', {
        element(el) {
          // Escaped, though both keys are constrained today (slugs and a fixed
          // pair): this runs on the customer's own pages, so the day either one
          // becomes free text it must not become stored XSS there.
          const esc = (v) => String(v).replace(/[&<>"]/g, (ch) =>
            ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[ch]);
          el.append('<meta name="vero-exp" content="' + esc(exp.key) + ':' + esc(variant.key) + '">', { html: true });
        },
      }),
    );
    return rw.transform(res);
  },
};