w-funnel/UNLEASH_ANALYTICS_FIX.md
dev.daminik00 6c50d05123 ab
2025-10-21 01:27:08 +02:00

325 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🐛 Проблема: Преждевременная отправка 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<string>();
// Из вариантов текущего экрана
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]);
```
Это хотя бы предотвратит дубликаты при перезагрузке, но не решит проблему преждевременной отправки.