Merchandising experiments (A/B)
Staged merchandising experiments run split tests on the server. Visitors never load third-party A/B snippets; the edge serves fully formed HTML for one variant per request.
When to use
- Test hero copy, product grids, or promotional sections on a published page.
- Compare two block subtrees without hurting Core Web Vitals or layout stability.
- Keep assignment and caching on the platform (sticky visitor, cacheable per variant).
Scope (v1)
- Pages only — visual CMS rows in the
pagescollection, not posts or block-builder presets. - At most two active experiments per page.
- No nested experiments — you cannot put an experiment slice inside an arm of another experiment.
Explicit non-goals
These boundaries apply to merchandising experiments (experiment_slice) as shipped today. They are intentional product limits — not missing features waiting on the next sprint.
v1 deferrals (merchandising may add later)
| Not in v1 | What we ship instead |
|---|---|
| Desk charts or winner dashboards | Deduped impression counts via API; no conversion UI |
| More than two active experiments per page | Up to four arms per slice (weights must sum to 100%) |
| Experiments on posts, product PDPs, or block-builder presets | Published pages in the canvas editor only |
Live in-editor AEO scoring (v1 deferral)
The canvas does not score arms in real time (no “Eonix Score”, traffic-light grades, or side-by-side ranking while you edit).
| Not in v1 | Why |
|---|---|
| Live AEO panel on the experiment inspector | Scoring pipelines and billing AEO lanes are separate from canvas save/publish; in-editor scores would imply certified results before publish |
| Auto-suggest copy changes from score deltas | Out of scope for experiment_slice; belongs in editor/AI products with their own review flow |
| Per-arm score badges on the structure tree | Would require synchronous scoring on every arm edit (latency + cost) |
What we do instead: after save, call GET /dxp/api/experiments/:id/aeo-export (staff session). The response includes every arm — headings, paragraphs, list row counts, plain text, token estimate — for offline comparison in your own tooling. Use Preview A / B for visual SSR checks; use export for structured copy review.
A future in-editor AEO experience would be a dedicated product slice (lane metering, score provenance, UI in inspector) — not a silent upgrade to the export JSON shape.
Block-library experiment templates (v1 deferral)
Block library and Block builder store reusable subtrees (presets, forms, layout patterns). They are not wired to CreateExperimentSliceOp or live assignment.
| Not in v1 | Why |
|---|---|
| “Save as experiment” from block builder | Experiments are bound to a published page route, manifest, and edge cache fingerprint — not library entry IDs |
| Platform-wide “50/50 hero” library SKU that auto-runs A/B | Would blur library (copy-on-insert) with runtime merchandising (sticky eonix_vid, impressions) |
Pasting a library entry that embeds experiment_slice | Normalization may reject nested experiments; arms must be edited on the page under test |
What we do instead: open the pages canvas, focus an eligible container (section, hero, layout_group, card, main), click A/B, edit arms, Save, then Publish. You may still paste library blocks inside an arm after the slice exists.
Client-side A/B libraries (rejected)
Eonix merchandising is SSR-only. The storefront must not load experiment runners in the browser.
| Rejected | Why |
|---|---|
| Google Optimize / GA4 experiment tags, VWO, Optimizely, AB Tasty, etc. | Third-party JS hurts CWV, privacy, and CSP; causes layout shift when variants swap in the DOM |
| “Hybrid” tests (server shell + client swap) | Breaks edge cache keys (exp:… variants) and duplicates assignment logic |
localStorage / sessionStorage arm picks in theme or GTM | Not sticky with eonix_vid; bypasses manifest + deduped impressions |
What we do instead: one arm is chosen on the server (ResolveExperiments), HTML is cached per variant on the edge, and the response contains no experiment_slice wrapper — only the winning subtree. Staff preview uses force_arm on SSR preview URLs, not client toggles.
Do not add experiment snippets to themes, header blocks, or site-wide custom HTML expecting the platform to coordinate with them.
Conversion analytics and auto-winner (rejected)
v1 records impressions (which arm HTML was served), not outcomes (orders, sign-ups, clicks, revenue).
| Rejected | Why |
|---|---|
| Purchase / revenue attribution per arm | No order→experiment join in the experiment pipeline; commerce events are a separate domain |
| Funnel goals, click maps, statistical significance | No desk analytics product on top of experiment_impressions |
Auto-promote winning arm to published content_blocks | Publishing stays a deliberate human action (Save → Publish); experiments never mutate live BSON without an operator |
What we do instead: GET …/impressions/count for smoke tests; AEO export for offline copy comparison. To pick a winner, an editor unwraps the slice (keeps control arm) or copies the winning arm manually, then publishes.
A future conversion product would need its own event schema, privacy review, and likely a different storage path — not an extension of impression dedup.
Multi-page experiments (rejected)
Each page route owns its own experiment manifest and edge cache keys. There is no site-wide or funnel-level experiment ID.
| Rejected | Why |
|---|---|
| One experiment spanning home + PLP + checkout | Manifests are per (site, locale, slug); exp:… variant keys are per rendered page |
| “Sticky arm B” across routes for the same visitor | Assignment is crc32(eonix_vid + experiment_id) per slice on that page — different pages may use different experiment_ids with no linkage |
| Cross-page winner or unified reporting | Impressions are recorded per experiment on the page that served HTML; no funnel rollup in v1 |
What we do instead: run independent slices on each page you care about (respecting the two active experiments per page cap). Accept that a visitor may see arm A on /home and arm B on /sale — by design.
A multi-page product would need shared experiment entities, cross-route manifests, and cache invalidation rules — a new schema, not a wider experiment_slice block.
Other future product boundaries (out of scope unless a new spec says otherwise)
Do not plan integrations or sales promises around these without an explicit merchandising product brief:
- Checkout, cart, and transactional surfaces — not part of the page canvas experiment model.
- Member-tier or signed-in personalization — use
<eonix-slot>/ member cache variants, notexperiment_slice. - Catalog merchandising — featured sort, per-store overrides, and assortment rules live in catalog/commerce data, not visual page A/B.
- Cross-site or cross-core experiment sync — experiments are site-scoped on one core DB.
- Adaptive / bandit allocation — weights are fixed until an operator changes them; no ML traffic rebalancing.
- Nested or chained experiments — one
experiment_sliceper eligible container; no experiment inside an arm. - Email, journeys, or off-site channels — experiments affect storefront HTML for a page route, not campaign sends.
If you need behaviour outside this list, treat it as a new product (separate spec and schema), not an extension of experiment slices.
Create an experiment
- Open the canvas editor for a page (Pages and editor).
- Focus an eligible container: section, hero, layout_group, card, or main.
- On the block toolbar, click A/B (creates a 50/50 experiment slice around that block).
- The page gains an experiment slice with Variant A and Variant B. Edit each arm’s nested blocks like any other content.
- Save the page, then Publish when ready — experiments go live with the published snapshot, same as other canvas changes.
Eligible block types get the A/B action automatically when the block is focused; experiment slices themselves do not offer A/B again.
Structure tree and inspector
- Structure (left aside) lists the experiment as Experiment · A 50% · B 50% (or · paused when paused), with Variant A / Variant B nested underneath. Experiment rows are not draggable — reorder the underlying blocks inside each arm on the canvas.
- Inspector (right aside): select the experiment slice (or click Settings on its toolbar) for status, traffic weights, SSR preview per arm, Edit arm focus, and Unwrap.
Experiment toolbar
When the experiment slice is focused, the chrome shows:
| Control | Effect |
|---|---|
| Settings | Opens the right-hand Merchandising experiment inspector. |
| Pause | All visitors see Variant A (control) until you Resume. |
| Resume | Restores weighted split (active status). |
| Unwrap | Removes the experiment and keeps Variant A only (destructive; confirms first). |
| Preview A / Preview B | Opens SSR preview for that arm (force_arm query; does not change live assignment). |
| Delete | Removes the entire experiment slice. |
Traffic weights
While status is active, set each variant’s percentage under Traffic, then Save weights. Weights must sum to 100% (the editor normalises on save). Default for a new slice is 50/50.
SSR preview (staff)
Use SSR preview on the canvas toolbar to see published HTML in a dialog. For a specific arm:
- Click Preview A or Preview B on the experiment chrome, or
- Append
force_arm=<experiment_id>:<arm_key>to the preview URL (comma-separated for multiple experiments), e.g.force_arm=abc123:bfor Variant B.
Preview does not change the visitor’s eonix_vid cookie or live assignment.
Pause and control
- Paused experiments always serve Variant A on the storefront.
- Active experiments assign arms by configured weights (see below).
AEO export (offline scoring)
GET /dxp/api/experiments/:experiment_id/aeo-export?page_id=… (or slug=…) returns structured JSON for every arm — headings, paragraphs, list row counts, plain text, and a rough token estimate. Use this to compare copy for answer-engine optimisation offline; there is no live “Eonix Score” in the editor yet.
Requires a signed-in canvas admin session.
Impressions
The edge records one impression per visitor per experiment per day (deduped server-side). For smoke tests:
GET /dxp/api/experiments/:experiment_id/impressions/count
There is no desk dashboard chart in v1.
How assignment works
- First visit sets a sticky
eonix_vidcookie (HttpOnly). - On publish, the platform writes a page experiment manifest the edge reads for active experiments and weights.
- Each active experiment picks an arm from
crc32(visitor_id + experiment_id)against those weights. - The edge cache key includes an
exp:…variant so HTML stays cacheable per arm combination. - Public HTML never contains the
experiment_slicewrapper — only the chosen arm’s blocks are rendered.
Desk APIs (integrators)
| Method | Path | Purpose |
|---|---|---|
| POST | /dxp/api/create-experiment-slice | Wrap a block in a new slice |
| PATCH | /dxp/api/update-experiment | Status, weights, labels |
| POST | /dxp/api/unwrap-experiment | Keep control arm only |
| GET | /dxp/api/experiments/:id/aeo-export | Structured copy per arm |
| GET | /dxp/api/experiments/:id/impressions/count | Deduped impression count |
Canvas saves and publishes also flow through the engine PageCanvas gRPC surface when ES_ENGINE_ADDR is configured.
Related
- Pages and canvas editor
- Block builder — presets only; live experiments are on pages
- Platform developer doc
eonixstream/docs/content-blocks-schema.md—experiment_sliceBSON shape