9.6 KiB
🐛 Исправление: Множественные вызовы 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() для одного экрана.
🔍 Причина
Исходный код
// FunnelRuntime.tsx (ДО исправления)
useEffect(() => {
currentScreenFlags.forEach((flag) => {
const variant = activeVariants[flag];
sendUnleashImpression(flag, variant);
});
}, [currentScreenFlags, activeVariants]); // ← Проблема!
Почему это происходило
activeVariants - это объект, который:
- Приходит из React Context (
UnleashContext) - Обновляется при загрузке вариантов флагов
- Может иметь новую ссылку при каждом рендере компонента
Результат: 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. Создание стабильного ключа
// Создаем мемоизированную строку вариантов
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
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. Очистить кеш браузера
// В консоли браузера:
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:
// В sendImpression.ts
const storageKey = `unleash_impression_${flag}_${variant}`;
const alreadySent = sessionStorage.getItem(storageKey);
if (alreadySent) {
// Уже отправляли - пропускаем
return;
}
// Отправка...
sessionStorage.setItem(storageKey, "true");
Два уровня защиты:
- ✅ useEffect оптимизация - предотвращает лишние вызовы функции
- ✅ sessionStorage проверка - предотвращает дубликаты в GA (на случай багов)
📝 Сводка изменений
Файл: src/components/funnel/FunnelRuntime.tsx
Добавлено:
// Создание стабильного ключа
const currentFlagsKey = useMemo(() => {
if (currentScreenFlags.length === 0) return "";
return currentScreenFlags
.map(flag => `${flag}:${activeVariants[flag] || "loading"}`)
.sort()
.join(",");
}, [currentScreenFlags, activeVariants]);
Изменено:
// Использование стабильного ключа в dependencies
useEffect(() => {
// ... логика отправки
}, [currentScreen.id, currentFlagsKey]); // ← Было: activeVariants
Добавлено:
// Проверка что все флаги загружены
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-тестов