573 lines
32 KiB
Markdown
573 lines
32 KiB
Markdown
# 📊 Примеры работы AB-тестов и Google Analytics
|
||
|
||
## 🎬 Сценарий 1: Первый вход пользователя
|
||
|
||
### Воронка с 2 AB-тестами
|
||
|
||
```json
|
||
{
|
||
"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
|
||
|
||
```javascript
|
||
// После первого экрана:
|
||
{
|
||
"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 событие
|
||
|
||
```http
|
||
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 событие
|
||
|
||
```http
|
||
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. Очистка истории для повторного тестирования
|
||
|
||
```javascript
|
||
// В консоли браузера:
|
||
import { clearUnleashImpressions } from "@/lib/funnel/unleash";
|
||
|
||
// Очистит все impression ключи
|
||
clearUnleashImpressions();
|
||
|
||
// Или вручную:
|
||
Object.keys(sessionStorage)
|
||
.filter(key => key.startsWith("unleash_impression_"))
|
||
.forEach(key => sessionStorage.removeItem(key));
|
||
|
||
// После этого события отправятся заново
|
||
```
|
||
|
||
### 2. Проверка текущего состояния
|
||
|
||
```javascript
|
||
// Посмотреть все 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. Имитация нового пользователя
|
||
|
||
```javascript
|
||
// 1. Очистить sessionStorage
|
||
sessionStorage.clear();
|
||
|
||
// 2. Открыть воронку в новой вкладке Incognito
|
||
// ИЛИ
|
||
// 3. Перезапустить браузер
|
||
|
||
// События отправятся как для нового пользователя
|
||
```
|
||
|
||
---
|
||
|
||
## 📈 Практические примеры AB тестов
|
||
|
||
### Тест 1: Кнопка оплаты
|
||
|
||
```json
|
||
{
|
||
"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 события:**
|
||
```javascript
|
||
// Группа A (v1):
|
||
{ feature: "trial-payment-button", treatment: "v1" }
|
||
|
||
// Группа B (disabled): НЕТ события
|
||
// потому что sendUnleashImpression пропускает variant="disabled"
|
||
```
|
||
|
||
### Тест 2: Короткая vs длинная воронка
|
||
|
||
```json
|
||
{
|
||
"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 события для обеих групп:**
|
||
```javascript
|
||
// Все пользователи видят 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 показывает события в реальном времени
|