Уявіть: ви запустили A/B-тест через Optimizely, але через тиждень помітили, що LCP зріс на 300 мс через сторонній скрипт. Або ви не можете отримати сирі дані — лише агреговані графіки. А вартість ліцензії для 10 000 відвідувачів — суттєві витрати на місяць. Кастомна платформа A/B-тестування вирішує всі ці проблеми: ви контролюєте код, дані та бюджет. Економія на ліцензійних відрахуваннях може сягати 70%, а окупність настає протягом 3–6 місяців. До того ж кастомна платформа працює в 2–3 рази швидше за рахунок відсутності зовнішніх скриптів.
Ми накопичили понад 5 років досвіду в розробці таких систем для інтернет-магазинів з трафіком 1 млн+ відвідувачів на місяць — понад 50 проєктів. Наша платформа будується за модульним принципом і легко адаптується під будь-яку архітектуру.
Переваги кастомної платформи A/B-тестування
Готові інструменти прискорюють старт, але при масштабуванні стають дорогими та негнучкими. Кастомна платформа окупається вже на кількох тестах — в одному з проєктів конверсія зросла на 15% після першого експерименту. При річному використанні економія на ліцензійних відрахуваннях може покрити вартість розробки протягом кількох місяців.
Як ми вирішуємо проблему детермінованого розподілу?
Ключова вимога A/B-тесту: користувач завжди повинен потрапляти в один і той самий варіант експерименту. Для цього ми використовуємо hash-based assignment. Хеш від user_id та назви експерименту за модулем 100% визначає номер варіанту. Результат зберігається в БД, що гарантує консистентність при багаторазових візитах.
Ось схема таблиць для зберігання експериментів та призначень:
CREATE TABLE experiments (
id SERIAL PRIMARY KEY,
slug VARCHAR(100) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
status VARCHAR(20) DEFAULT 'draft', -- draft, running, paused, completed
traffic SMALLINT DEFAULT 100, -- % трафіку, що бере участь в експерименті
start_at TIMESTAMPTZ,
end_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE experiment_variants (
id SERIAL PRIMARY KEY,
experiment_id INTEGER REFERENCES experiments(id),
slug VARCHAR(100) NOT NULL,
name VARCHAR(255),
weight SMALLINT DEFAULT 50,
config JSONB DEFAULT '{}',
UNIQUE(experiment_id, slug)
);
CREATE TABLE user_assignments (
user_id BIGINT NOT NULL,
experiment_id INTEGER REFERENCES experiments(id),
variant_id INTEGER REFERENCES experiment_variants(id),
assigned_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (user_id, experiment_id)
);
PHP-сервіс реалізує детермінований розподіл з використанням crc32 та збереженням у БД:
class ExperimentAssignmentService
{
public function getVariant(int $userId, string $experimentSlug): ?string
{
$experiment = $this->getActiveExperiment($experimentSlug);
if (!$experiment) return null;
$existing = $this->assignmentRepo->find($userId, $experiment['id']);
if ($existing) return $existing['variant_slug'];
$trafficBucket = $this->hashToBucket($userId, $experimentSlug . '_traffic');
if ($trafficBucket >= $experiment['traffic']) return null;
$variantBucket = $this->hashToBucket($userId, $experimentSlug);
$variant = $this->selectVariant($experiment['variants'], $variantBucket);
$this->assignmentRepo->assign($userId, $experiment['id'], $variant['id']);
$this->eventTracker->track($userId, 'experiment.assigned', [
'experiment' => $experimentSlug,
'variant' => $variant['slug'],
]);
return $variant['slug'];
}
private function hashToBucket(int $userId, string $salt): int
{
$hash = crc32($userId . '_' . $salt);
return abs($hash) % 100;
}
private function selectVariant(array $variants, int $bucket): array
{
$cumulative = 0;
foreach ($variants as $variant) {
$cumulative += $variant['weight'];
if ($bucket < $cumulative) return $variant;
}
return end($variants);
}
}
Трекінг подій у ClickHouse
Ми надсилаємо всі значущі дії користувача з контекстом експерименту. Щоб не сповільнювати користувацький досвід, події записуються асинхронно через чергу.
class ExperimentEventTracker
{
public function track(int $userId, string $event, array $properties = []): void
{
$activeVariants = $this->assignmentRepo->getUserVariants($userId);
$payload = [
'event' => $event,
'user_id' => $userId,
'session_id' => session_id(),
'occurred_at' => now()->toIso8601String(),
'experiments' => $activeVariants,
'properties' => $properties,
];
$this->queue->push(new TrackExperimentEvent($payload));
}
}
Дані зберігаються в ClickHouse — колонковій СУБД, оптимізованій для аналітичних запитів. Це дозволяє швидко рахувати конверсії та будувати звіти навіть при мільйонах подій.
Обчислення результатів: Z-тест
Після збору даних ми використовуємо двосторонній Z-тест для пропорцій (Wikipedia). Він показує, чи є різниця між конверсіями контрольної та тестової груп статистично значущою. Мінімальний виявлюваний ефект (MDE) налаштовується заздалегідь — наприклад, 5% при потужності 80%.
import numpy as np
from scipy import stats
def calculate_significance(control, treatment):
p1 = control['conversions'] / control['users']
p2 = treatment['conversions'] / treatment['users']
n1, n2 = control['users'], treatment['users']
p_pool = (control['conversions'] + treatment['conversions']) / (n1 + n2)
se = np.sqrt(p_pool * (1 - p_pool) * (1/n1 + 1/n2))
if se == 0: return {'error': 'Insufficient data'}
z = (p2 - p1) / se
p_value = 2 * (1 - stats.norm.cdf(abs(z)))
diff = p2 - p1
se_diff = np.sqrt(p1*(1-p1)/n1 + p2*(1-p2)/n2)
ci = [diff - 1.96*se_diff, diff + 1.96*se_diff]
return {'significant': p_value < 0.05, 'p_value': round(p_value, 6), 'lift': round((p2-p1)/p1*100,2) if p1>0 else None}
Щоб результати були достовірними, ми також перевіряємо Sample Ratio Mismatch (SRM) — чи не відхилився реальний розподіл користувачів від очікуваного. Якщо p-value хі-квадрат тесту нижче 0.01, дані позначаються як ненадійні.
| Параметр | Розрахунок |
|---|---|
| Конверсія | conversions / users |
| Lift | (p2-p1)/p1 * 100% |
| Довірчий інтервал | p ± 1.96 * SE |
| Етап | Тривалість | Результат |
|---|---|---|
| Аналітика | 1–2 дні | Цілі, метрики, архітектура |
| Проектування | 2–3 дні | Схема БД, API, контракти |
| Реалізація | 7–10 днів | Код, тести |
| Тестування | 2 дні | Юніт, інтеграційне, навантаження |
| Деплой | 1–2 дні | Розгортання, моніторинг |
Як працюють Feature Flags у A/B-тестах?
A/B-тестування та feature flags — суміжні концепції. Ми інтегруємо їх так: варіант експерименту містить JSON-конфігурацію, яка впливає на роботу фіч. Наприклад, варіант treatment_a включає {"checkout_steps": 1, "show_trust_badges": true}. Код на фронті або бекенді просто читає цей конфіг і змінює поведінку.
$variant = $experimentService->getVariant($userId, 'checkout-redesign');
$config = $experimentService->getVariantConfig('checkout-redesign', $variant);
$checkoutSteps = $config['checkout_steps'] ?? 3;
Що входить у роботу
- Розробка assignment service з hash-based розподілом та unit-тестами
- Трекінг подій з асинхронним записом у ClickHouse
- Обчислення статистичної значущості (Z-тест, довірчі інтервали)
- Адмін-панель для запуску та моніторингу експериментів
- Документація з інтеграції та навчання вашої команди
- Підтримка першого місяця пілотного запуску
Процес роботи
- Аналітика — розбираємо ваші цілі, метрики, поточну архітектуру (1–2 дні)
- Проектування — готуємо схему БД, API, контракти (2–3 дні)
- Реалізація — пишемо код, пишемо тести (7–10 днів)
- Тестування — юніт-тести, інтеграційне тестування, навантаження (2 дні)
- Деплой — розгортаємо у вашому середовищі, налаштовуємо моніторинг (1–2 дні)
З усіх питань звертайтеся до нас — ми допоможемо оцінити обсяг робіт і розрахувати економію.
Строки та бюджет
Орієнтовний час розробки — від 14 до 21 дня. Вартість розраховується індивідуально залежно від складності інтеграції та додаткових вимог. Замовте розробку кастомної платформи вже сьогодні — ми підготуємо пропозицію протягом 2 робочих днів.
Ми гарантуємо якість коду: використовуємо код-рев'ю, покриття тестами не менше 80%, і надаємо гарантію на усунення помилок протягом 30 днів після введення в експлуатацію. Отримайте консультацію та дізнайтеся, як кастомна платформа A/B-тестування може покращити ваші конверсії без компромісів щодо продуктивності.







