32 KiB
32 KiB
📊 Примеры работы AB-тестов и Google Analytics
🎬 Сценарий 1: Первый вход пользователя
Воронка с 2 AB-тестами
{
"meta": {
"id": "soulmate",
"googleAnalyticsId": "G-XXXXXXXXXX"
},
"screens": [
{
"id": "payment",
"variants": [{
"conditions": [{
"conditionType": "unleash",
"unleashFlag": "trial-button-test",
"unleashVariants": ["v1"]
}]
}]
},
{
"id": "gender",
"navigation": {
"rules": [{
"conditions": [{
"conditionType": "unleash",
"unleashFlag": "onboarding-flow",
"unleashVariants": ["short"]
}]
}]
}
}
]
}
Временная последовательность
┌─────────────────────────────────────────────────────────┐
│ T+0ms: Пользователь открывает /soulmate/payment │
└─────────────────────────────────────────────────────────┘
│
├─ Server-side render
├─ layout.tsx загружается
│
┌─────────────────────────────────────────────────────────┐
│ T+50ms: UnleashProvider инициализируется │
│ Подключение к Unleash API │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+100ms: PixelsProvider монтируется │
│ GoogleAnalytics загружает gtag.js │
│ window.gtag становится доступен │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+200ms: FunnelUnleashWrapper сканирует воронку │
│ Находит флаги: │
│ • trial-button-test (из payment.variants) │
│ • onboarding-flow (из gender.navigation) │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+300ms: FlagVariantFetcher загружает варианты │
│ Unleash SDK возвращает: │
│ • trial-button-test → "v1" │
│ • onboarding-flow → "short" │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+350ms: activeVariants обновляется │
│ { │
│ "trial-button-test": "v1", │
│ "onboarding-flow": "short" │
│ } │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+400ms: FunnelRuntime монтируется │
│ currentScreen = payment экран │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+410ms: currentScreenFlags вычисляется │
│ Анализирует payment экран: │
│ • payment.variants → trial-button-test │
│ • payment.navigation → нет флагов │
│ Результат: ["trial-button-test"] │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+420ms: useEffect срабатывает │
│ currentScreenFlags = ["trial-button-test"] │
│ activeVariants["trial-button-test"] = "v1" │
│ │
│ sendUnleashImpression("trial-button-test", "v1")│
└─────────────────────────────────────────────────────────┘
│
├─ Проверка: вариант валидный ✅
├─ Проверка: браузер ✅
├─ Проверка sessionStorage: пусто ✅
├─ Проверка window.gtag: есть ✅
│
┌─────────────────────────────────────────────────────────┐
│ T+421ms: ОТПРАВКА В GOOGLE ANALYTICS │
│ │
│ window.gtag("event", "experiment_impression", { │
│ app_name: "witlab-funnel", │
│ feature: "trial-button-test", │
│ treatment: "v1" │
│ }); │
│ │
│ sessionStorage.setItem( │
│ "unleash_impression_trial-button-test_v1", │
│ "true" │
│ ); │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+422ms: Network request отправлен │
│ POST /g/collect?v=2&tid=G-XXX │
│ &en=experiment_impression │
│ &ep.feature=trial-button-test │
│ &ep.treatment=v1 │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ Console (development): │
│ [Unleash Impression] ✅ Sent successfully: │
│ { feature: "trial-button-test", variant: "v1" } │
└─────────────────────────────────────────────────────────┘
🎬 Сценарий 2: Переход на следующий экран
┌─────────────────────────────────────────────────────────┐
│ T+0ms: Пользователь нажимает "Continue" │
│ Router.push("/soulmate/gender") │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+50ms: URL изменился → /soulmate/gender │
│ FunnelRuntime ре-рендерится │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+100ms: currentScreen обновляется │
│ currentScreen = gender экран │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+110ms: currentScreenFlags пересчитывается │
│ Анализирует gender экран: │
│ • gender.variants → нет флагов │
│ • gender.navigation → onboarding-flow │
│ Результат: ["onboarding-flow"] │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+120ms: useEffect срабатывает (deps изменились) │
│ currentScreenFlags = ["onboarding-flow"] │
│ activeVariants["onboarding-flow"] = "short" │
│ │
│ sendUnleashImpression("onboarding-flow", "short")│
└─────────────────────────────────────────────────────────┘
│
├─ Проверка sessionStorage:
│ "unleash_impression_onboarding-flow_short"
│ не найдено ✅
│
┌─────────────────────────────────────────────────────────┐
│ T+121ms: ОТПРАВКА ВТОРОГО СОБЫТИЯ │
│ │
│ window.gtag("event", "experiment_impression", { │
│ app_name: "witlab-funnel", │
│ feature: "onboarding-flow", │
│ treatment: "short" │
│ }); │
│ │
│ sessionStorage.setItem( │
│ "unleash_impression_onboarding-flow_short", │
│ "true" │
│ ); │
└─────────────────────────────────────────────────────────┘
Состояние sessionStorage
// После первого экрана:
{
"unleash_impression_trial-button-test_v1": "true"
}
// После второго экрана:
{
"unleash_impression_trial-button-test_v1": "true",
"unleash_impression_onboarding-flow_short": "true"
}
🎬 Сценарий 3: Возврат назад (защита от дубликатов)
┌─────────────────────────────────────────────────────────┐
│ T+0ms: Пользователь нажимает "Back" │
│ Router.push("/soulmate/payment") │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+50ms: URL изменился → /soulmate/payment │
│ FunnelRuntime ре-рендерится │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+100ms: currentScreen = payment (снова) │
│ currentScreenFlags = ["trial-button-test"] │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+110ms: useEffect срабатывает │
│ sendUnleashImpression("trial-button-test", "v1")│
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ Проверка sessionStorage: │
│ │
│ const key = "unleash_impression_trial-button-test_v1"; │
│ const alreadySent = sessionStorage.getItem(key); │
│ // "true" ← Уже отправлялось! │
│ │
│ if (alreadySent) { │
│ return; // ❌ ОТПРАВКА НЕ ПРОИСХОДИТ │
│ } │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ Console (development): │
│ [Unleash Impression] Skipped (already sent): │
│ { feature: "trial-button-test", variant: "v1" } │
└─────────────────────────────────────────────────────────┘
🎬 Сценарий 4: Перезагрузка страницы (F5)
┌─────────────────────────────────────────────────────────┐
│ Пользователь на экране /soulmate/gender │
│ sessionStorage содержит: │
│ { │
│ "unleash_impression_trial-button-test_v1": "true", │
│ "unleash_impression_onboarding-flow_short": "true" │
│ } │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+0ms: Пользователь нажимает F5 │
│ Браузер перезагружает страницу │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ ВАЖНО: sessionStorage НЕ очищается при перезагрузке! │
│ Данные сохраняются! │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ T+500ms: Страница загружена заново │
│ FunnelRuntime монтируется снова │
│ currentScreen = gender │
│ currentScreenFlags = ["onboarding-flow"] │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ useEffect срабатывает: │
│ sendUnleashImpression("onboarding-flow", "short") │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ Проверка sessionStorage: │
│ "unleash_impression_onboarding-flow_short" = "true" │
│ │
│ ❌ УЖЕ ОТПРАВЛЯЛОСЬ - ПРОПУСКАЕМ │
└─────────────────────────────────────────────────────────┘
Результат: Даже после F5 события НЕ отправляются повторно.
🎬 Сценарий 5: Новая вкладка / Закрытие браузера
┌─────────────────────────────────────────────────────────┐
│ ВКЛАДКА 1: Пользователь прошел воронку │
│ sessionStorage: │
│ { "unleash_impression_trial-button-test_v1": "true" } │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ Пользователь открывает НОВУЮ ВКЛАДКУ │
│ с тем же URL: /soulmate/payment │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ ВКЛАДКА 2: Новая сессия браузера! │
│ sessionStorage пустой: │
│ {} │
│ │
│ (sessionStorage изолирован для каждой вкладки) │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ FunnelRuntime загружается: │
│ sendUnleashImpression("trial-button-test", "v1") │
│ │
│ Проверка sessionStorage: ПУСТО │
│ ✅ СОБЫТИЕ ОТПРАВЛЯЕТСЯ ЗАНОВО │
└─────────────────────────────────────────────────────────┘
Важно: sessionStorage изолирован для каждой вкладки браузера.
📊 Что видно в Google Analytics
Events Report
Event name: experiment_impression
Total events: 2,345
By feature parameter:
┌────────────────────┬───────────┬────────┐
│ Feature │ Count │ Users │
├────────────────────┼───────────┼────────┤
│ trial-button-test │ 1,234 │ 987 │
│ onboarding-flow │ 1,111 │ 896 │
└────────────────────┴───────────┴────────┘
By treatment parameter:
┌────────────────────┬───────────┬────────┐
│ Treatment │ Count │ Users │
├────────────────────┼───────────┼────────┤
│ v1 │ 634 │ 507 │
│ v2 │ 600 │ 480 │
│ control │ 555 │ 445 │
│ short │ 556 │ 448 │
└────────────────────┴───────────┴────────┘
Cross-tabulation:
┌────────────────────┬───────────┬────────┐
│ Feature + Treatment│ Count │ Users │
├────────────────────┼───────────┼────────┤
│ trial-button-test │ │ │
│ ├─ v1 │ 634 │ 507 │
│ └─ v2 │ 600 │ 480 │
│ │ │ │
│ onboarding-flow │ │ │
│ ├─ short │ 556 │ 448 │
│ └─ control │ 555 │ 445 │
└────────────────────┴───────────┴────────┘
DebugView (Realtime)
User: 12345abc
Session ID: sess_xyz789
Events:
┌─────────┬────────────────────────┬────────────────────┐
│ Time │ Event │ Parameters │
├─────────┼────────────────────────┼────────────────────┤
│ 14:32:10│ page_view │ page: /payment │
│ 14:32:11│ experiment_impression │ feature: trial-.. │
│ │ │ treatment: v1 │
│ 14:32:45│ page_view │ page: /gender │
│ 14:32:46│ experiment_impression │ feature: onboard.. │
│ │ │ treatment: short │
└─────────┴────────────────────────┴────────────────────┘
🔍 Network Tab Примеры
Первое impression событие
POST /g/collect HTTP/1.1
Host: www.google-analytics.com
Query Parameters:
v=2
tid=G-XXXXXXXXXX
_p=1234567890
cid=abc-def-ghi-jkl
en=experiment_impression ← Event Name
epn.value=1
ep.app_name=witlab-funnel ← Custom Parameter
ep.feature=trial-button-test ← Custom Parameter
ep.treatment=v1 ← Custom Parameter
_s=1
Второе impression событие
POST /g/collect HTTP/1.1
Host: www.google-analytics.com
Query Parameters:
v=2
tid=G-XXXXXXXXXX
en=experiment_impression
ep.app_name=witlab-funnel
ep.feature=onboarding-flow ← Другой флаг
ep.treatment=short ← Другой вариант
_s=2
🧪 Тестирование
1. Очистка истории для повторного тестирования
// В консоли браузера:
import { clearUnleashImpressions } from "@/lib/funnel/unleash";
// Очистит все impression ключи
clearUnleashImpressions();
// Или вручную:
Object.keys(sessionStorage)
.filter(key => key.startsWith("unleash_impression_"))
.forEach(key => sessionStorage.removeItem(key));
// После этого события отправятся заново
2. Проверка текущего состояния
// Посмотреть все impression ключи:
Object.keys(sessionStorage)
.filter(key => key.startsWith("unleash_impression_"))
.forEach(key => {
console.log(key, sessionStorage.getItem(key));
});
// Вывод:
// unleash_impression_trial-button-test_v1 "true"
// unleash_impression_onboarding-flow_short "true"
3. Имитация нового пользователя
// 1. Очистить sessionStorage
sessionStorage.clear();
// 2. Открыть воронку в новой вкладке Incognito
// ИЛИ
// 3. Перезапустить браузер
// События отправятся как для нового пользователя
📈 Практические примеры AB тестов
Тест 1: Кнопка оплаты
{
"id": "payment",
"variants": [{
"conditions": [{
"conditionType": "unleash",
"unleashFlag": "trial-payment-button",
"unleashVariants": ["v1"]
}],
"override": {
"bottomActionButton": {
"text": "Start 7-Day Free Trial"
}
}
}]
}
Unleash возвращает:
- 50% пользователей:
v1(кнопка "Start 7-Day Free Trial") - 50% пользователей:
disabled(кнопка "Continue to Payment")
GA события:
// Группа A (v1):
{ feature: "trial-payment-button", treatment: "v1" }
// Группа B (disabled): НЕТ события
// потому что sendUnleashImpression пропускает variant="disabled"
Тест 2: Короткая vs длинная воронка
{
"id": "intro",
"navigation": {
"rules": [{
"conditions": [{
"conditionType": "unleash",
"unleashFlag": "funnel-length-test",
"unleashVariants": ["short"]
}],
"nextScreenId": "payment"
}],
"default": "details"
}
}
Unleash возвращает:
- 50%:
short→ переход сразу на payment (3 экрана) - 50%:
disabled→ переход на details (5 экранов)
GA события для обеих групп:
// Все пользователи видят intro экран:
{ feature: "funnel-length-test", treatment: "short" }
{ feature: "funnel-length-test", treatment: "disabled" }
// Но дальше идут разными путями!
✅ Контрольный список
Перед запуском AB теста:
- Google Analytics ID настроен в
funnel.meta.googleAnalyticsId - Unleash флаг создан и активен
- Unleash возвращает правильные варианты
- События отправляются (проверить Network tab)
- События видны в GA DebugView
- sessionStorage работает корректно
При отладке:
window.gtagопределен (в консоли)- Console логи показывают "Sent successfully"
- Network tab показывает POST /g/collect
- sessionStorage содержит impression ключи
- DebugView показывает события в реальном времени