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.html → hero.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