ab test
This commit is contained in:
parent
54fdf8dc5a
commit
ea381ea399
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useVariant } from "@unleash/proxy-client-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef, memo } from "react";
|
||||
|
||||
interface FlagVariantFetcherProps {
|
||||
flag: string;
|
||||
@ -12,13 +12,26 @@ interface FlagVariantFetcherProps {
|
||||
* Компонент для получения варианта одного флага
|
||||
* Каждый экземпляр этого компонента вызывает useVariant на верхнем уровне
|
||||
* Это позволяет обходить ограничение правил хуков React
|
||||
*
|
||||
* ВАЖНО: Мемоизация и useRef критически важны для предотвращения
|
||||
* множественных impression событий. Каждый вызов useVariant() генерирует
|
||||
* новое impression событие, поэтому минимизируем ре-рендеры.
|
||||
*
|
||||
* В aura-webapp дедупликации нет - каждое impression отправляется в GA напрямую.
|
||||
*/
|
||||
export function FlagVariantFetcher({ flag, onVariantLoaded }: FlagVariantFetcherProps) {
|
||||
export const FlagVariantFetcher = memo(function FlagVariantFetcher({ flag, onVariantLoaded }: FlagVariantFetcherProps) {
|
||||
const variant = useVariant(flag);
|
||||
const lastVariantRef = useRef<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
onVariantLoaded(flag, variant?.name);
|
||||
const currentVariant = variant?.name;
|
||||
|
||||
// Отправляем только если вариант изменился
|
||||
if (currentVariant !== lastVariantRef.current) {
|
||||
lastVariantRef.current = currentVariant;
|
||||
onVariantLoaded(flag, currentVariant);
|
||||
}
|
||||
}, [flag, variant?.name, onVariantLoaded]);
|
||||
|
||||
return null; // Этот компонент не рендерит UI
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, type ReactNode } from "react";
|
||||
import { useState, useMemo, useCallback, type ReactNode } from "react";
|
||||
import { useFlagsStatus } from "@unleash/proxy-client-react";
|
||||
import { UnleashContextProvider } from "@/lib/funnel/unleash";
|
||||
import { useUnleashAnalytics } from "@/lib/funnel/unleash/useUnleashAnalytics";
|
||||
import { FunnelLoadingScreen } from "./FunnelLoadingScreen";
|
||||
import { FlagVariantFetcher } from "./FlagVariantFetcher";
|
||||
import type { NavigationConditionDefinition } from "@/lib/funnel/types";
|
||||
@ -37,6 +38,10 @@ export function FunnelUnleashWrapper({
|
||||
funnel,
|
||||
currentScreenId,
|
||||
}: FunnelUnleashWrapperProps) {
|
||||
// ✅ КРИТИЧЕСКИ ВАЖНО: Подписываемся на impression события ДО загрузки флагов
|
||||
// Это гарантирует что события от useVariant() будут пойманы и отправлены в аналитику
|
||||
useUnleashAnalytics();
|
||||
|
||||
const { flagsReady } = useFlagsStatus();
|
||||
|
||||
// Собираем флаги ТОЛЬКО для текущего экрана (или все, если currentScreenId не передан)
|
||||
@ -80,16 +85,6 @@ export function FunnelUnleashWrapper({
|
||||
// Состояние для хранения вариантов флагов
|
||||
const [loadedVariants, setLoadedVariants] = useState<Record<string, string>>({});
|
||||
|
||||
// ✅ КРИТИЧЕСКИ ВАЖНО: Очищаем loadedVariants при смене экрана
|
||||
// Это предотвращает ситуацию когда старые варианты используются для нового экрана
|
||||
useEffect(() => {
|
||||
setLoadedVariants({});
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log(`[FunnelUnleashWrapper] Screen changed to "${currentScreenId}", clearing loaded variants`);
|
||||
}
|
||||
}, [currentScreenId]);
|
||||
|
||||
// Колбэк для получения варианта от FlagVariantFetcher компонента
|
||||
const handleVariantLoaded = useCallback((flag: string, variant: string | undefined) => {
|
||||
// ✅ Сохраняем вариант в любом случае (даже если undefined или "disabled")
|
||||
@ -99,9 +94,7 @@ export function FunnelUnleashWrapper({
|
||||
|
||||
// Обновляем только если значение изменилось
|
||||
if (prev[flag] !== newVariant) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log(`[FunnelUnleashWrapper] Flag "${flag}" = "${newVariant}"`);
|
||||
}
|
||||
console.log(`🚩 [FunnelUnleashWrapper] Flag loaded: "${flag}" = "${newVariant}"`);
|
||||
return { ...prev, [flag]: newVariant };
|
||||
}
|
||||
return prev;
|
||||
@ -111,11 +104,13 @@ export function FunnelUnleashWrapper({
|
||||
// Проверяем что ВСЕ флаги текущего экрана загружены
|
||||
const allFlagsLoaded = useMemo(() => {
|
||||
if (!flagsReady) {
|
||||
console.log("⏳ [FunnelUnleashWrapper] Waiting for Unleash client to be ready...");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Если нет флагов на экране - сразу готовы
|
||||
if (currentScreenFlags.length === 0) {
|
||||
console.log("✅ [FunnelUnleashWrapper] No AB test flags on current screen - ready to render");
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -124,16 +119,16 @@ export function FunnelUnleashWrapper({
|
||||
return flag in loadedVariants;
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("[FunnelUnleashWrapper] Flags status:", {
|
||||
currentScreenFlags,
|
||||
loadedVariants,
|
||||
allLoaded,
|
||||
});
|
||||
}
|
||||
console.log(`${allLoaded ? '✅' : '⏳'} [FunnelUnleashWrapper] Flags status:`, {
|
||||
currentScreenId,
|
||||
flagsRequired: currentScreenFlags,
|
||||
flagsLoaded: Object.keys(loadedVariants),
|
||||
allReady: allLoaded,
|
||||
variants: loadedVariants,
|
||||
});
|
||||
|
||||
return allLoaded;
|
||||
}, [flagsReady, currentScreenFlags, loadedVariants]);
|
||||
}, [flagsReady, currentScreenFlags, loadedVariants, currentScreenId]);
|
||||
|
||||
// Создаем объект активных вариантов
|
||||
const activeVariants = useMemo(() => {
|
||||
|
||||
@ -4,33 +4,25 @@ import type { ReactNode } from "react";
|
||||
|
||||
import { FunnelProvider } from "@/lib/funnel/FunnelProvider";
|
||||
import { UnleashProvider } from "./UnleashProvider";
|
||||
import { useUnleashAnalytics } from "@/lib/funnel/unleash/useUnleashAnalytics";
|
||||
|
||||
interface AppProvidersProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Компонент для инициализации автоматической отправки AB test impression событий
|
||||
* Идентично aura-webapp: events отправляются когда пользователь доходит до экрана
|
||||
*/
|
||||
function UnleashAnalyticsInitializer() {
|
||||
useUnleashAnalytics();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Корневой Provider приложения
|
||||
*
|
||||
* Структура идентична aura-webapp:
|
||||
* 1. UnleashProvider (FlagProvider) - инициализация Unleash Client
|
||||
* 2. UnleashAnalyticsInitializer - автоматическая подписка на impression события
|
||||
* 3. FunnelProvider - управление состоянием воронки
|
||||
* ВАЖНО: UnleashAnalyticsInitializer перемещен в FunnelUnleashWrapper
|
||||
* чтобы гарантировать что impression listener готов ДО загрузки флагов
|
||||
*
|
||||
* Структура:
|
||||
* 1. UnleashProvider (FlagProvider) - инициализация Unleash Client (глобально)
|
||||
* 2. FunnelProvider - управление состоянием воронки
|
||||
* 3. FunnelUnleashWrapper (в layout) - подписка на impression события + загрузка флагов
|
||||
*/
|
||||
export function AppProviders({ children }: AppProvidersProps) {
|
||||
return (
|
||||
<UnleashProvider>
|
||||
<UnleashAnalyticsInitializer />
|
||||
<FunnelProvider>{children}</FunnelProvider>
|
||||
</UnleashProvider>
|
||||
);
|
||||
|
||||
@ -47,50 +47,59 @@ export function MetricsProvider({
|
||||
},
|
||||
});
|
||||
|
||||
console.log('[Metrics] Google Analytics initialized:', {
|
||||
console.log('✅ [Metrics] Google Analytics initialized:', {
|
||||
measurementId: googleAnalyticsId,
|
||||
debugMode: isDevelopEnvironment,
|
||||
ready: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Metrics] Failed to initialize Google Analytics:', error);
|
||||
}
|
||||
}, [googleAnalyticsId]);
|
||||
|
||||
// Инициализация Yandex Metrika (синхронно через скрипт)
|
||||
// Инициализация Yandex Metrika (официальный способ)
|
||||
useEffect(() => {
|
||||
if (!yandexMetrikaId) return;
|
||||
|
||||
try {
|
||||
// Проверяем что скрипт еще не загружен
|
||||
if (typeof window.ym === 'function') {
|
||||
console.log('[Metrics] Yandex Metrika already loaded');
|
||||
if (typeof window.ym === 'function' && window.__YM_COUNTER_ID__) {
|
||||
console.log('[Metrics] Yandex Metrika already initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
// Загружаем скрипт Yandex Metrika
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.async = true;
|
||||
script.src = 'https://mc.yandex.ru/metrika/tag.js';
|
||||
|
||||
script.onload = () => {
|
||||
// Инициализируем счетчик после загрузки скрипта
|
||||
if (typeof window.ym === 'function') {
|
||||
window.ym(Number(yandexMetrikaId), 'init', {
|
||||
clickmap: true,
|
||||
trackLinks: true,
|
||||
accurateTrackBounce: true,
|
||||
webvisor: true,
|
||||
});
|
||||
|
||||
// Сохраняем ID счетчика для использования в analytics service
|
||||
window.__YM_COUNTER_ID__ = Number(yandexMetrikaId);
|
||||
|
||||
console.log('[Metrics] Yandex Metrika initialized:', yandexMetrikaId);
|
||||
// ✅ Официальный код инициализации Яндекс Метрики
|
||||
// Создает функцию-заглушку ym() для накопления вызовов до загрузки скрипта
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(function(m: any, e: Document, t: string, r: string, i: string, k: HTMLScriptElement, a: HTMLScriptElement | null) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
m[i] = m[i] || function() { (m[i].a = m[i].a || []).push(arguments); };
|
||||
m[i].l = 1 * new Date().getTime();
|
||||
k = e.createElement(t) as HTMLScriptElement;
|
||||
a = e.getElementsByTagName(t)[0] as HTMLScriptElement;
|
||||
k.async = true;
|
||||
k.src = r;
|
||||
if (a && a.parentNode) {
|
||||
a.parentNode.insertBefore(k, a);
|
||||
}
|
||||
};
|
||||
})(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym", {} as HTMLScriptElement, null);
|
||||
|
||||
document.head.appendChild(script);
|
||||
// ✅ Вызываем init сразу (накопится в очереди до загрузки скрипта)
|
||||
window.ym(Number(yandexMetrikaId), 'init', {
|
||||
clickmap: true,
|
||||
trackLinks: true,
|
||||
accurateTrackBounce: true,
|
||||
webvisor: true,
|
||||
});
|
||||
|
||||
// Сохраняем ID счетчика для использования в analytics service
|
||||
window.__YM_COUNTER_ID__ = Number(yandexMetrikaId);
|
||||
|
||||
console.log('✅ [Metrics] Yandex Metrika initialized:', {
|
||||
counterId: yandexMetrikaId,
|
||||
method: 'official',
|
||||
ready: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Metrics] Failed to initialize Yandex Metrika:', error);
|
||||
}
|
||||
|
||||
@ -45,8 +45,6 @@ export function UnleashProvider({
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
console.log("[UnleashProvider] Initializing with sessionId:", sessionId || userId || "anonymous");
|
||||
|
||||
return (
|
||||
<FlagProvider config={config}>
|
||||
{children}
|
||||
|
||||
@ -30,52 +30,81 @@ export function useUnleashAnalytics() {
|
||||
const unleashClient = useUnleashClient();
|
||||
|
||||
useEffect(() => {
|
||||
// Подписываемся на все impression события от Unleash
|
||||
console.log("🎯 [Unleash Analytics] Impression listener initialized");
|
||||
|
||||
// Подписываемся на все impression события от Unleash (идентично aura-webapp)
|
||||
unleashClient.on("impression", (impressionEvent: UnleashImpressionEvent) => {
|
||||
console.log("📊 [Unleash Analytics] Impression event received:", {
|
||||
feature: impressionEvent.featureName,
|
||||
variant: impressionEvent.variant,
|
||||
enabled: impressionEvent.enabled,
|
||||
});
|
||||
|
||||
// Проверяем что флаг включен (идентично aura-webapp)
|
||||
if ("enabled" in impressionEvent && impressionEvent.enabled) {
|
||||
const isDevelopEnvironment = typeof window !== "undefined" &&
|
||||
window.location.hostname.includes('develop.funnel.witlab.us') ||
|
||||
window.location.hostname.includes('localhost');
|
||||
(window.location.hostname.includes('develop.funnel.witlab.us') ||
|
||||
window.location.hostname.includes('localhost'));
|
||||
|
||||
// ✅ 1. Отправляем в Google Analytics
|
||||
if (typeof window !== "undefined" && window.gtag) {
|
||||
const gaAvailable = typeof window !== "undefined" && typeof window.gtag !== "undefined";
|
||||
if (gaAvailable) {
|
||||
window.gtag("event", "experiment_impression", {
|
||||
app_name: impressionEvent.context.appName || "witlab-funnel",
|
||||
feature: impressionEvent.featureName,
|
||||
treatment: impressionEvent.variant,
|
||||
debug_mode: isDevelopEnvironment,
|
||||
});
|
||||
console.log("✅ [Google Analytics] AB test event sent:", {
|
||||
event: "experiment_impression",
|
||||
feature: impressionEvent.featureName,
|
||||
treatment: impressionEvent.variant,
|
||||
debug_mode: isDevelopEnvironment,
|
||||
});
|
||||
} else {
|
||||
console.warn("⚠️ [Google Analytics] Not available - gtag function not found");
|
||||
}
|
||||
|
||||
// ✅ 2. Отправляем в Яндекс Метрику через params (параметры визита)
|
||||
if (typeof window !== "undefined" && typeof window.ym === "function") {
|
||||
const counterId = window.__YM_COUNTER_ID__;
|
||||
if (counterId) {
|
||||
// Отправляем параметры визита для AB теста
|
||||
window.ym(counterId, 'params', {
|
||||
[`ab_test_${impressionEvent.featureName}`]: impressionEvent.variant,
|
||||
ab_test_app: impressionEvent.context.appName || "witlab-funnel",
|
||||
});
|
||||
}
|
||||
const ymAvailable = typeof window !== "undefined" && typeof window.ym !== "undefined";
|
||||
const counterId = typeof window !== "undefined" ? window.__YM_COUNTER_ID__ : undefined;
|
||||
|
||||
if (ymAvailable && counterId) {
|
||||
// Отправляем параметры визита для AB теста
|
||||
// ym() накопит вызов в очереди если скрипт еще не загрузился
|
||||
window.ym(counterId, 'params', {
|
||||
[`ab_test_${impressionEvent.featureName}`]: impressionEvent.variant,
|
||||
ab_test_app: impressionEvent.context.appName || "witlab-funnel",
|
||||
});
|
||||
console.log("✅ [Yandex Metrika] AB test params sent:", {
|
||||
counterId,
|
||||
param: `ab_test_${impressionEvent.featureName}`,
|
||||
value: impressionEvent.variant,
|
||||
queued: !!window.ym.a, // true если событие в очереди, false если отправлено
|
||||
});
|
||||
} else if (!ymAvailable) {
|
||||
console.warn("⚠️ [Yandex Metrika] Not initialized - check MetricsProvider");
|
||||
} else if (!counterId) {
|
||||
console.warn("⚠️ [Yandex Metrika] Counter ID not set");
|
||||
}
|
||||
|
||||
// Логирование для debug
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log(`[Analytics] AB Test Impression:`, {
|
||||
feature: impressionEvent.featureName,
|
||||
variant: impressionEvent.variant,
|
||||
appName: impressionEvent.context.appName,
|
||||
debugMode: isDevelopEnvironment,
|
||||
sentToGA: typeof window.gtag !== 'undefined',
|
||||
sentToYM: typeof window.ym === 'function' && !!window.__YM_COUNTER_ID__,
|
||||
});
|
||||
}
|
||||
// Summary log
|
||||
console.log("📈 [Unleash Analytics] AB Test Impression Summary:", {
|
||||
feature: impressionEvent.featureName,
|
||||
variant: impressionEvent.variant,
|
||||
appName: impressionEvent.context.appName,
|
||||
debugMode: isDevelopEnvironment,
|
||||
sentToGA: gaAvailable,
|
||||
sentToYM: ymAvailable && !!window.__YM_COUNTER_ID__,
|
||||
});
|
||||
} else {
|
||||
console.log("⏭️ [Unleash Analytics] Impression event skipped - flag not enabled");
|
||||
}
|
||||
});
|
||||
|
||||
// Отписываемся при unmount (идентично aura-webapp)
|
||||
return () => {
|
||||
console.log("🔌 [Unleash Analytics] Impression listener removed");
|
||||
unleashClient.off("impression");
|
||||
};
|
||||
}, [unleashClient]);
|
||||
|
||||
@ -8,7 +8,11 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
// Yandex Metrika
|
||||
ym: (counterId: number | string, method: string, ...args: any[]) => void;
|
||||
ym: {
|
||||
(counterId: number | string, method: string, ...args: any[]): void;
|
||||
a?: any[]; // Queue for calls before script loads
|
||||
l?: number; // Timestamp
|
||||
};
|
||||
__YM_COUNTER_ID__?: number | string;
|
||||
|
||||
// Google Analytics (GA4)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user