Developing an A/B Testing Module for 1C-Bitrix
An A/B test is not just "show half of users a red button". It's correct audience segmentation, stable variant assignment (one user always sees one variant), tracking of goal events, statistical significance of results. Without a module, developers usually do this through if (rand(0,1)) — and get flickering variants, useless statistics, and inability to scale experiments. A proper module solves all these problems.
Data Model
Module vendor.abtest:
-
b_vendor_ab_experiment— experiments: id, name, code, status (draft/running/paused/finished), traffic_percent (10-100), started_at, finished_at, goal_event, description -
b_vendor_ab_variant— variants: id, experiment_id, name, weight (for uneven split), is_control -
b_vendor_ab_assignment— assignments: id, experiment_id, variant_id, user_id (or null), session_id, created_at -
b_vendor_ab_event— goal events: id, experiment_id, variant_id, assignment_id, event_type, created_at -
b_vendor_ab_stat— aggregated statistics (daily snapshots): experiment_id, variant_id, date, participants, conversions, revenue
Stable Variant Assignment
A user must always see the same variant. Assignment is performed once and saved:
class AssignmentService
{
public function getVariant(string $experimentCode): ?Variant
{
$experiment = ExperimentTable::getByCode($experimentCode);
if (!$experiment || $experiment['STATUS'] !== 'running') return null;
$sessionId = $this->getSessionId();
$userId = $GLOBALS['USER']->GetID() ?: null;
// Look for existing assignment
$existing = AssignmentTable::getList([
'filter' => [
'EXPERIMENT_ID' => $experiment['ID'],
'LOGIC' => 'OR',
'USER_ID' => $userId,
'SESSION_ID' => $sessionId,
],
])->fetch();
if ($existing) {
return VariantTable::getById($existing['VARIANT_ID'])->fetch();
}
// Check traffic percentage (deterministically by session_id)
$hash = crc32($sessionId . $experimentCode) % 100;
if ($hash >= $experiment['TRAFFIC_PERCENT']) return null; // user outside test
// Select variant by weights
$variant = $this->selectVariant($experiment['ID']);
AssignmentTable::add([
'EXPERIMENT_ID' => $experiment['ID'],
'VARIANT_ID' => $variant['ID'],
'USER_ID' => $userId,
'SESSION_ID' => $sessionId,
]);
return $variant;
}
}
Deterministic hash by session_id + experiment_code guarantees stable traffic percentage membership without storing extra data.
Usage in Templates
In component template or layout:
$abTest = \Vendor\AbTest\AbTestService::getInstance();
$variant = $abTest->getVariant('checkout_button_color');
$buttonClass = match ($variant?->getName()) {
'green' => 'btn-success',
'orange' => 'btn-warning',
default => 'btn-primary', // control variant or outside test
};
In JavaScript:
// Variant data passed through data-attribute or JS-variable
const variant = window.ABTEST_VARIANTS?.checkout_button_color;
if (variant === 'new_form') {
document.querySelector('.checkout-form').classList.add('new-form');
}
Tracking Goal Events
We track conversion — purchase, registration, CTA click:
// When order is paid
AddEventHandler('sale', 'OnSaleOrderPaid', function(\Bitrix\Main\Event $event) {
$orderId = $event->getParameter('id');
$order = \Bitrix\Sale\Order::load($orderId);
\Vendor\AbTest\EventTracker::track('purchase', [
'user_id' => $order->getUserId(),
'revenue' => $order->getPrice(),
'order_id' => $orderId,
]);
});
EventTracker::track() finds active assignments for a given user and records event in b_vendor_ab_event.
Statistical Significance
In the administrative interface for each experiment, the following is calculated:
- Conversion by variants (conversion rate = events / participants)
- p-value using chi-squared test (χ²) — significance threshold 95%
- Uplift — relative improvement versus control variant
- Sample ratio mismatch — check if variant ratio was not violated
// χ² test for two variants
function chiSquaredTest(int $a_visitors, int $a_conversions, int $b_visitors, int $b_conversions): float
{
$a_rate = $a_conversions / $a_visitors;
$b_rate = $b_conversions / $b_visitors;
$pooled = ($a_conversions + $b_conversions) / ($a_visitors + $b_visitors);
$expected_a = $pooled * $a_visitors;
$expected_b = $pooled * $b_visitors;
return (($a_conversions - $expected_a) ** 2 / $expected_a) +
(($b_conversions - $expected_b) ** 2 / $expected_b);
}
p-value < 0.05 → result is statistically significant.
Development Timeline
| Stage | Duration |
|---|---|
| ORM-tables, deterministic assignment | 2 days |
| Event tracking, sales integration | 2 days |
| Statistics, χ² test | 2 days |
| JavaScript SDK for frontend | 1 day |
| Administrative interface | 2 days |
| Testing | 1 day |
Total: 10 working days. Multivariate testing (3+ variants) and server-side feature-flag service — additional estimate required.







