Run page-level A/B (and multivariate) experiments in Silverstripe CMS 6. Assign visitors to weighted variations, measure conversions in the CMS, push events to the Google dataLayer, and forward the same canonical payload to any other analytics or CDP endpoint through a pluggable tracking API.
- PHP 8.3+
- Silverstripe CMS 6
symbiote/silverstripe-gridfieldextensions^5
composer require fullscreeninteractive/silverstripe-abtestingThen run a rebuild:
vendor/bin/sake db:build --flushThe module registers itself via Composer autoload and YAML config. No Page.php edits are required.
- CMS experiment manager under A/B Testing
- Weighted variations plus a control arm
- Traffic enrolment % so you can ramp an experiment gradually
- Cookie or session persistence for assignments
- QA override via query string (default
?abv=variant-b) - Content variation by alternate (usually hidden) page
- Template variation via alternate layout templates
- Conversion pages at experiment or per-variation level
- Custom conversions from JS, forms, or server code
- Google dataLayer exposure + conversion events
- Webhook / bridge API for Segment, GA4 Measurement Protocol, custom CDPs, etc.
- CMS reporting with summary table and Chart.js trends
- Raw data export of visitor exposures/conversions
- Page CMS tab listing experiments that touch a page
- Open A/B Testing → Experiments and create an experiment.
- Set Tested page (where visitors are split) and optional Conversion page.
- On Variations, add one or more variants:
- Alternate page for different content (same URL)
- Alternate template for different layout
- Tune weights / enrolment on Assignment.
- Confirm dataLayer / webhook settings on Tracking.
- Switch Status to
Active.
Visitors hitting the tested page are enrolled, assigned a variation, recorded as an exposure, and (by default) pushed into window.dataLayer.
| Goal | How |
|---|---|
| Different copy / images | Create a hidden child page and set it as the variation Alternate page |
| Different template | Set presentation to AlternateTemplate and enter template names, e.g. HomeVariant,Page |
| Both | Combine alternate page + alternate template on the same variation |
| QA a specific arm | Visit https://example.org/page?abv=variant-b |
The public URL always stays on the tested page — alternate page fields are copied into the request at runtime so navigation/menus keep working.
When Push events to Google dataLayer is enabled (default), exposures and conversions push:
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'ab_test_exposure', // or ab_test_conversion
ab_experiment_id: 12,
ab_experiment_key: 'homepage-hero-cta',
ab_experiment_name: 'Homepage hero CTA',
ab_variation: 'variant-b',
ab_variation_name: 'Variant B',
ab_is_control: false,
ab_event_id: 'a1b2c3d4e5f60718'
});Event names are configurable:
FullscreenInteractive\ABTesting\Bridge\DataLayerBridge:
event_exposure: ab_test_exposure
event_conversion: ab_test_conversionUse these events as Google Tag Manager triggers for GA4, Ads, or Floodlight tags.
The module exposes window.SSABTesting:
// Current assignment on an experimented page
console.log(SSABTesting.assignment);
// Custom conversion (form submit, CTA click, etc.)
SSABTesting.trackConversion('homepage-hero-cta', {
assertiveness: 100,
custom: { source: 'signup-form' }
});
// Listen locally
SSABTesting.onEvent(function (type, detail) {
console.log(type, detail);
});Or declare a conversion in markup:
<button type="button" data-ab-convert="homepage-hero-cta">Buy now</button>Every exposure/conversion is built into one canonical JSON payload and sent through tracking bridges.
{
"event_type": "exposure",
"event_id": "a1b2c3d4e5f60718",
"timestamp": "2026-10-12T20:15:30+00:00",
"experiment": {
"id": 12,
"key": "homepage-hero-cta",
"title": "Homepage hero CTA",
"status": "Active"
},
"variation": {
"key": "variant-b",
"title": "Variant B",
"is_control": false
},
"page": {
"id": 4,
"url": "https://example.org/home"
},
"visitor": {
"id": "9f8e7d6c5b4a3210"
}
}Inspect the schema live at /abtesting/eventschema.
- DataLayerBridge — browser
dataLayer.push - WebhookBridge — HTTP POST JSON to one or more HTTPS endpoints
Global webhooks:
FullscreenInteractive\ABTesting\Bridge\WebhookBridge:
endpoints:
- 'https://hooks.example.org/ab-events'
timeout: 5Per-experiment webhooks can also be set in the CMS Tracking tab.
| Method | Path | Purpose |
|---|---|---|
GET |
/abtesting |
Discovery |
GET |
/abtesting/assignment?experiment=homepage-hero-cta |
Current visitor assignment |
POST |
/abtesting/convert |
Record a conversion for the current visitor |
GET |
/abtesting/eventschema |
Payload schema |
POST /abtesting/convert body:
{
"experiment": "homepage-hero-cta",
"assertiveness": 100,
"custom": { "plan": "pro" }
}Implement TrackingBridgeInterface and register it:
namespace My\App;
use FullscreenInteractive\ABTesting\Bridge\TrackingBridgeInterface;
class SegmentBridge implements TrackingBridgeInterface
{
public function track(array $payload): void
{
// forward $payload to Segment, RudderStack, etc.
}
}FullscreenInteractive\ABTesting\Service\TrackingService:
bridges:
- FullscreenInteractive\ABTesting\Bridge\DataLayerBridge
- FullscreenInteractive\ABTesting\Bridge\WebhookBridge
- My\App\SegmentBridgeuse FullscreenInteractive\ABTesting\Model\ABTestExperiment;
use FullscreenInteractive\ABTesting\Service\ExperimentTracker;
use FullscreenInteractive\ABTesting\Service\TrackingService;
$experiment = ABTestExperiment::get()->find('Key', 'homepage-hero-cta');
$tracker = ExperimentTracker::create();
$data = $tracker->hitConversion($experiment, $request);
if ($data) {
TrackingService::create()->trackConversion(
$experiment,
$data->VariationKey,
['visitor_id' => $data->VisitorID]
);
}Templates can read the active assignment:
<% if $ABTestAssignment %>
<body data-ab-variation="$ABTestAssignment.variation_key">
<% end_if %>composer install
composer lint
composer testGitHub Actions runs PHPUnit on PHP 8.3/8.4 and PHPCS (PSR-12).
This package replaces the pre-Composer abtesting module:
- Classes moved to the
FullscreenInteractive\ABTestingnamespace - Table names (
ABTestExperiment,ABTestVariation,ABTestData) are preserved StateVariableValue/PHPSessionIDcolumns remain for upgrade safety; new code writesVariationKey/VisitorID- Dynamic Templates support was removed (module unmaintained); use alternate pages/templates instead
- Status values are now
Draft,Active,Paused,Complete
After upgrading, run db:build and re-activate experiments intentionally.
- Fullscreen Interactive — https://www.fullscreen.io
- Originally created by Mark Stephens (Silverstripe)
BSD-3-Clause. See LICENSE.


