# ✅ Реализация 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` **Было:** ```typescript // useUnleash автоматически подписывался на события impression useEffect(() => { unleashClient.on("impression", handleImpression); // Отправлял события сразу при загрузке флагов }, [unleashClient]); ``` **Стало:** ```typescript // useUnleash теперь только получает вариант, не отправляет события export function useUnleash({ flag }: UseUnleashProps) { const variant = useVariant(flag); return { variant: variant?.name }; } ``` ### 2. Создан `sendImpression.ts` - ручная отправка ```typescript 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` ```typescript // Собираем флаги для ТЕКУЩЕГО экрана const currentScreenFlags = useMemo(() => { const flags = new Set(); // Из вариантов экрана 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 событий ```bash # 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 ```javascript // В консоли браузера: 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. Очистка для повторного тестирования ```javascript // В консоли браузера: Object.keys(sessionStorage) .filter(key => key.startsWith('unleash_impression_')) .forEach(key => sessionStorage.removeItem(key)); ``` --- ## 🐛 Debug и мониторинг ### Development логи ```javascript // При отправке нового 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 мониторинг ```javascript // Логи отключены в 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. **Обратная совместимость** - не ломает существующий код --- ## 🔄 Миграция (если нужно откатить) Если понадобится вернуться к старой логике: ```typescript // В 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