13 KiB
13 KiB
✅ Реализация 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
Было:
// useUnleash автоматически подписывался на события impression
useEffect(() => {
unleashClient.on("impression", handleImpression);
// Отправлял события сразу при загрузке флагов
}, [unleashClient]);
Стало:
// useUnleash теперь только получает вариант, не отправляет события
export function useUnleash({ flag }: UseUnleashProps) {
const variant = useVariant(flag);
return { variant: variant?.name };
}
2. Создан sendImpression.ts - ручная отправка
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
// Собираем флаги для ТЕКУЩЕГО экрана
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 событий
# 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
// В консоли браузера:
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. Очистка для повторного тестирования
// В консоли браузера:
Object.keys(sessionStorage)
.filter(key => key.startsWith('unleash_impression_'))
.forEach(key => sessionStorage.removeItem(key));
🐛 Debug и мониторинг
Development логи
// При отправке нового 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 мониторинг
// Логи отключены в 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 |
Альтернативный подход (не используется) | ℹ️ Создан |
🎯 Результаты
✅ Решенные проблемы:
-
Преждевременная отправка - ИСПРАВЛЕНО
- Было: события для всех экранов сразу
- Стало: события только для видимых экранов
-
Дубликаты при перезагрузке - ИСПРАВЛЕНО
- Было: повторная отправка при F5
- Стало: sessionStorage блокирует дубликаты
-
Завышенные метрики - ИСПРАВЛЕНО
- Было: 100% impression vs 20% достигших
- Стало: impression = реальная видимость
✅ Сохраненные преимущества:
- Быстрые переходы - флаги загружаются заранее
- Нет задержек - все готово к моменту рендера
- Graceful degradation - работает без GA
- Обратная совместимость - не ломает существующий код
🔄 Миграция (если нужно откатить)
Если понадобится вернуться к старой логике:
// В useUnleash.ts восстановить:
useEffect(() => {
unleashClient.on("impression", handleImpression);
return () => unleashClient.off("impression", handleImpression);
}, [unleashClient]);
// В FunnelRuntime.tsx удалить:
// - currentScreenFlags
// - useEffect с sendUnleashImpression
📚 Связанные документы
UNLEASH_ANALYTICS_FLOW.md- Как работает отправка в GAUNLEASH_ANALYTICS_FIX.md- Анализ проблемы и варианты решенияAB_TESTING_GUIDE.md- Общее руководство по AB тестам
Дата: 2025-01-20
Статус: ✅ Реализовано и протестировано
Версия: 1.0