w-funnel/docs/UNLEASH_LAZY_IMPRESSION_IMPLEMENTATION.md
2025-10-22 22:42:01 +02:00

13 KiB
Raw Permalink Blame History

Реализация Lazy Impression для Unleash AB тестов

🎯 Цель

Отправлять impression события в Google Analytics только когда пользователь реально видит экран с AB тестом, но при этом загружать все флаги заранее для быстрых переходов.


🏗️ Архитектура решения

┌─────────────────────────────────────────────────────────────┐
│  FunnelUnleashWrapper                                       │
│  ✅ Загружает ВСЕ флаги из воронки заранее                  │
│  ✅ Сохраняет активные варианты в UnleashContext            │
│  ❌ НЕ отправляет impression автоматически                  │
└─────────────────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────┐
│  UnleashContext                                             │
│  activeVariants: { "trial-test": "v1", "flow-test": "v2" } │
└─────────────────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────┐
│  FunnelRuntime                                              │
│  1. Собирает флаги для ТЕКУЩЕГО экрана                     │
│  2. Получает варианты из activeVariants                     │
│  3. Отправляет impression через sendUnleashImpression()     │
│  4. sessionStorage предотвращает дубликаты                  │
└─────────────────────────────────────────────────────────────┘

📝 Реализованные изменения

1. Убрана автоматическая отправка из useUnleash.ts

Было:

// useUnleash автоматически подписывался на события impression
useEffect(() => {
  unleashClient.on("impression", handleImpression);
  // Отправлял события сразу при загрузке флагов
}, [unleashClient]);

Стало:

// useUnleash теперь только получает вариант, не отправляет события
export function useUnleash({ flag }: UseUnleashProps) {
  const variant = useVariant(flag);
  return { variant: variant?.name };
}

2. Создан sendImpression.ts - ручная отправка

export function sendUnleashImpression(flag: string, variant: string | undefined) {
  // Проверки валидности
  if (!variant || variant === "disabled") return;
  if (typeof window === "undefined") return;

  // ✅ Защита от дубликатов через sessionStorage
  const storageKey = `unleash_impression_${flag}_${variant}`;
  if (sessionStorage.getItem(storageKey)) return;

  // Отправка в GA
  if (window.gtag) {
    window.gtag("event", "experiment_impression", {
      app_name: "witlab-funnel",
      feature: flag,
      treatment: variant,
    });
    
    sessionStorage.setItem(storageKey, "true");
  }
}

Особенности:

  • sessionStorage - предотвращает дубликаты при перезагрузке
  • Graceful degradation - не падает если GA не установлена
  • Debug логи в development режиме

3. Добавлена логика в 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]);

// Отправляем impression когда экран меняется
useEffect(() => {
  if (currentScreenFlags.length === 0) return;

  currentScreenFlags.forEach((flag) => {
    const variant = activeVariants[flag];
    sendUnleashImpression(flag, variant);
  });
}, [currentScreenFlags, activeVariants]);

Ключевые моменты:

  • Собираем флаги только для текущего экрана
  • Получаем варианты из activeVariants (уже загружены)
  • Отправляем impression при рендере экрана
  • sessionStorage не дает отправить дважды

Преимущества решения

Критерий Результат
Точность аналитики Impression = пользователь увидел экран
Нет дубликатов sessionStorage предотвращает
Скорость загрузки Флаги загружаются заранее (preload)
Плавность переходов Нет задержек - все уже готово
Обратная совместимость Существующая логика не затронута

📊 Примеры работы

Сценарий 1: Пользователь проходит воронку

T+0ms:    Открывает /soulmate/onboarding
          → FunnelUnleashWrapper загружает все флаги
          → activeVariants: { "trial-test": "v1", "flow-test": "short" }

T+500ms:  Экран onboarding рендерится
          → FunnelRuntime собирает флаги для onboarding: []
          → Impression не отправляется (нет AB тестов)

T+10s:    Переходит на /soulmate/payment
          → Экран payment рендерится
          → FunnelRuntime собирает флаги: ["trial-test"]
          → ✅ Отправка: experiment_impression { feature: "trial-test", treatment: "v1" }
          → sessionStorage: "unleash_impression_trial-test_v1" = "true"

T+20s:    Переходит на /soulmate/gender
          → Экран gender рендерится
          → FunnelRuntime собирает флаги: ["flow-test"]
          → ✅ Отправка: experiment_impression { feature: "flow-test", treatment: "short" }

Сценарий 2: Пользователь перезагружает страницу

T+0ms:    На экране payment
          → FunnelRuntime пытается отправить impression для "trial-test"
          → sessionStorage.getItem("unleash_impression_trial-test_v1") = "true"
          → ❌ Отправка пропущена (уже было)
          → Console: [Unleash Impression] Skipped (already sent)

Сценарий 3: Без Google Analytics

T+0ms:    GA не установлена
          → window.gtag = undefined
          → sendUnleashImpression() проверяет window.gtag
          → ❌ Отправка пропущена (gracefully)
          → Console: [Unleash Impression] Google Analytics not available
          → ✅ AB тесты продолжают работать нормально

🔍 Тестирование

1. Проверка impression событий

# 1. Запустить dev сервер
npm run dev:full

# 2. Открыть консоль браузера
# 3. Увидеть логи:
[Unleash Impression] Sent: { feature: "trial-test", variant: "v1" }

# 4. Перезагрузить страницу (F5)
# 5. Увидеть:
[Unleash Impression] Skipped (already sent): { feature: "trial-test", variant: "v1" }

2. Проверка sessionStorage

// В консоли браузера:
Object.keys(sessionStorage)
  .filter(key => key.startsWith('unleash_impression_'))
  .forEach(key => console.log(key, sessionStorage.getItem(key)));

// Output:
// unleash_impression_trial-test_v1 "true"
// unleash_impression_flow-test_short "true"

3. Проверка в Google Analytics

1. Откройте GA → Realtime → Events
2. Найдите событие: experiment_impression
3. Параметры должны быть:
   - app_name: "witlab-funnel"
   - feature: "trial-test"
   - treatment: "v1"

4. Очистка для повторного тестирования

// В консоли браузера:
Object.keys(sessionStorage)
  .filter(key => key.startsWith('unleash_impression_'))
  .forEach(key => sessionStorage.removeItem(key));

🐛 Debug и мониторинг

Development логи

// При отправке нового impression:
[Unleash Impression] Sent: {
  feature: "trial-button-test",
  variant: "v1"
}

// При попытке отправить дубликат:
[Unleash Impression] Skipped (already sent): {
  feature: "trial-button-test",
  variant: "v1"
}

// Если GA не доступна:
[Unleash Impression] Google Analytics not available

Production мониторинг

// Логи отключены в production
// Для мониторинга используйте:
// 1. Google Analytics DebugView
// 2. Network tab (запросы к google-analytics.com)
// 3. Sentry/другие error tracking сервисы

📦 Файлы изменений

Файл Изменение Статус
src/lib/funnel/unleash/useUnleash.ts Убрана автоотправка impression Изменен
src/lib/funnel/unleash/sendImpression.ts Новый файл для ручной отправки Создан
src/lib/funnel/unleash/index.ts Экспорт sendUnleashImpression Обновлен
src/components/funnel/FunnelRuntime.tsx Логика отправки по экранам Изменен
src/lib/funnel/unleash/useScreenUnleash.ts Альтернативный подход (не используется) Создан

🎯 Результаты

Решенные проблемы:

  1. Преждевременная отправка - ИСПРАВЛЕНО

    • Было: события для всех экранов сразу
    • Стало: события только для видимых экранов
  2. Дубликаты при перезагрузке - ИСПРАВЛЕНО

    • Было: повторная отправка при F5
    • Стало: sessionStorage блокирует дубликаты
  3. Завышенные метрики - ИСПРАВЛЕНО

    • Было: 100% impression vs 20% достигших
    • Стало: impression = реальная видимость

Сохраненные преимущества:

  1. Быстрые переходы - флаги загружаются заранее
  2. Нет задержек - все готово к моменту рендера
  3. Graceful degradation - работает без GA
  4. Обратная совместимость - не ломает существующий код

🔄 Миграция (если нужно откатить)

Если понадобится вернуться к старой логике:

// В useUnleash.ts восстановить:
useEffect(() => {
  unleashClient.on("impression", handleImpression);
  return () => unleashClient.off("impression", handleImpression);
}, [unleashClient]);

// В FunnelRuntime.tsx удалить:
// - currentScreenFlags
// - useEffect с sendUnleashImpression

📚 Связанные документы

  • UNLEASH_ANALYTICS_FLOW.md - Как работает отправка в GA
  • UNLEASH_ANALYTICS_FIX.md - Анализ проблемы и варианты решения
  • AB_TESTING_GUIDE.md - Общее руководство по AB тестам

Дата: 2025-01-20
Статус: Реализовано и протестировано
Версия: 1.0