345 lines
13 KiB
Markdown
345 lines
13 KiB
Markdown
# ✅ Реализация Lazy Impression для Unleash AB тестов
|
||
|
||
## 🎯 Цель
|
||
|
||
Отправлять impression события в Google Analytics **только когда пользователь реально видит экран** с AB тестом, но при этом **загружать все флаги заранее** для быстрых переходов.
|
||
|
||
---
|
||
|
||
## 🏗️ Архитектура решения
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ FunnelUnleashWrapper │
|
||
│ ✅ Загружает ВСЕ флаги из воронки заранее │
|
||
│ ✅ Сохраняет активные варианты в UnleashContext │
|
||
│ ❌ НЕ отправляет impression автоматически │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
↓
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ UnleashContext │
|
||
│ activeVariants: { "trial-test": "v1", "flow-test": "v2" } │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
↓
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ FunnelRuntime │
|
||
│ 1. Собирает флаги для ТЕКУЩЕГО экрана │
|
||
│ 2. Получает варианты из activeVariants │
|
||
│ 3. Отправляет impression через sendUnleashImpression() │
|
||
│ 4. sessionStorage предотвращает дубликаты │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 📝 Реализованные изменения
|
||
|
||
### 1. Убрана автоматическая отправка из `useUnleash.ts`
|
||
|
||
**Было:**
|
||
```typescript
|
||
// useUnleash автоматически подписывался на события impression
|
||
useEffect(() => {
|
||
unleashClient.on("impression", handleImpression);
|
||
// Отправлял события сразу при загрузке флагов
|
||
}, [unleashClient]);
|
||
```
|
||
|
||
**Стало:**
|
||
```typescript
|
||
// useUnleash теперь только получает вариант, не отправляет события
|
||
export function useUnleash({ flag }: UseUnleashProps) {
|
||
const variant = useVariant(flag);
|
||
return { variant: variant?.name };
|
||
}
|
||
```
|
||
|
||
### 2. Создан `sendImpression.ts` - ручная отправка
|
||
|
||
```typescript
|
||
export function sendUnleashImpression(flag: string, variant: string | undefined) {
|
||
// Проверки валидности
|
||
if (!variant || variant === "disabled") return;
|
||
if (typeof window === "undefined") return;
|
||
|
||
// ✅ Защита от дубликатов через sessionStorage
|
||
const storageKey = `unleash_impression_${flag}_${variant}`;
|
||
if (sessionStorage.getItem(storageKey)) return;
|
||
|
||
// Отправка в GA
|
||
if (window.gtag) {
|
||
window.gtag("event", "experiment_impression", {
|
||
app_name: "witlab-funnel",
|
||
feature: flag,
|
||
treatment: variant,
|
||
});
|
||
|
||
sessionStorage.setItem(storageKey, "true");
|
||
}
|
||
}
|
||
```
|
||
|
||
**Особенности:**
|
||
- ✅ sessionStorage - предотвращает дубликаты при перезагрузке
|
||
- ✅ Graceful degradation - не падает если GA не установлена
|
||
- ✅ Debug логи в development режиме
|
||
|
||
### 3. Добавлена логика в `FunnelRuntime.tsx`
|
||
|
||
```typescript
|
||
// Собираем флаги для ТЕКУЩЕГО экрана
|
||
const currentScreenFlags = useMemo(() => {
|
||
const flags = new Set<string>();
|
||
|
||
// Из вариантов экрана
|
||
currentScreen.variants?.forEach((variant) => {
|
||
variant.conditions.forEach((condition) => {
|
||
if (condition.conditionType === "unleash" && condition.unleashFlag) {
|
||
flags.add(condition.unleashFlag);
|
||
}
|
||
});
|
||
});
|
||
|
||
// Из правил навигации
|
||
currentScreen.navigation?.rules?.forEach((rule) => {
|
||
rule.conditions.forEach((condition) => {
|
||
if (condition.conditionType === "unleash" && condition.unleashFlag) {
|
||
flags.add(condition.unleashFlag);
|
||
}
|
||
});
|
||
});
|
||
|
||
return Array.from(flags);
|
||
}, [currentScreen]);
|
||
|
||
// Отправляем impression когда экран меняется
|
||
useEffect(() => {
|
||
if (currentScreenFlags.length === 0) return;
|
||
|
||
currentScreenFlags.forEach((flag) => {
|
||
const variant = activeVariants[flag];
|
||
sendUnleashImpression(flag, variant);
|
||
});
|
||
}, [currentScreenFlags, activeVariants]);
|
||
```
|
||
|
||
**Ключевые моменты:**
|
||
- Собираем флаги **только для текущего экрана**
|
||
- Получаем варианты из `activeVariants` (уже загружены)
|
||
- Отправляем impression **при рендере экрана**
|
||
- `sessionStorage` не дает отправить дважды
|
||
|
||
---
|
||
|
||
## ✅ Преимущества решения
|
||
|
||
| Критерий | Результат |
|
||
|----------|-----------|
|
||
| **Точность аналитики** | ✅ Impression = пользователь увидел экран |
|
||
| **Нет дубликатов** | ✅ sessionStorage предотвращает |
|
||
| **Скорость загрузки** | ✅ Флаги загружаются заранее (preload) |
|
||
| **Плавность переходов** | ✅ Нет задержек - все уже готово |
|
||
| **Обратная совместимость** | ✅ Существующая логика не затронута |
|
||
|
||
---
|
||
|
||
## 📊 Примеры работы
|
||
|
||
### Сценарий 1: Пользователь проходит воронку
|
||
|
||
```
|
||
T+0ms: Открывает /soulmate/onboarding
|
||
→ FunnelUnleashWrapper загружает все флаги
|
||
→ activeVariants: { "trial-test": "v1", "flow-test": "short" }
|
||
|
||
T+500ms: Экран onboarding рендерится
|
||
→ FunnelRuntime собирает флаги для onboarding: []
|
||
→ Impression не отправляется (нет AB тестов)
|
||
|
||
T+10s: Переходит на /soulmate/payment
|
||
→ Экран payment рендерится
|
||
→ FunnelRuntime собирает флаги: ["trial-test"]
|
||
→ ✅ Отправка: experiment_impression { feature: "trial-test", treatment: "v1" }
|
||
→ sessionStorage: "unleash_impression_trial-test_v1" = "true"
|
||
|
||
T+20s: Переходит на /soulmate/gender
|
||
→ Экран gender рендерится
|
||
→ FunnelRuntime собирает флаги: ["flow-test"]
|
||
→ ✅ Отправка: experiment_impression { feature: "flow-test", treatment: "short" }
|
||
```
|
||
|
||
### Сценарий 2: Пользователь перезагружает страницу
|
||
|
||
```
|
||
T+0ms: На экране payment
|
||
→ FunnelRuntime пытается отправить impression для "trial-test"
|
||
→ sessionStorage.getItem("unleash_impression_trial-test_v1") = "true"
|
||
→ ❌ Отправка пропущена (уже было)
|
||
→ Console: [Unleash Impression] Skipped (already sent)
|
||
```
|
||
|
||
### Сценарий 3: Без Google Analytics
|
||
|
||
```
|
||
T+0ms: GA не установлена
|
||
→ window.gtag = undefined
|
||
→ sendUnleashImpression() проверяет window.gtag
|
||
→ ❌ Отправка пропущена (gracefully)
|
||
→ Console: [Unleash Impression] Google Analytics not available
|
||
→ ✅ AB тесты продолжают работать нормально
|
||
```
|
||
|
||
---
|
||
|
||
## 🔍 Тестирование
|
||
|
||
### 1. Проверка impression событий
|
||
|
||
```bash
|
||
# 1. Запустить dev сервер
|
||
npm run dev:full
|
||
|
||
# 2. Открыть консоль браузера
|
||
# 3. Увидеть логи:
|
||
[Unleash Impression] Sent: { feature: "trial-test", variant: "v1" }
|
||
|
||
# 4. Перезагрузить страницу (F5)
|
||
# 5. Увидеть:
|
||
[Unleash Impression] Skipped (already sent): { feature: "trial-test", variant: "v1" }
|
||
```
|
||
|
||
### 2. Проверка sessionStorage
|
||
|
||
```javascript
|
||
// В консоли браузера:
|
||
Object.keys(sessionStorage)
|
||
.filter(key => key.startsWith('unleash_impression_'))
|
||
.forEach(key => console.log(key, sessionStorage.getItem(key)));
|
||
|
||
// Output:
|
||
// unleash_impression_trial-test_v1 "true"
|
||
// unleash_impression_flow-test_short "true"
|
||
```
|
||
|
||
### 3. Проверка в Google Analytics
|
||
|
||
```
|
||
1. Откройте GA → Realtime → Events
|
||
2. Найдите событие: experiment_impression
|
||
3. Параметры должны быть:
|
||
- app_name: "witlab-funnel"
|
||
- feature: "trial-test"
|
||
- treatment: "v1"
|
||
```
|
||
|
||
### 4. Очистка для повторного тестирования
|
||
|
||
```javascript
|
||
// В консоли браузера:
|
||
Object.keys(sessionStorage)
|
||
.filter(key => key.startsWith('unleash_impression_'))
|
||
.forEach(key => sessionStorage.removeItem(key));
|
||
```
|
||
|
||
---
|
||
|
||
## 🐛 Debug и мониторинг
|
||
|
||
### Development логи
|
||
|
||
```javascript
|
||
// При отправке нового impression:
|
||
[Unleash Impression] Sent: {
|
||
feature: "trial-button-test",
|
||
variant: "v1"
|
||
}
|
||
|
||
// При попытке отправить дубликат:
|
||
[Unleash Impression] Skipped (already sent): {
|
||
feature: "trial-button-test",
|
||
variant: "v1"
|
||
}
|
||
|
||
// Если GA не доступна:
|
||
[Unleash Impression] Google Analytics not available
|
||
```
|
||
|
||
### Production мониторинг
|
||
|
||
```javascript
|
||
// Логи отключены в production
|
||
// Для мониторинга используйте:
|
||
// 1. Google Analytics DebugView
|
||
// 2. Network tab (запросы к google-analytics.com)
|
||
// 3. Sentry/другие error tracking сервисы
|
||
```
|
||
|
||
---
|
||
|
||
## 📦 Файлы изменений
|
||
|
||
| Файл | Изменение | Статус |
|
||
|------|-----------|--------|
|
||
| `src/lib/funnel/unleash/useUnleash.ts` | Убрана автоотправка impression | ✅ Изменен |
|
||
| `src/lib/funnel/unleash/sendImpression.ts` | Новый файл для ручной отправки | ✅ Создан |
|
||
| `src/lib/funnel/unleash/index.ts` | Экспорт sendUnleashImpression | ✅ Обновлен |
|
||
| `src/components/funnel/FunnelRuntime.tsx` | Логика отправки по экранам | ✅ Изменен |
|
||
| `src/lib/funnel/unleash/useScreenUnleash.ts` | Альтернативный подход (не используется) | ℹ️ Создан |
|
||
|
||
---
|
||
|
||
## 🎯 Результаты
|
||
|
||
### ✅ Решенные проблемы:
|
||
|
||
1. **Преждевременная отправка** - ИСПРАВЛЕНО
|
||
- Было: события для всех экранов сразу
|
||
- Стало: события только для видимых экранов
|
||
|
||
2. **Дубликаты при перезагрузке** - ИСПРАВЛЕНО
|
||
- Было: повторная отправка при F5
|
||
- Стало: sessionStorage блокирует дубликаты
|
||
|
||
3. **Завышенные метрики** - ИСПРАВЛЕНО
|
||
- Было: 100% impression vs 20% достигших
|
||
- Стало: impression = реальная видимость
|
||
|
||
### ✅ Сохраненные преимущества:
|
||
|
||
1. **Быстрые переходы** - флаги загружаются заранее
|
||
2. **Нет задержек** - все готово к моменту рендера
|
||
3. **Graceful degradation** - работает без GA
|
||
4. **Обратная совместимость** - не ломает существующий код
|
||
|
||
---
|
||
|
||
## 🔄 Миграция (если нужно откатить)
|
||
|
||
Если понадобится вернуться к старой логике:
|
||
|
||
```typescript
|
||
// В useUnleash.ts восстановить:
|
||
useEffect(() => {
|
||
unleashClient.on("impression", handleImpression);
|
||
return () => unleashClient.off("impression", handleImpression);
|
||
}, [unleashClient]);
|
||
|
||
// В FunnelRuntime.tsx удалить:
|
||
// - currentScreenFlags
|
||
// - useEffect с sendUnleashImpression
|
||
```
|
||
|
||
---
|
||
|
||
## 📚 Связанные документы
|
||
|
||
- `UNLEASH_ANALYTICS_FLOW.md` - Как работает отправка в GA
|
||
- `UNLEASH_ANALYTICS_FIX.md` - Анализ проблемы и варианты решения
|
||
- `AB_TESTING_GUIDE.md` - Общее руководство по AB тестам
|
||
|
||
---
|
||
|
||
**Дата:** 2025-01-20
|
||
**Статус:** ✅ Реализовано и протестировано
|
||
**Версия:** 1.0
|