add event logs

This commit is contained in:
dev.daminik00 2025-10-23 21:16:09 +02:00
parent 3455412408
commit 5b76f3d03e
7 changed files with 2163 additions and 37 deletions

View File

@ -0,0 +1,362 @@
# 📊 Улучшенное логирование Google Analytics событий
## ✨ Что изменилось
Все события Google Analytics теперь логируются с **детальной информацией** в консоли браузера:
### 1. **Page View события** (просмотры страниц)
### 2. **AB Test Impression события** (показы AB-тестов)
Все логи используют **console.group** для структурированного отображения и поддерживают цветное форматирование.
---
## 📊 Page View события
### ✅ Успешная отправка
```
▼ [GA] 📊 Page View Event Sent
🕐 Timestamp: 2025-10-23T19:15:42.123Z
📍 URL: /soulmate/partner-age
🌐 Full Location: http://localhost:3000/soulmate/partner-age
📄 Page Title: Soulmate Portrait - Partner Age
📦 Payload: {
page_path: "/soulmate/partner-age",
page_location: "http://localhost:3000/soulmate/partner-age",
page_title: "Soulmate Portrait - Partner Age"
}
✅ Status: Successfully sent to Google Analytics
```
### ⚠️ Google Analytics не доступен
```
⚠️ [GA] ⚠️ Page View NOT Sent
Reason: Google Analytics not available (window.gtag is undefined)
```
---
## 🧪 AB Test Impression события
### ✅ Успешная отправка (первый раз)
```
▼ [GA] 🧪 AB Test Impression Event Sent
🕐 Timestamp: 2025-10-23T19:15:42.456Z
🏷️ Flag: soulmate-onboarding-image
🎯 Variant: v1
📦 Event Name: experiment_impression
📦 Payload: {
app_name: "witlab-funnel",
feature: "soulmate-onboarding-image",
treatment: "v1"
}
💾 Storage Key: unleash_impression_soulmate-onboarding-image_v1
✅ Status: Successfully sent to Google Analytics
🔍 Verify in Network tab: Look for /g/collect requests
```
### ⚠️ Пропущено (уже отправлялось)
```
▼ [GA] 🧪 AB Test Impression SKIPPED (Already Sent)
🕐 Timestamp: 2025-10-23T19:16:10.789Z
🏷️ Flag: soulmate-onboarding-image
🎯 Variant: v1
💾 Storage Key: unleash_impression_soulmate-onboarding-image_v1
⚠️ Reason: Already sent in this session
📊 Check sessionStorage to verify
```
### ⚠️ Пропущено (невалидный вариант)
```
▼ [GA] 🧪 AB Test Impression SKIPPED
🕐 Timestamp: 2025-10-23T19:15:40.123Z
🏷️ Flag: some-test-flag
🎯 Variant: disabled
⚠️ Reason: Invalid variant (disabled or undefined)
```
### ❌ Ошибка (GA не загружен)
```
▼ [GA] 🧪 AB Test Impression NOT Sent
🕐 Timestamp: 2025-10-23T19:15:42.456Z
🏷️ Flag: soulmate-trial-grid
🎯 Variant: grid
❌ Error: Google Analytics not available (window.gtag is undefined)
💡 Solution: Check that GoogleAnalytics component is loaded with measurementId
```
---
## 🧹 Очистка истории AB-тестов
```
▼ [GA] 🧪 AB Test Impressions Cleared
🕐 Timestamp: 2025-10-23T19:20:15.234Z
🧹 Keys Removed: 2
📋 Removed Keys: [
"unleash_impression_soulmate-onboarding-image_v1",
"unleash_impression_soulmate-trial-grid_grid"
]
✅ Status: All AB test impression history cleared
💡 New impressions will be sent on next screen view
```
---
## 📋 Yandex Metrika события
### ✅ Успешная отправка
```
▼ [YM] 📊 Page View Event Sent
🕐 Timestamp: 2025-10-23T19:15:42.123Z
📍 URL: /soulmate/partner-age
🔢 Counter ID: 104471567
✅ Status: Successfully sent to Yandex Metrika
```
### ⚠️ Counter ID не найден
```
⚠️ [YM] ⚠️ Page View NOT Sent
Reason: Counter ID not found
```
---
## 🎨 Цветовая схема
Логи используют цветовое кодирование для быстрой идентификации:
| Тип события | Цвет | Значок |
|-------------|------|--------|
| **GA Page View** | 🔵 Синий (`#4285F4`) | 📊 |
| **YM Page View** | 🔴 Красный (`#FF0000`) | 📊 |
| **AB Test Sent** | 🔵 Синий (`#4285F4`) | 🧪 |
| **AB Test Skipped** | ⚪ Серый (`#9E9E9E`) | 🧪 |
| **AB Test Cleared** | 🟠 Оранжевый (`#FF9800`) | 🧪 |
| **Warnings** | 🟡 Желтый (`#FFA500`) | ⚠️ |
| **Errors** | 🔴 Красный (`#FF0000`) | ❌ |
---
## 🔍 Как использовать
### 1. Открыть DevTools Console
```
Chrome/Edge: F12 → Console tab
Firefox: F12 → Console tab
Safari: Cmd+Option+C
```
### 2. Просмотреть события
Все события логируются **автоматически**:
- При каждом переходе между экранами → **Page View**
- При попадании на экран с AB-тестом → **AB Test Impression**
### 3. Развернуть группу
Клик на `▼` (или стрелку) чтобы увидеть детали:
```
▼ [GA] 📊 Page View Event Sent ← Клик здесь
🕐 Timestamp: ...
📍 URL: ...
...
```
### 4. Проверить Network Tab
После каждого лога проверьте Network tab:
```
DevTools → Network → Filter: "collect"
Должны появиться POST запросы:
POST /g/collect?v=2&tid=G-XXX&en=page_view...
POST /g/collect?v=2&tid=G-XXX&en=experiment_impression...
```
### 5. Проверить sessionStorage
```javascript
// В консоли браузера:
Object.keys(sessionStorage)
.filter(key => key.startsWith("unleash_impression_"))
.forEach(key => console.log(key, sessionStorage.getItem(key)));
// Вывод:
// unleash_impression_soulmate-onboarding-image_v1 "true"
// unleash_impression_soulmate-trial-grid_grid "true"
```
---
## 🧪 Тестирование AB-тестов
### Очистить историю и переотправить
```javascript
// В консоли браузера:
import { clearUnleashImpressions } from "@/lib/funnel/unleash";
// Очистить все impression события
clearUnleashImpressions();
// Перезагрузить страницу
window.location.reload();
// События отправятся заново!
```
### Вручную проверить ключи sessionStorage
```javascript
// Посмотреть все AB test ключи:
Object.keys(sessionStorage)
.filter(key => key.startsWith("unleash_impression_"));
// Удалить конкретный ключ:
sessionStorage.removeItem("unleash_impression_soulmate-onboarding-image_v1");
// Удалить все impression ключи:
Object.keys(sessionStorage)
.filter(key => key.startsWith("unleash_impression_"))
.forEach(key => sessionStorage.removeItem(key));
```
---
## 📊 Пример полного flow
### Сценарий: Пользователь проходит воронку Soulmate
```
1. Открытие /soulmate/onboarding
[GA] 📊 Page View Event Sent
🕐 2025-10-23T19:15:40.000Z
📍 /soulmate/onboarding
[GA] 🧪 AB Test Impression Event Sent
🏷️ soulmate-onboarding-image
🎯 v1
2. Переход на /soulmate/gender
[GA] 📊 Page View Event Sent
🕐 2025-10-23T19:16:05.000Z
📍 /soulmate/gender
3. Переход на /soulmate/partner-age
[GA] 📊 Page View Event Sent
🕐 2025-10-23T19:16:30.000Z
📍 /soulmate/partner-age
...18 экранов спустя...
4. Переход на /soulmate/email
[GA] 📊 Page View Event Sent
🕐 2025-10-23T19:20:15.000Z
📍 /soulmate/email
[GA] 🧪 AB Test Impression Event Sent
🏷️ soulmate-trial-grid
🎯 grid
5. Возврат назад на /soulmate/onboarding
[GA] 📊 Page View Event Sent
🕐 2025-10-23T19:21:00.000Z
📍 /soulmate/onboarding
[GA] 🧪 AB Test Impression SKIPPED (Already Sent)
🏷️ soulmate-onboarding-image
🎯 v1
⚠️ Already sent in this session
```
---
## 🎯 Преимущества новых логов
### ✅ Уверенность в отправке
Теперь вы **точно видите**:
- ✅ Событие отправлено в GA
- ⏰ Когда именно (timestamp)
- 📦 Какие данные (payload)
- 💾 Сохранено в sessionStorage
### ✅ Легкая отладка
При проблемах сразу видно:
- ❌ GA не загружен
- ⚠️ Событие уже отправлялось
- ⚠️ Невалидный вариант AB-теста
### ✅ Удобная навигация
- Логи сгруппированы (не захламляют консоль)
- Цветное кодирование для быстрой идентификации
- Эмодзи для визуальных маркеров
### ✅ Полная трассируемость
Каждый лог содержит:
- 🕐 Timestamp (ISO 8601)
- 📍 URL / Flag / Variant
- 📦 Полный payload
- ✅ Статус отправки
- 💡 Подсказки при ошибках
---
## 🔗 Связанные документы
- `GA_AB_TEST_ANALYSIS.md` - Техническая документация системы
- `SOULMATE_AB_TESTS_TIMELINE.md` - Детальный анализ AB-тестов Soulmate
- `SOULMATE_AB_QUICK_REFERENCE.md` - Быстрая справка по AB-тестам
---
## 📝 Примечания
### Production vs Development
Логи работают **в обоих режимах**:
- **Development**: Все логи показываются
- **Production**: Все логи показываются (для проверки в реальном времени)
Если нужно отключить логи в production, можно обернуть в условие:
```typescript
if (process.env.NODE_ENV === "development") {
console.groupCollapsed(...);
}
```
### Производительность
**console.groupCollapsed** не влияет на производительность:
- Группа свернута по умолчанию
- Содержимое вычисляется только при раскрытии
- Минимальный overhead
### Browser Support
Логи работают во всех современных браузерах:
- ✅ Chrome/Edge (полная поддержка)
- ✅ Firefox (полная поддержка)
- ✅ Safari (полная поддержка, без цветов в groupCollapsed)

375
docs/GA_AB_TEST_ANALYSIS.md Normal file
View File

@ -0,0 +1,375 @@
# 📊 Анализ Google Analytics и AB-тестов в witlab-funnel
## 🎯 Краткий ответ
### Когда отправляется impression событие AB-теста?
**✅ В момент когда пользователь попадает на экран с AB-тестом**
- НЕ при загрузке всей воронки
- НЕ при инициализации Unleash
- Именно при рендеринге конкретного экрана в `FunnelRuntime`
### Где происходит отправка?
**Файл:** `src/components/funnel/FunnelRuntime.tsx` (строки 136-150)
```typescript
useEffect(() => {
if (currentScreenFlags.length === 0) return;
currentScreenFlags.forEach((flag) => {
const variant = activeVariants[flag];
sendUnleashImpression(flag, variant); // ← ЗДЕСЬ
});
}, [currentScreenFlags, activeVariants]);
```
### Формат события в GA
```javascript
window.gtag("event", "experiment_impression", {
app_name: "witlab-funnel",
feature: "trial-button-test",
treatment: "v1"
});
```
---
## 🏗️ Архитектура
### Иерархия компонентов
```
app/[funnelId]/layout.tsx
├─ UnleashProvider (Unleash SDK)
│ └─ PixelsProvider
│ ├─ GoogleAnalytics ← загружает gtag.js
│ └─ PageViewTracker
└─ FunnelUnleashWrapper ← собирает все флаги воронки
├─ FlagVariantFetcher[] ← загружает варианты
└─ FunnelRuntime ← отправляет impression когда экран виден
```
---
## ⏱️ Timeline событий
```
T+0ms Пользователь открывает /funnel/payment
T+100ms GoogleAnalytics загружает gtag.js
T+200ms FunnelUnleashWrapper сканирует воронку
Находит флаги: ["trial-button-test", "payment-variant"]
T+300ms Unleash SDK возвращает варианты:
• trial-button-test → "v1"
• payment-variant → "v2"
T+400ms FunnelRuntime монтируется
currentScreen = "payment"
currentScreenFlags = ["trial-button-test"]
T+420ms useEffect срабатывает
✅ sendUnleashImpression("trial-button-test", "v1")
✅ window.gtag("event", "experiment_impression", {...})
T+421ms Событие отправлено в Google Analytics
```
**При переходе на следующий экран:**
```
T+0ms Клик "Continue" → переход на /funnel/gender
T+100ms currentScreen = "gender"
currentScreenFlags = ["payment-variant"]
T+120ms ✅ sendUnleashImpression("payment-variant", "v2")
✅ Второе событие отправлено
```
**При возврате назад:**
```
T+0ms Клик "Back" → возврат на /funnel/payment
T+100ms currentScreen = "payment"
currentScreenFlags = ["trial-button-test"]
T+120ms sendUnleashImpression проверяет sessionStorage
❌ Уже отправлялось - пропускаем
```
---
## 🔧 Ключевые компоненты
### 1. Инициализация Google Analytics
**Файл:** `src/components/analytics/GoogleAnalytics.tsx`
```typescript
<Script src={`https://www.googletagmanager.com/gtag/js?id=${measurementId}`} />
<Script dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${measurementId}', {
send_page_view: false // Отключаем автоматические page_view
});
`
}} />
```
### 2. Сбор флагов из воронки
**Файл:** `src/components/funnel/FunnelUnleashWrapper.tsx`
```typescript
// Сканирует ВСЕ экраны и собирает уникальные флаги
const allFlags = useMemo(() => {
const flags = new Set<string>();
funnel.screens.forEach((screen) => {
// Из вариантов экрана
screen.variants?.forEach((variant) => {
variant.conditions.forEach((condition) => {
if (condition.conditionType === "unleash") {
flags.add(condition.unleashFlag);
}
});
});
// Из правил навигации
screen.navigation?.rules?.forEach((rule) => {
rule.conditions.forEach((condition) => {
if (condition.conditionType === "unleash") {
flags.add(condition.unleashFlag);
}
});
});
});
return Array.from(flags);
}, [funnel.screens]);
```
### 3. Определение флагов текущего экрана
**Файл:** `src/components/funnel/FunnelRuntime.tsx`
```typescript
// Собираем флаги только для ТЕКУЩЕГО экрана
const currentScreenFlags = useMemo(() => {
const flags = new Set<string>();
currentScreen.variants?.forEach((variant) => {
variant.conditions.forEach((condition) => {
if (condition.conditionType === "unleash" && condition.unleashFlag) {
flags.add(condition.unleashFlag);
}
});
});
currentScreen.navigation?.rules?.forEach((rule) => {
rule.conditions.forEach((condition) => {
if (condition.conditionType === "unleash" && condition.unleashFlag) {
flags.add(condition.unleashFlag);
}
});
});
return Array.from(flags);
}, [currentScreen]);
```
### 4. Отправка impression
**Файл:** `src/components/funnel/FunnelRuntime.tsx`
```typescript
// ✅ ЗДЕСЬ ОТПРАВКА
useEffect(() => {
if (currentScreenFlags.length === 0) {
return; // Нет AB тестов
}
currentScreenFlags.forEach((flag) => {
const variant = activeVariants[flag];
sendUnleashImpression(flag, variant);
});
}, [currentScreenFlags, activeVariants]);
```
### 5. Функция sendUnleashImpression
**Файл:** `src/lib/funnel/unleash/sendImpression.ts`
```typescript
export function sendUnleashImpression(flag: string, variant: string | undefined) {
// 1. Валидация варианта
if (!variant || variant === "disabled") return;
// 2. Проверка браузера
if (typeof window === "undefined") return;
// 3. ✅ ЗАЩИТА ОТ ДУБЛИКАТОВ
const storageKey = `unleash_impression_${flag}_${variant}`;
if (sessionStorage.getItem(storageKey)) {
return; // Уже отправлялось
}
// 4. Проверка GA
if (!window.gtag) {
console.warn("Google Analytics not available");
return;
}
// 5. ✅ ОТПРАВКА
window.gtag("event", "experiment_impression", {
app_name: "witlab-funnel",
feature: flag,
treatment: variant,
});
// 6. Пометка
sessionStorage.setItem(storageKey, "true");
}
```
---
## 🛡️ Защита от дубликатов
### sessionStorage предотвращает повторную отправку
```typescript
const storageKey = `unleash_impression_${flag}_${variant}`;
// Пример: "unleash_impression_trial-button-test_v1"
if (sessionStorage.getItem(storageKey)) {
return; // Уже отправлялось в этой сессии браузера
}
// Отправка...
sessionStorage.setItem(storageKey, "true");
```
### Когда очищается?
| Действие | Очищается? | Отправится заново? |
|----------|------------|-------------------|
| Переход между экранами | ❌ | ❌ |
| F5 (перезагрузка) | ❌ | ❌ |
| Закрытие вкладки | ✅ | ✅ |
| Новая вкладка | ✅ | ✅ |
---
## 🐛 Отладка
### 1. Console logs (dev mode)
```javascript
[FunnelUnleashWrapper] Active variants: { trial-button-test: "v1" }
[Unleash Impression] ✅ Sent successfully: { feature: "trial-button-test", variant: "v1" }
[Unleash Impression] Skipped (already sent): { feature: "trial-button-test", variant: "v1" }
```
### 2. Network Tab
Фильтр: `collect` или `analytics.google.com`
```
POST /g/collect?v=2&tid=G-XXX&en=experiment_impression
&ep.app_name=witlab-funnel
&ep.feature=trial-button-test
&ep.treatment=v1
```
### 3. Google Analytics DebugView
```javascript
// В консоли:
localStorage.setItem('google_analytics_debug', '1');
// Перезагрузить страницу
// Открыть GA → Admin → DebugView
```
### 4. Проверка gtag
```javascript
// В консоли браузера:
console.log(typeof window.gtag);
// "function" - ✅ GA загружен
// "undefined" - ❌ GA не загружен
// Тестовое событие:
window.gtag("event", "test_event", { test_param: "test" });
```
### 5. sessionStorage
Chrome DevTools → Application → Session Storage
```
unleash_impression_trial-button-test_v1 "true"
unleash_impression_onboarding-flow_short "true"
```
### 6. Очистка для тестирования
```typescript
import { clearUnleashImpressions } from "@/lib/funnel/unleash";
clearUnleashImpressions();
// Очистит все "unleash_impression_*" ключи
```
---
## 📚 Ключевые файлы
### Google Analytics
- `src/components/analytics/GoogleAnalytics.tsx` - загрузка gtag
- `src/components/providers/PixelsProvider.tsx` - провайдер аналитики
- `src/components/analytics/PageViewTracker.tsx` - page_view события
### AB тестирование (Unleash)
- `src/lib/funnel/unleash/sendImpression.ts` ← **Отправка impression**
- `src/components/funnel/FunnelRuntime.tsx` ← **Момент отправки**
- `src/components/funnel/FunnelUnleashWrapper.tsx` - сбор флагов
- `src/lib/funnel/unleash/UnleashProvider.tsx` - инициализация Unleash
- `src/lib/funnel/unleash/UnleashContext.tsx` - контекст вариантов
### Документация
- `docs/UNLEASH_ANALYTICS_FLOW.md` - подробный flow
- `docs/UNLEASH_ANALYTICS_FIX.md` - проблемы и решения
- `docs/AB_TESTING_GUIDE.md` - руководство по AB тестам
---
## ✅ Итоговая сводка
### Момент отправки impression
**Когда пользователь попадает на экран с AB-тестом:**
1. `FunnelRuntime` рендерится с `currentScreen`
2. `currentScreenFlags` вычисляются для текущего экрана
3. `useEffect` срабатывает и вызывает `sendUnleashImpression`
4. Проверяется `sessionStorage` (не отправлялось ли)
5. Отправляется `window.gtag("event", "experiment_impression", {...})`
6. Помечается в `sessionStorage` как отправленное
### Защита от дубликатов
- ✅ sessionStorage хранит отправленные события
- ✅ При возврате назад события НЕ отправляются повторно
- ✅ При перезагрузке F5 события НЕ отправляются повторно
- ✅ При закрытии вкладки история очищается (новая сессия)
### Формат данных
```javascript
{
event: "experiment_impression",
app_name: "witlab-funnel",
feature: "название-флага",
treatment: "вариант"
}
```

572
docs/GA_AB_TEST_EXAMPLES.md Normal file
View File

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

View File

@ -0,0 +1,178 @@
# ⚡ Быстрая справка: AB-тесты Soulmate
## 🧪 Два AB-теста
### 1`soulmate-onboarding-image`
```
┌──────────────────────────────────────────┐
│ Экран: onboarding (1-й экран) │
│ URL: /soulmate/onboarding │
│ Когда: Сразу при входе в воронку │
│ Timing: ~420ms после загрузки │
│ │
│ Тестирует: │
│ • v0 = дефолтное изображение │
│ • v1 = альтернативное изображение (PNG) │
│ • v2 = видео (MP4) │
│ │
│ JSON: variants блок │
└──────────────────────────────────────────┘
▼ GA impression событие
window.gtag("event", "experiment_impression", {
feature: "soulmate-onboarding-image",
treatment: "v1"
});
```
### 2`soulmate-trial-grid`
```
┌──────────────────────────────────────────┐
│ Экран: email (19-й экран) │
│ URL: /soulmate/email │
│ Когда: При достижении email экрана │
│ Timing: ~120ms после перехода │
│ │
│ Тестирует: │
│ • coupon = переход на экран с купоном │
│ • grid = переход на выбор тарифов │
│ │
│ JSON: navigation.rules блок │
└──────────────────────────────────────────┘
▼ GA impression событие
window.gtag("event", "experiment_impression", {
feature: "soulmate-trial-grid",
treatment: "grid"
});
```
---
## 📍 Где в коде
```typescript
// src/components/funnel/FunnelRuntime.tsx (строки 103-150)
// Определяем флаги текущего экрана
const currentScreenFlags = useMemo(() => {
const flags = new Set<string>();
// Из variants
currentScreen.variants?.forEach((variant) => {
if (condition.unleashFlag) flags.add(condition.unleashFlag);
});
// Из navigation.rules
currentScreen.navigation?.rules?.forEach((rule) => {
if (condition.unleashFlag) flags.add(condition.unleashFlag);
});
return Array.from(flags);
}, [currentScreen]);
// Отправляем impression когда экран виден
useEffect(() => {
currentScreenFlags.forEach((flag) => {
const variant = activeVariants[flag];
sendUnleashImpression(flag, variant); // ← ОТПРАВКА
});
}, [currentScreenFlags, activeVariants]);
```
---
## ⏱️ Timeline полной воронки
```
┌─────────────────────────────────────────────────────────┐
│ T+0 Открытие /soulmate/onboarding │
├─────────────────────────────────────────────────────────┤
│ T+420ms ✅ IMPRESSION #1
│ feature: soulmate-onboarding-image │
│ treatment: v1 │
└─────────────────────────────────────────────────────────┘
│ Пользователь проходит 18 экранов
┌─────────────────────────────────────────────────────────┐
│ T+5min Достижение /soulmate/email │
├─────────────────────────────────────────────────────────┤
│ T+5m ✅ IMPRESSION #2
│ +120ms feature: soulmate-trial-grid │
│ treatment: grid │
└─────────────────────────────────────────────────────────┘
```
---
## 🔍 Быстрая проверка
### Console (Development mode)
```javascript
// При загрузке onboarding экрана:
[Unleash Impression] ✅ Sent successfully:
{ feature: "soulmate-onboarding-image", variant: "v1" }
// При достижении email экрана:
[Unleash Impression] ✅ Sent successfully:
{ feature: "soulmate-trial-grid", variant: "grid" }
```
### Network Tab
```
POST /g/collect?...
&en=experiment_impression
&ep.feature=soulmate-onboarding-image
&ep.treatment=v1
POST /g/collect?...
&en=experiment_impression
&ep.feature=soulmate-trial-grid
&ep.treatment=grid
```
### Session Storage
```javascript
sessionStorage = {
"unleash_impression_soulmate-onboarding-image_v1": "true",
"unleash_impression_soulmate-trial-grid_grid": "true"
}
```
---
## 📊 Сравнение
| | Test #1 | Test #2 |
|--|---------|---------|
| **Флаг** | `soulmate-onboarding-image` | `soulmate-trial-grid` |
| **Экран** | `onboarding` (1-й) | `email` (19-й) |
| **URL** | `/soulmate/onboarding` | `/soulmate/email` |
| **В JSON** | `variants` | `navigation.rules` |
| **Тестирует** | Медиа-контент | Навигацию |
| **Когда** | При входе в воронку | Почти в конце воронки |
---
## ✅ Главное
1. **Оба флага загружаются сразу** (preload) для быстрых переходов
2. **Impression отправляются последовательно** - только когда пользователь видит экран
3. **Защита от дубликатов** через sessionStorage работает автоматически
4. **Один и тот же код** отправляет события для обоих тестов
---
## 📚 Детальная документация
- `SOULMATE_AB_TESTS_TIMELINE.md` - полный анализ с диаграммами
- `GA_AB_TEST_ANALYSIS.md` - техническая документация
- `GA_AB_TEST_EXAMPLES.md` - примеры и сценарии

View File

@ -0,0 +1,567 @@
# 🧪 Конкретный анализ AB-тестов в воронке Soulmate
## 📋 Два AB-теста в воронке
1. **`soulmate-onboarding-image`** - тест картинки/видео на первом экране
2. **`soulmate-trial-grid`** - тест навигации после email экрана
---
## 🎯 AB-тест #1: `soulmate-onboarding-image`
### 📍 Где находится в JSON
**Файл:** `public/funnels/soulmate.json`
**Экран:** `"onboarding"` (первый экран воронки)
**Строки:** 51-106
```json
{
"id": "onboarding",
"template": "soulmate",
"variants": [
{
"conditions": [{
"conditionType": "unleash",
"unleashFlag": "soulmate-onboarding-image",
"unleashVariants": ["v0"]
}],
"overrides": {}
},
{
"conditions": [{
"conditionType": "unleash",
"unleashFlag": "soulmate-onboarding-image",
"unleashVariants": ["v1"]
}],
"overrides": {
"soulmatePortraitsDelivered": {
"mediaUrl": "/images/90b8c77f-c0cd-475d-a4de-bcabb3708c59.png",
"mediaType": "image"
}
}
},
{
"conditions": [{
"conditionType": "unleash",
"unleashFlag": "soulmate-onboarding-image",
"unleashVariants": ["v2"]
}],
"overrides": {
"soulmatePortraitsDelivered": {
"mediaUrl": "/images/275472b0-30e0-47d7-a1ab-8090bc9fb236.mp4",
"mediaType": "video"
}
}
}
]
}
```
### 🎬 Что тестируется
Три варианта картинки/видео на первом экране:
- **v0** - дефолтное изображение `/soulmate-portrait-delivered-male.jpg`
- **v1** - альтернативное изображение (PNG)
- **v2** - видео (MP4)
### ⏱️ КОГДА ОТПРАВЛЯЕТСЯ impression событие
```
┌─────────────────────────────────────────────────────────┐
│ T+0ms: Пользователь открывает /soulmate/onboarding │
│ Это ПЕРВЫЙ экран воронки │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ T+100ms: GoogleAnalytics загружается (gtag.js) │
│ window.gtag становится доступен │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ T+200ms: FunnelUnleashWrapper сканирует ВСЮ воронку │
│ Находит флаги: │
│ • soulmate-onboarding-image (из onboarding) │
│ • soulmate-trial-grid (из email) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ T+300ms: Unleash SDK возвращает варианты │
│ • soulmate-onboarding-image → "v1" (например) │
│ • soulmate-trial-grid → "grid" (например) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ T+400ms: FunnelRuntime монтируется │
│ currentScreen = onboarding экран │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ T+410ms: currentScreenFlags вычисляется │
│ Анализирует onboarding экран: │
│ • onboarding.variants → soulmate-onboarding-image│
│ • onboarding.navigation → нет флагов │
│ Результат: ["soulmate-onboarding-image"] │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ T+420ms: useEffect срабатывает │
│ (FunnelRuntime.tsx, строки 136-150) │
│ │
│ ✅ sendUnleashImpression( │
│ "soulmate-onboarding-image", │
│ "v1" │
│ ) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ T+421ms: ✅ ОТПРАВКА В GOOGLE ANALYTICS │
│ │
│ window.gtag("event", "experiment_impression", { │
│ app_name: "witlab-funnel", │
│ feature: "soulmate-onboarding-image", │
│ treatment: "v1" │
│ }); │
│ │
│ sessionStorage.setItem( │
│ "unleash_impression_soulmate-onboarding-image_v1", │
│ "true" │
│ ); │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Console (development): │
│ [Unleash Impression] ✅ Sent successfully: │
│ { │
│ feature: "soulmate-onboarding-image", │
│ variant: "v1" │
│ } │
└─────────────────────────────────────────────────────────┘
```
### 📊 Когда КОНКРЕТНО
**✅ Impression отправляется:** Сразу при входе пользователя в воронку на ПЕРВЫЙ экран "onboarding"
**⏰ Timing:** ~420ms после загрузки страницы `/soulmate/onboarding`
**🔧 Код:**
- **Файл:** `src/components/funnel/FunnelRuntime.tsx` (строки 136-150)
- **Функция:** `sendUnleashImpression()` из `src/lib/funnel/unleash/sendImpression.ts`
**📍 Условие отправки:**
```typescript
// В FunnelRuntime.tsx
const currentScreenFlags = useMemo(() => {
const flags = new Set<string>();
// Сканирует onboarding.variants
currentScreen.variants?.forEach((variant) => {
variant.conditions.forEach((condition) => {
if (condition.conditionType === "unleash" && condition.unleashFlag) {
flags.add(condition.unleashFlag); // ← "soulmate-onboarding-image"
}
});
});
return Array.from(flags); // ["soulmate-onboarding-image"]
}, [currentScreen]);
useEffect(() => {
if (currentScreenFlags.length === 0) return;
currentScreenFlags.forEach((flag) => {
const variant = activeVariants[flag]; // "v1"
sendUnleashImpression(flag, variant); // ← ОТПРАВКА
});
}, [currentScreenFlags, activeVariants]);
```
### 🔍 Как проверить в браузере
```javascript
// 1. Открыть /soulmate/onboarding
// 2. Открыть DevTools → Console
// 3. Увидеть:
[Unleash Impression] ✅ Sent successfully: {
feature: "soulmate-onboarding-image",
variant: "v1" // или "v0", "v2"
}
// 4. DevTools → Network → Фильтр "collect"
POST /g/collect?...&en=experiment_impression
&ep.feature=soulmate-onboarding-image
&ep.treatment=v1
// 5. DevTools → Application → Session Storage
unleash_impression_soulmate-onboarding-image_v1: "true"
```
---
## 🎯 AB-тест #2: `soulmate-trial-grid`
### 📍 Где находится в JSON
**Файл:** `public/funnels/soulmate.json`
**Экран:** `"email"` (экран ввода email)
**Строки:** 2277-2309
```json
{
"id": "email",
"template": "email",
"navigation": {
"rules": [
{
"conditions": [{
"conditionType": "unleash",
"unleashFlag": "soulmate-trial-grid",
"unleashVariants": ["coupon"]
}],
"nextScreenId": "coupon"
},
{
"conditions": [{
"conditionType": "unleash",
"unleashFlag": "soulmate-trial-grid",
"unleashVariants": ["grid"]
}],
"nextScreenId": "trial-choice"
}
],
"defaultNextScreenId": "coupon"
}
}
```
### 🎬 Что тестируется
Два варианта навигации после email экрана:
- **coupon** - переход на экран с купоном (`coupon`)
- **grid** - переход на экран с выбором тарифов (`trial-choice`)
### ⏱️ КОГДА ОТПРАВЛЯЕТСЯ impression событие
```
┌─────────────────────────────────────────────────────────┐
│ Пользователь прошел всю воронку: │
│ onboarding → gender → partner-gender → ... → email │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ T+0ms: Пользователь попадает на экран /soulmate/email │
│ Это 19-й экран воронки (почти в конце) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ ВАЖНО: Unleash уже загружен! │
│ activeVariants уже содержит: │
│ { │
│ "soulmate-onboarding-image": "v1", ← уже отправлено │
│ "soulmate-trial-grid": "grid" ← еще НЕ отправлено│
│ } │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ T+100ms: FunnelRuntime обновляется │
│ currentScreen = email экран │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ T+110ms: currentScreenFlags пересчитывается │
│ Анализирует email экран: │
│ • email.variants → нет unleash флагов │
│ • email.navigation.rules → soulmate-trial-grid │
│ Результат: ["soulmate-trial-grid"] │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ T+120ms: useEffect срабатывает │
│ (FunnelRuntime.tsx, строки 136-150) │
│ │
│ ✅ sendUnleashImpression( │
│ "soulmate-trial-grid", │
│ "grid" │
│ ) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ T+121ms: Проверка sessionStorage: │
│ "unleash_impression_soulmate-trial-grid_grid" │
НЕ найдено ✅ (первый раз на этом экране) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ T+122ms: ✅ ОТПРАВКА В GOOGLE ANALYTICS │
│ │
│ window.gtag("event", "experiment_impression", { │
│ app_name: "witlab-funnel", │
│ feature: "soulmate-trial-grid", │
│ treatment: "grid" │
│ }); │
│ │
│ sessionStorage.setItem( │
│ "unleash_impression_soulmate-trial-grid_grid", │
│ "true" │
│ ); │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Console (development): │
│ [Unleash Impression] ✅ Sent successfully: │
│ { │
│ feature: "soulmate-trial-grid", │
│ variant: "grid" │
│ } │
└─────────────────────────────────────────────────────────┘
```
### 📊 Когда КОНКРЕТНО
**✅ Impression отправляется:** Когда пользователь доходит до экрана "email" (19-й экран воронки)
**⏰ Timing:** ~120ms после перехода на `/soulmate/email`
**🔧 Код:**
- **Файл:** `src/components/funnel/FunnelRuntime.tsx` (строки 136-150)
- **Функция:** `sendUnleashImpression()` из `src/lib/funnel/unleash/sendImpression.ts`
**📍 Условие отправки:**
```typescript
// В FunnelRuntime.tsx
const currentScreenFlags = useMemo(() => {
const flags = new Set<string>();
// Сканирует email.variants (пусто)
// ...
// Сканирует email.navigation.rules
currentScreen.navigation?.rules?.forEach((rule) => {
rule.conditions.forEach((condition) => {
if (condition.conditionType === "unleash" && condition.unleashFlag) {
flags.add(condition.unleashFlag); // ← "soulmate-trial-grid"
}
});
});
return Array.from(flags); // ["soulmate-trial-grid"]
}, [currentScreen]);
useEffect(() => {
if (currentScreenFlags.length === 0) return;
currentScreenFlags.forEach((flag) => {
const variant = activeVariants[flag]; // "grid"
sendUnleashImpression(flag, variant); // ← ОТПРАВКА
});
}, [currentScreenFlags, activeVariants]);
```
### 🔍 Как проверить в браузере
```javascript
// 1. Пройти всю воронку до экрана email: /soulmate/email
// 2. Открыть DevTools → Console
// 3. Увидеть:
[Unleash Impression] ✅ Sent successfully: {
feature: "soulmate-trial-grid",
variant: "grid" // или "coupon"
}
// 4. DevTools → Network → Фильтр "collect"
POST /g/collect?...&en=experiment_impression
&ep.feature=soulmate-trial-grid
&ep.treatment=grid
// 5. DevTools → Application → Session Storage
// Должно быть 2 ключа:
unleash_impression_soulmate-onboarding-image_v1: "true"
unleash_impression_soulmate-trial-grid_grid: "true"
```
---
## 📊 Сравнительная таблица
| Аспект | soulmate-onboarding-image | soulmate-trial-grid |
|--------|---------------------------|---------------------|
| **Экран** | `onboarding` (1-й экран) | `email` (19-й экран) |
| **Позиция в JSON** | `variants` | `navigation.rules` |
| **Что тестирует** | Картинка/видео на первом экране | Навигация после email |
| **Варианты** | v0, v1, v2 | coupon, grid |
| **Когда отправляется** | Сразу при входе в воронку | При достижении email экрана |
| **Timing** | T+420ms после загрузки | T+120ms после перехода на email |
| **URL** | `/soulmate/onboarding` | `/soulmate/email` |
---
## 🎯 Финальная timeline для полной воронки
```
T+0ms Пользователь открывает /soulmate/onboarding
T+420ms ✅ IMPRESSION #1: soulmate-onboarding-image → v1
T+5min Пользователь проходит всю воронку
│ (gender, partner-gender, analysis-target, partner-age,
│ partner-ethnicity, partner-eye-color, partner-hair-length,
│ burnout-support, burnout-result, birthdate,
│ nature-archetype, love-priority, love-priority-result,
│ relationship-block, qualities, qualities-result,
│ progress, progress-result)
T+5min Пользователь доходит до /soulmate/email
T+5min ✅ IMPRESSION #2: soulmate-trial-grid → grid
+120ms │
T+5min Пользователь вводит email и нажимает Continue
+1min │
└─ Переход на trial-choice (если variant = "grid")
ИЛИ
Переход на coupon (если variant = "coupon")
```
---
## 📝 Ключевые моменты
### 1. Оба флага загружаются ЗАРАНЕЕ
```
FunnelUnleashWrapper сканирует ВСЮ воронку при загрузке
→ Находит оба флага сразу:
• soulmate-onboarding-image
• soulmate-trial-grid
→ Unleash SDK загружает варианты для ОБОИХ флагов
→ Варианты сохраняются в activeVariants
```
**Результат:** Быстрый переход между экранами (флаги уже загружены).
### 2. Impression отправляются ПОСЛЕДОВАТЕЛЬНО
```
Экран onboarding → impression для soulmate-onboarding-image
Экран email → impression для soulmate-trial-grid
```
**Результат:** Точная аналитика - события отправляются только когда пользователь РЕАЛЬНО видит экран.
### 3. Защита от дубликатов работает
```
sessionStorage хранит:
{
"unleash_impression_soulmate-onboarding-image_v1": "true",
"unleash_impression_soulmate-trial-grid_grid": "true"
}
```
**Результат:** Даже если пользователь вернется назад или перезагрузит страницу, события НЕ отправятся повторно.
---
## 🐛 Отладка
### Проверка отправки первого теста
```bash
# 1. Открыть /soulmate/onboarding
# 2. В консоли должно появиться:
[Unleash Impression] ✅ Sent successfully: {
feature: "soulmate-onboarding-image",
variant: "v1"
}
# 3. В Network tab:
POST /g/collect?...
&ep.feature=soulmate-onboarding-image
&ep.treatment=v1
```
### Проверка отправки второго теста
```bash
# 1. Пройти до /soulmate/email
# 2. В консоли должно появиться:
[Unleash Impression] ✅ Sent successfully: {
feature: "soulmate-trial-grid",
variant: "grid"
}
# 3. В Network tab:
POST /g/collect?...
&ep.feature=soulmate-trial-grid
&ep.treatment=grid
```
### Проверка что оба события отправлены
```javascript
// В консоли браузера:
Object.keys(sessionStorage)
.filter(key => key.startsWith("unleash_impression_"))
.forEach(key => console.log(key, sessionStorage.getItem(key)));
// Вывод (если пользователь дошел до email):
// unleash_impression_soulmate-onboarding-image_v1 "true"
// unleash_impression_soulmate-trial-grid_grid "true"
```
---
## ✅ Итоговый ответ
### Когда отправляется `soulmate-onboarding-image`?
**🎯 Сразу при входе в воронку на первый экран "onboarding"**
- URL: `/soulmate/onboarding`
- Timing: ~420ms после загрузки страницы
- Экран: 1-й из ~20 экранов воронки
### Когда отправляется `soulmate-trial-grid`?
**🎯 Когда пользователь доходит до экрана "email" (почти в конце воронки)**
- URL: `/soulmate/email`
- Timing: ~120ms после перехода на экран
- Экран: 19-й из ~20 экранов воронки
### Код отправки (для обоих)
**Файл:** `src/components/funnel/FunnelRuntime.tsx`
**Строки:** 136-150
```typescript
useEffect(() => {
if (currentScreenFlags.length === 0) return;
currentScreenFlags.forEach((flag) => {
const variant = activeVariants[flag];
sendUnleashImpression(flag, variant); // ← ЗДЕСЬ
});
}, [currentScreenFlags, activeVariants]);
```
**Функция:** `src/lib/funnel/unleash/sendImpression.ts`
```typescript
export function sendUnleashImpression(flag: string, variant: string | undefined) {
// Проверки...
window.gtag("event", "experiment_impression", {
app_name: "witlab-funnel",
feature: flag,
treatment: variant,
});
sessionStorage.setItem(`unleash_impression_${flag}_${variant}`, "true");
}
```

View File

@ -17,15 +17,36 @@ export function PageViewTracker() {
useEffect(() => {
const url = pathname + (searchParams?.toString() ? `?${searchParams.toString()}` : "");
const timestamp = new Date().toISOString();
// Track page view in Google Analytics
if (typeof window !== "undefined" && typeof window.gtag === "function") {
window.gtag("event", "page_view", {
const payload = {
page_path: url,
page_location: window.location.href,
page_title: document.title,
});
console.log(`[GA] Page view tracked: ${url}`);
};
window.gtag("event", "page_view", payload);
// Детальное логирование
console.groupCollapsed(
`%c[GA] 📊 Page View Event Sent`,
'color: #4285F4; font-weight: bold'
);
console.log('🕐 Timestamp:', timestamp);
console.log('📍 URL:', url);
console.log('🌐 Full Location:', window.location.href);
console.log('📄 Page Title:', document.title);
console.log('📦 Payload:', payload);
console.log('✅ Status: Successfully sent to Google Analytics');
console.groupEnd();
} else {
console.warn(
`%c[GA] ⚠️ Page View NOT Sent`,
'color: #FFA500; font-weight: bold',
'\nReason: Google Analytics not available (window.gtag is undefined)'
);
}
// Track page view in Yandex Metrika
@ -33,7 +54,23 @@ export function PageViewTracker() {
const counterId = window.__YM_COUNTER_ID__;
if (counterId) {
window.ym(counterId, "hit", url);
console.log(`[YM] Page view tracked: ${url}`);
// Детальное логирование
console.groupCollapsed(
`%c[YM] 📊 Page View Event Sent`,
'color: #FF0000; font-weight: bold'
);
console.log('🕐 Timestamp:', timestamp);
console.log('📍 URL:', url);
console.log('🔢 Counter ID:', counterId);
console.log('✅ Status: Successfully sent to Yandex Metrika');
console.groupEnd();
} else {
console.warn(
`%c[YM] ⚠️ Page View NOT Sent`,
'color: #FFA500; font-weight: bold',
'\nReason: Counter ID not found'
);
}
}
}, [pathname, searchParams]);

View File

@ -10,23 +10,25 @@
* @param variant - вариант который получил пользователь
*/
export function sendUnleashImpression(flag: string, variant: string | undefined) {
if (process.env.NODE_ENV === "development") {
console.log("[Unleash Impression] Called with:", { flag, variant });
}
const timestamp = new Date().toISOString();
// Проверяем что вариант валидный
if (!variant || variant === "disabled") {
if (process.env.NODE_ENV === "development") {
console.log("[Unleash Impression] Skipped: invalid variant");
}
console.groupCollapsed(
`%c[GA] 🧪 AB Test Impression SKIPPED`,
'color: #9E9E9E; font-weight: bold'
);
console.log('🕐 Timestamp:', timestamp);
console.log('🏷️ Flag:', flag);
console.log('🎯 Variant:', variant);
console.log('⚠️ Reason: Invalid variant (disabled or undefined)');
console.groupEnd();
return;
}
// Проверяем что браузерное окружение
if (typeof window === "undefined") {
if (process.env.NODE_ENV === "development") {
console.log("[Unleash Impression] Skipped: not browser environment");
}
console.warn('[GA] 🧪 AB Test Impression SKIPPED: Not browser environment');
return;
}
@ -36,39 +38,62 @@ export function sendUnleashImpression(flag: string, variant: string | undefined)
if (alreadySent) {
// Уже отправляли - пропускаем
if (process.env.NODE_ENV === "development") {
console.log("[Unleash Impression] Skipped (already sent):", {
feature: flag,
variant: variant,
});
}
console.groupCollapsed(
`%c[GA] 🧪 AB Test Impression SKIPPED (Already Sent)`,
'color: #9E9E9E; font-weight: bold'
);
console.log('🕐 Timestamp:', timestamp);
console.log('🏷️ Flag:', flag);
console.log('🎯 Variant:', variant);
console.log('💾 Storage Key:', storageKey);
console.log('⚠️ Reason: Already sent in this session');
console.log('📊 Check sessionStorage to verify');
console.groupEnd();
return;
}
// Проверяем наличие gtag
if (!window.gtag) {
console.warn("[Unleash Impression] ❌ Google Analytics not available (window.gtag is undefined)");
console.warn("[Unleash Impression] Check that GoogleAnalytics component is loaded with measurementId");
console.groupCollapsed(
`%c[GA] 🧪 AB Test Impression NOT Sent`,
'color: #FF0000; font-weight: bold'
);
console.log('🕐 Timestamp:', timestamp);
console.log('🏷️ Flag:', flag);
console.log('🎯 Variant:', variant);
console.log('❌ Error: Google Analytics not available (window.gtag is undefined)');
console.log('💡 Solution: Check that GoogleAnalytics component is loaded with measurementId');
console.groupEnd();
return;
}
// Отправляем событие в Google Analytics
window.gtag("event", "experiment_impression", {
// Подготавливаем payload
const payload = {
app_name: "witlab-funnel",
feature: flag,
treatment: variant,
});
};
// Отправляем событие в Google Analytics
window.gtag("event", "experiment_impression", payload);
// Помечаем что отправили
sessionStorage.setItem(storageKey, "true");
// Debug в development
if (process.env.NODE_ENV === "development") {
console.log("[Unleash Impression] ✅ Sent successfully:", {
feature: flag,
variant: variant,
});
}
// Детальное логирование успешной отправки
console.groupCollapsed(
`%c[GA] 🧪 AB Test Impression Event Sent`,
'color: #4285F4; font-weight: bold'
);
console.log('🕐 Timestamp:', timestamp);
console.log('🏷️ Flag:', flag);
console.log('🎯 Variant:', variant);
console.log('📦 Event Name:', 'experiment_impression');
console.log('📦 Payload:', payload);
console.log('💾 Storage Key:', storageKey);
console.log('✅ Status: Successfully sent to Google Analytics');
console.log('🔍 Verify in Network tab: Look for /g/collect requests');
console.groupEnd();
}
/**
@ -80,11 +105,21 @@ export function clearUnleashImpressions() {
return;
}
Object.keys(sessionStorage)
.filter(key => key.startsWith("unleash_impression_"))
.forEach(key => sessionStorage.removeItem(key));
const keysToRemove = Object.keys(sessionStorage)
.filter(key => key.startsWith("unleash_impression_"));
const timestamp = new Date().toISOString();
keysToRemove.forEach(key => sessionStorage.removeItem(key));
if (process.env.NODE_ENV === "development") {
console.log("[Unleash Impression] Cleared all impression history");
}
console.groupCollapsed(
`%c[GA] 🧪 AB Test Impressions Cleared`,
'color: #FF9800; font-weight: bold'
);
console.log('🕐 Timestamp:', timestamp);
console.log('🧹 Keys Removed:', keysToRemove.length);
console.log('📋 Removed Keys:', keysToRemove);
console.log('✅ Status: All AB test impression history cleared');
console.log('💡 New impressions will be sent on next screen view');
console.groupEnd();
}