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

11 KiB
Raw Blame History

📊 Анализ 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 - загрузка 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 события НЕ отправляются повторно
  • При закрытии вкладки история очищается (новая сессия)

Формат данных

{
  event: "experiment_impression",
  app_name: "witlab-funnel",
  feature: "название-флага",
  treatment: "вариант"
}