376 lines
11 KiB
Markdown
376 lines
11 KiB
Markdown
# 📊 Анализ 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: "вариант"
|
||
}
|
||
```
|