Уявіть: ви запустили 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-тестування може покращити ваші конверсії без компромісів щодо продуктивності.







