# 🐛 Исправление: Множественные вызовы AB Test Impression ## 🔴 Проблема При запуске воронки в консоли браузера появлялись **множественные логи** для одного AB-теста: ``` [GA] 🧪 AB Test Impression SKIPPED [GA] 🧪 AB Test Impression Event Sent [GA] 🧪 AB Test Impression SKIPPED (Already Sent) [GA] 🧪 AB Test Impression SKIPPED (Already Sent) [GA] 🧪 AB Test Impression SKIPPED (Already Sent) ``` **Ожидаемое поведение:** Только **один вызов** `sendUnleashImpression()` для каждого экрана. **Фактическое поведение:** **5 вызовов** `sendUnleashImpression()` для одного экрана. --- ## 🔍 Причина ### Исходный код ```typescript // FunnelRuntime.tsx (ДО исправления) useEffect(() => { currentScreenFlags.forEach((flag) => { const variant = activeVariants[flag]; sendUnleashImpression(flag, variant); }); }, [currentScreenFlags, activeVariants]); // ← Проблема! ``` ### Почему это происходило `activeVariants` - это **объект**, который: 1. Приходит из React Context (`UnleashContext`) 2. Обновляется при загрузке вариантов флагов 3. Может иметь **новую ссылку** при каждом рендере компонента **Результат:** `useEffect` срабатывал при каждом изменении ссылки на объект, даже если содержимое оставалось прежним. ### Последовательность событий ``` T+0ms FunnelRuntime монтируется activeVariants = {} (пустой) ↓ useEffect срабатывает #1 variant = undefined → SKIPPED (invalid variant) T+100ms activeVariants обновляется activeVariants = { "soulmate-onboarding-image": "v1" } ↓ useEffect срабатывает #2 variant = "v1" → Event Sent ✅ T+110ms activeVariants получает новую ссылку (тот же контент) activeVariants = { "soulmate-onboarding-image": "v1" } (новая ссылка) ↓ useEffect срабатывает #3 → SKIPPED (Already Sent) T+120ms activeVariants снова обновляется ↓ useEffect срабатывает #4 → SKIPPED (Already Sent) T+130ms activeVariants снова обновляется ↓ useEffect срабатывает #5 → SKIPPED (Already Sent) ``` --- ## ✅ Решение ### 1. Создание стабильного ключа ```typescript // Создаем мемоизированную строку вариантов const currentFlagsKey = useMemo(() => { if (currentScreenFlags.length === 0) { return ""; } // Строка вида "flag1:variant1,flag2:variant2" return currentScreenFlags .map(flag => `${flag}:${activeVariants[flag] || "loading"}`) .sort() .join(","); }, [currentScreenFlags, activeVariants]); ``` **Пример значений:** - `""` - нет флагов - `"soulmate-onboarding-image:loading"` - флаг загружается - `"soulmate-onboarding-image:v1"` - флаг загружен ### 2. Использование стабильного ключа в useEffect ```typescript useEffect(() => { if (currentScreenFlags.length === 0) { return; } // Проверяем что все флаги загружены const allFlagsLoaded = currentScreenFlags.every(flag => { const variant = activeVariants[flag]; return variant !== undefined && variant !== "loading"; }); if (!allFlagsLoaded) { return; // Ждем загрузки } // Отправляем impression currentScreenFlags.forEach((flag) => { const variant = activeVariants[flag]; sendUnleashImpression(flag, variant); }); }, [currentScreen.id, currentFlagsKey]); // ← Стабильная зависимость! ``` ### 3. Как это работает **Старая логика:** - useEffect зависит от `activeVariants` (объект) - При каждом изменении **ссылки** на объект → useEffect срабатывает - **Результат:** 5 вызовов **Новая логика:** - useEffect зависит от `currentFlagsKey` (строка) - При изменении **значений** вариантов → строка меняется → useEffect срабатывает - **Результат:** 1 вызов --- ## 📊 Ожидаемое поведение после исправления ### В консоли браузера ``` ▼ [GA] 🧪 AB Test Impression Event Sent 🕐 Timestamp: 2025-10-23T19:15:42.456Z 🏷️ Flag: soulmate-onboarding-image 🎯 Variant: v1 📦 Event Name: experiment_impression 📦 Payload: { ... } ✅ Status: Successfully sent to Google Analytics ``` **Только ОДИН лог** для каждого экрана с AB-тестом! ### Timeline событий ``` T+0ms FunnelRuntime монтируется currentFlagsKey = "" ↓ useEffect: нет флагов, return T+100ms Флаги загружаются currentFlagsKey = "soulmate-onboarding-image:loading" ↓ useEffect: флаги не загружены, return T+200ms Флаги загружены currentFlagsKey = "soulmate-onboarding-image:v1" ↓ useEffect срабатывает (ОДИН РАЗ) → Event Sent ✅ T+300ms activeVariants получает новую ссылку (тот же контент) currentFlagsKey = "soulmate-onboarding-image:v1" (не изменился!) ↓ useEffect НЕ срабатывает (ключ не изменился) ``` **Результат:** Только **1 вызов** `sendUnleashImpression()` ✅ --- ## 🧪 Как проверить ### 1. Очистить кеш браузера ```javascript // В консоли браузера: sessionStorage.clear(); ``` ### 2. Открыть воронку в Incognito ``` Chrome: Ctrl+Shift+N (Windows) / Cmd+Shift+N (Mac) Firefox: Ctrl+Shift+P (Windows) / Cmd+Shift+P (Mac) ``` ### 3. Открыть первый экран ``` http://localhost:3000/soulmate/onboarding ``` ### 4. Проверить консоль **Ожидаемый результат:** ``` [GA] 📊 Page View Event Sent [YM] 📊 Page View Event Sent [GA] 🧪 AB Test Impression Event Sent ← ТОЛЬКО ОДИН РАЗ! ``` **НЕ должно быть:** - ❌ Множественных "SKIPPED" логов - ❌ Множественных "Event Sent" логов --- ## 🔒 Дополнительная защита Даже при множественных вызовах `sendUnleashImpression()`, **события НЕ отправляются повторно** благодаря защите через `sessionStorage`: ```typescript // В sendImpression.ts const storageKey = `unleash_impression_${flag}_${variant}`; const alreadySent = sessionStorage.getItem(storageKey); if (alreadySent) { // Уже отправляли - пропускаем return; } // Отправка... sessionStorage.setItem(storageKey, "true"); ``` **Два уровня защиты:** 1. ✅ **useEffect оптимизация** - предотвращает лишние вызовы функции 2. ✅ **sessionStorage проверка** - предотвращает дубликаты в GA (на случай багов) --- ## 📝 Сводка изменений ### Файл: `src/components/funnel/FunnelRuntime.tsx` **Добавлено:** ```typescript // Создание стабильного ключа const currentFlagsKey = useMemo(() => { if (currentScreenFlags.length === 0) return ""; return currentScreenFlags .map(flag => `${flag}:${activeVariants[flag] || "loading"}`) .sort() .join(","); }, [currentScreenFlags, activeVariants]); ``` **Изменено:** ```typescript // Использование стабильного ключа в dependencies useEffect(() => { // ... логика отправки }, [currentScreen.id, currentFlagsKey]); // ← Было: activeVariants ``` **Добавлено:** ```typescript // Проверка что все флаги загружены const allFlagsLoaded = currentScreenFlags.every(flag => { const variant = activeVariants[flag]; return variant !== undefined && variant !== "loading"; }); if (!allFlagsLoaded) { return; // Ждем загрузки } ``` --- ## ✅ Итог **Проблема:** 5 вызовов `sendUnleashImpression()` для одного экрана **Причина:** `activeVariants` объект менял ссылку → useEffect срабатывал многократно **Решение:** Создали стабильный ключ `currentFlagsKey` → useEffect срабатывает только при изменении значений **Результат:** ✅ Только 1 вызов для каждого экрана с AB-тестом **Дополнительно:** ✅ sessionStorage защита на случай багов --- ## 🔗 Связанные документы - `ENHANCED_LOGGING_GUIDE.md` - Как читать логи в консоли - `GA_AB_TEST_ANALYSIS.md` - Техническая документация системы - `SOULMATE_AB_TESTS_TIMELINE.md` - Timeline AB-тестов