11 KiB
11 KiB
📊 Анализ Google Analytics и AB-тестов в witlab-funnel
🎯 Краткий ответ
Когда отправляется impression событие AB-теста?
✅ В момент когда пользователь попадает на экран с AB-тестом
- НЕ при загрузке всей воронки
- НЕ при инициализации Unleash
- Именно при рендеринге конкретного экрана в
FunnelRuntime
Где происходит отправка?
Файл: src/components/funnel/FunnelRuntime.tsx (строки 136-150)
useEffect(() => {
if (currentScreenFlags.length === 0) return;
currentScreenFlags.forEach((flag) => {
const variant = activeVariants[flag];
sendUnleashImpression(flag, variant); // ← ЗДЕСЬ
});
}, [currentScreenFlags, activeVariants]);
Формат события в GA
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
<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
// Сканирует ВСЕ экраны и собирает уникальные флаги
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
// Собираем флаги только для ТЕКУЩЕГО экрана
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
// ✅ ЗДЕСЬ ОТПРАВКА
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
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 предотвращает повторную отправку
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)
[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
// В консоли:
localStorage.setItem('google_analytics_debug', '1');
// Перезагрузить страницу
// Открыть GA → Admin → DebugView
4. Проверка gtag
// В консоли браузера:
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. Очистка для тестирования
import { clearUnleashImpressions } from "@/lib/funnel/unleash";
clearUnleashImpressions();
// Очистит все "unleash_impression_*" ключи
📚 Ключевые файлы
Google Analytics
src/components/analytics/GoogleAnalytics.tsx- загрузка gtagsrc/components/providers/PixelsProvider.tsx- провайдер аналитикиsrc/components/analytics/PageViewTracker.tsx- page_view события
AB тестирование (Unleash)
src/lib/funnel/unleash/sendImpression.ts← Отправка impressionsrc/components/funnel/FunnelRuntime.tsx← Момент отправкиsrc/components/funnel/FunnelUnleashWrapper.tsx- сбор флаговsrc/lib/funnel/unleash/UnleashProvider.tsx- инициализация Unleashsrc/lib/funnel/unleash/UnleashContext.tsx- контекст вариантов
Документация
docs/UNLEASH_ANALYTICS_FLOW.md- подробный flowdocs/UNLEASH_ANALYTICS_FIX.md- проблемы и решенияdocs/AB_TESTING_GUIDE.md- руководство по AB тестам
✅ Итоговая сводка
Момент отправки impression
Когда пользователь попадает на экран с AB-тестом:
FunnelRuntimeрендерится сcurrentScreencurrentScreenFlagsвычисляются для текущего экранаuseEffectсрабатывает и вызываетsendUnleashImpression- Проверяется
sessionStorage(не отправлялось ли) - Отправляется
window.gtag("event", "experiment_impression", {...}) - Помечается в
sessionStorageкак отправленное
Защита от дубликатов
- ✅ sessionStorage хранит отправленные события
- ✅ При возврате назад события НЕ отправляются повторно
- ✅ При перезагрузке F5 события НЕ отправляются повторно
- ✅ При закрытии вкладки история очищается (новая сессия)
Формат данных
{
event: "experiment_impression",
app_name: "witlab-funnel",
feature: "название-флага",
treatment: "вариант"
}