w-funnel/docs/GA_AB_TEST_EXAMPLES.md
2025-10-23 21:16:09 +02:00

32 KiB
Raw Permalink Blame History

📊 Примеры работы 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 показывает события в реальном времени