# 🐛 Исправление: Множественные вызовы 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-тестов
