# 🐛 Проблема: Преждевременная отправка Unleash impression ## Описание проблемы ### ❌ Текущая реализация ```typescript // FunnelUnleashWrapper.tsx // Сканирует ВСЮ воронку и загружает ВСЕ флаги сразу const allFlags = useMemo(() => { funnel.screens.forEach(screen => { // Собирает флаги со ВСЕХ экранов screen.variants?.forEach(...) screen.navigation?.rules?.forEach(...) }); return Array.from(flags); }, [funnel.screens]); // Получает варианты для ВСЕХ флагов allFlags.map(flag => useVariant(flag)); // ↓ // Unleash SDK эмитит "impression" для ВСЕХ флагов // ↓ // Отправка в GA для экранов, которые пользователь НЕ ВИДЕЛ ``` ### 🐛 Проблемы: 1. **Преждевременная отправка** ``` Пользователь на экране 1/10 → События отправлены для всех 10 экранов → Аналитика: "100% видели AB тест на экране 10" → Реальность: "Только 20% дошли до экрана 10" ``` 2. **Повторная отправка при перезагрузке** ``` T+0: Пользователь открыл воронку → события отправлены T+60s: Пользователь перезагрузил страницу → события отправлены СНОВА → Дубликаты в аналитике ``` 3. **Искажение метрик** - Impression не отражает реальную видимость - Невозможно посчитать conversion rate "видел AB тест → совершил действие" - Завышенное количество impression событий --- ## ✅ Решение 1: Per-Screen Impressions (Рекомендуется) ### Идея: Отправлять impression события **только когда пользователь дошел до экрана** ### Реализация: #### 1. Создать новый хук `useScreenUnleash`: ```typescript // src/lib/funnel/unleash/useScreenUnleash.ts export function useScreenUnleash(flags: string[]) { // Получаем варианты только для флагов ТЕКУЩЕГО экрана const variants = flags.map(flag => useVariant(flag)); useEffect(() => { // Отправляем события только когда экран рендерится variants.forEach(({ flag, variant }) => { // Проверяем не отправляли ли уже (sessionStorage) const storageKey = `unleash_impression_${flag}_${variant}`; if (!sessionStorage.getItem(storageKey)) { // Отправка в GA window.gtag("event", "experiment_impression", {...}); sessionStorage.setItem(storageKey, "true"); } }); }, [variants]); return activeVariants; } ``` #### 2. Использовать в FunnelRuntime: ```typescript // src/components/funnel/FunnelRuntime.tsx export function FunnelRuntime({ funnel, initialScreenId }) { const currentScreen = ...; // Собираем флаги только для ТЕКУЩЕГО экрана const currentScreenFlags = useMemo(() => { const flags = new Set(); // Из вариантов текущего экрана currentScreen.variants?.forEach(variant => { variant.conditions.forEach(condition => { if (condition.conditionType === "unleash") { flags.add(condition.unleashFlag); } }); }); // Из правил навигации текущего экрана currentScreen.navigation?.rules?.forEach(rule => { rule.conditions.forEach(condition => { if (condition.conditionType === "unleash") { flags.add(condition.unleashFlag); } }); }); return Array.from(flags); }, [currentScreen]); // Загружаем и отправляем impression только для текущего экрана const screenVariants = useScreenUnleash(currentScreenFlags); // ... rest } ``` ### ✅ Преимущества: 1. **Точная аналитика**: impression = пользователь реально увидел экран 2. **Нет дубликатов**: sessionStorage помнит отправленные события 3. **Последовательная загрузка**: флаги загружаются по мере прохождения воронки ### ⚠️ Недостатки: 1. **Прогрузка флагов**: небольшая задержка при переходе на новый экран (если флаг еще не загружен) 2. **Сложность**: нужно отслеживать флаги для каждого экрана отдельно --- ## ✅ Решение 2: Preload + Lazy Impression (Баланс) ### Идея: Загружать все флаги заранее (как сейчас), но отправлять impression только по мере просмотра экранов ### Реализация: #### 1. Убрать автоматическую отправку из useUnleash: ```typescript // src/lib/funnel/unleash/useUnleash.ts export function useUnleash({ flag }: UseUnleashProps) { // ... existing code // ❌ Убрать автоматическую отправку // useEffect(() => { // unleashClient.on("impression", handleImpression); // }, [unleashClient]); } ``` #### 2. Создать хук для ручной отправки: ```typescript // src/lib/funnel/unleash/useSendImpression.ts export function useSendImpression( flag: string, variant: string | undefined ) { useEffect(() => { if (!variant || variant === "disabled") return; if (typeof window === "undefined" || !window.gtag) return; // Проверка дубликатов const storageKey = `unleash_impression_${flag}_${variant}`; if (sessionStorage.getItem(storageKey)) return; // Отправка window.gtag("event", "experiment_impression", { app_name: "witlab-funnel", feature: flag, treatment: variant, }); sessionStorage.setItem(storageKey, "true"); }, [flag, variant]); } ``` #### 3. Отправлять вручную в FunnelRuntime: ```typescript // src/components/funnel/FunnelRuntime.tsx export function FunnelRuntime({ funnel, initialScreenId }) { const { checkVariant } = useUnleashContext(); const currentScreen = ...; // Флаги уже загружены через FunnelUnleashWrapper // Отправляем impression только для текущего экрана useEffect(() => { const currentFlags = extractFlagsFromScreen(currentScreen); currentFlags.forEach(flag => { const variant = checkVariant(flag, [], "includesAny"); // Отправляем impression useSendImpression(flag, variant); }); }, [currentScreen]); } ``` ### ✅ Преимущества: 1. **Быстрый переход**: флаги уже загружены заранее 2. **Точная аналитика**: impression когда экран виден 3. **Нет дубликатов**: sessionStorage ### ⚠️ Недостатки: 1. **Лишние запросы**: загружаем флаги которые пользователь может не увидеть --- ## ✅ Решение 3: Гибридный подход (Оптимальный) ### Идея: - Загружаем флаги **только для первых N экранов** (например 3) - По мере прохождения догружаем следующие - Impression отправляем только при просмотре ### Реализация: ```typescript // FunnelUnleashWrapper с preload окном export function FunnelUnleashWrapper({ funnel, currentScreenIndex }) { const PRELOAD_WINDOW = 3; // Загружаем флаги для текущего + 2 следующих const preloadFlags = useMemo(() => { const startIndex = Math.max(0, currentScreenIndex); const endIndex = Math.min(funnel.screens.length, currentScreenIndex + PRELOAD_WINDOW); const screensToPreload = funnel.screens.slice(startIndex, endIndex); return extractFlagsFromScreens(screensToPreload); }, [currentScreenIndex, funnel.screens]); // Загружаем только флаги в окне preload const flagVariants = preloadFlags.map(flag => useVariant(flag)); // Impression отправляется только в FunnelRuntime когда экран виден } ``` ### ✅ Преимущества: 1. **Оптимальная производительность**: загружаем только нужное 2. **Плавные переходы**: следующие экраны уже готовы 3. **Точная аналитика**: impression = видимость 4. **Экономия**: не загружаем флаги для экранов 7-10 если пользователь на экране 2 --- ## 📊 Сравнение решений | Критерий | Текущее | Решение 1 | Решение 2 | Решение 3 | |----------|---------|-----------|-----------|-----------| | **Точность аналитики** | ❌ Плохо | ✅ Отлично | ✅ Отлично | ✅ Отлично | | **Скорость загрузки** | ✅ Быстро | ⚠️ Медленно | ✅ Быстро | ✅ Быстро | | **Плавность переходов** | ✅ Плавно | ❌ Лаги | ✅ Плавно | ✅ Плавно | | **Нет дубликатов** | ❌ Есть | ✅ Нет | ✅ Нет | ✅ Нет | | **Экономия ресурсов** | ❌ Плохо | ✅ Хорошо | ❌ Плохо | ✅ Отлично | | **Сложность реализации** | ✅ Простая | ⚠️ Средняя | ⚠️ Средняя | ❌ Сложная | --- ## 🎯 Рекомендация ### Для witlab-funnel: **Решение 2 (Preload + Lazy Impression)** **Почему:** 1. Воронки короткие (обычно 5-10 экранов) - preload допустим 2. Плавность переходов важна для UX 3. Относительно просто реализовать 4. Исправляет главную проблему - точность аналитики ### Для длинных воронок (20+ экранов): **Решение 3 (Гибридный)** --- ## 📝 TODO для исправления - [ ] Создать `useScreenUnleash.ts` хук - [ ] Убрать автоотправку из `useUnleash.ts` (или сделать опциональной) - [ ] Добавить sessionStorage для предотвращения дубликатов - [ ] Интегрировать в `FunnelRuntime.tsx` - [ ] Обновить тесты - [ ] Обновить документацию --- ## 🔧 Быстрый фикс (минимальный) Если нужно быстро исправить проблему с дубликатами при перезагрузке: ```typescript // src/lib/funnel/unleash/useUnleash.ts useEffect(() => { const handleImpression = (impressionEvent) => { if (impressionEvent.enabled) { // Проверка дубликатов const storageKey = `unleash_${impressionEvent.featureName}_${impressionEvent.variant}`; if (sessionStorage.getItem(storageKey)) { return; // Уже отправляли } // Отправка window.gtag("event", "experiment_impression", {...}); // Помечаем sessionStorage.setItem(storageKey, "true"); } }; unleashClient.on("impression", handleImpression); }, [unleashClient]); ``` Это хотя бы предотвратит дубликаты при перезагрузке, но не решит проблему преждевременной отправки.