Experiment variants

A/B testing fragments without a client-side SDK. Declare an experiment on the include, bext assigns a variant per user, rewrites the source path, and caches each arm independently.

<bext-include src="/partials/hero"
  experiment="hero-redesign"
></bext-include>

The experiment attribute names a configured experiment. Based on the user's assignment (sticky cookie), bext resolves /partials/hero for the control arm, /partials/hero-v2 for variant A, /partials/hero-v3 for variant B — without any changes to the HTML tag.

Experiment configuration

Declare experiments in bext.config.toml:

[[experiments]]
name = "hero-redesign"
cookie = "bext_exp_hero"
cookie_ttl = "30d"
variants = [
  { name = "control",  weight = 50, src_suffix = "" },
  { name = "variant-a", weight = 25, src_suffix = "-v2" },
  { name = "variant-b", weight = 25, src_suffix = "-v3" },
]
Field Purpose
name Experiment identifier referenced by experiment="..."
cookie Sticky-assignment cookie name
cookie_ttl How long the assignment persists
variants Array of {name, weight, src_suffix}

Weights are relative — they don't need to sum to 100. The total is recomputed and each variant gets weight / total traffic.

Source rewriting

The src_suffix is appended to the include's declared src:

Declared src Variant Resolved src
/partials/hero control /partials/hero
/partials/hero variant-a /partials/hero-v2
/partials/hero variant-b /partials/hero-v3

You can organize variant content however you like:

- Separate files: _partials/hero-v2.html, _partials/hero-v3.html.

- Separate endpoints: /cms/hero?variant=v2, /cms/hero?variant=v3.

- Component props: combine experiment with render="v8" and props derived from variant name.

Assignment logic

Assignment priority (first match wins):

1. Query param override (for debugging): ?exp_override_hero-redesign=variant-a. 2. Existing cookie: read from the request's exp_hero cookie, use that variant. 3. Stable hash: if the user is authenticated, hash their user ID; if not, hash headers and request path. Use the hash % total_weight to pick a variant.

Assignment is deterministic:

- Same user always gets the same variant (across sessions, via cookie).

- Same anonymous fingerprint always gets the same variant (within a session).

- Changing the cookie clears the assignment — useful for QA.

The assignment cookie is written on the response, so the first request establishes the sticky assignment.

Per-variant caching

Experiments automatically inject themselves as a vary dimension:

<bext-include src="/partials/hero"
  experiment="hero-redesign"
  ttl="1h"
></bext-include>

Is equivalent to:

<bext-include src="/partials/hero"
  experiment="hero-redesign"
  ttl="1h"
  vary="experiment:hero-redesign"
></bext-include>

Each variant arm gets its own cache entry. Control, variant-a, and variant-b are cached independently — no cross-variant pollution.

Combining with auth and vary

Experiments stack cleanly with other attributes:

<bext-include
  src="/partials/pricing"
  experiment="pricing-test"
  auth="required"
  vary="role,locale"
  ttl="10m"
></bext-include>

The cache key includes the experiment variant, role, and locale. You get:

- pricing-control × admin × en (cached)

- pricing-variant-a × admin × en (cached)

- pricing-control × viewer × fr (cached)

- ...etc

Each combination cached separately; each user sees their consistent experience.

Manual override for debugging

Append a query param to force a variant:

https://example.com/page?exp_override_hero-redesign=variant-a

This bypasses the cookie and assignment logic — useful for QA, screenshots, internal testing. Override does not set a cookie, so it's ephemeral to the current request only.

Analytics

Every include resolution under an experiment emits an event:

{
  "event": "bext.include.experiment",
  "experiment": "hero-redesign",
  "variant": "variant-a",
  "src_original": "/partials/hero",
  "src_resolved": "/partials/hero-v2",
  "user_id": "user-42"
}

Feed this to your analytics pipeline (Segment, PostHog, custom) via a bext analytics plugin to measure conversion impact per variant.

Rolling out a winner

To promote a winning variant to 100%:

[[experiments]]
name = "hero-redesign"
cookie = "bext_exp_hero"
cookie_ttl = "30d"
variants = [
  { name = "winner", weight = 100, src_suffix = "-v2" },
]

All users are assigned winner and see /partials/hero-v2. Once you're confident, update the source files (rename hero-v2.htmlhero.html with empty suffix) and remove the experiment.

Variants for A/A/A testing

A/A testing verifies your experiment framework doesn't introduce bias. Declare identical variants with different names:

[[experiments]]
name = "aaa-sanity-check"
variants = [
  { name = "a1", weight = 33, src_suffix = "" },
  { name = "a2", weight = 33, src_suffix = "" },
  { name = "a3", weight = 34, src_suffix = "" },
]

All three resolve to the same content; the only difference is the recorded variant name in analytics. Use this to validate that conversion metrics across variants are statistically indistinguishable.

See also

- Caching includes — cache lifecycle interacts with variants

- Vary-aware caching — how experiment variants become vary dimensions

- Auth-gated includes — experiments + auth

- Feature flags — for non-variant boolean rollouts