w-funnel/docs/AB_TEST_MULTIPLE_CALLS_FIX.md
dev.daminik00 3e735ab0f6 add
2025-10-24 00:05:07 +02:00

9.6 KiB
Raw Permalink Blame History

🐛 Исправление: Множественные вызовы 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 - это объект, который:

  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. Создание стабильного ключа

// Создаем мемоизированную строку вариантов
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");

Два уровня защиты:

  1. useEffect оптимизация - предотвращает лишние вызовы функции
  2. 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-тестов