diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..ab616a9
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,2 @@
+@wit-lab-llc:registry=https://npm.pkg.github.com
+//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
diff --git a/next.config.ts b/next.config.ts
index dbc5789..1f71cf3 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,6 +1,8 @@
import type { NextConfig } from "next";
import { BAKED_FUNNELS } from "./src/lib/funnel/bakedFunnels";
+const repoRoot = new URL("../", import.meta.url).pathname.replace(/\/$/, "");
+
const buildVariant =
process.env.FUNNEL_BUILD_VARIANT ??
process.env.NEXT_PUBLIC_FUNNEL_BUILD_VARIANT ??
@@ -48,6 +50,12 @@ const nextConfig: NextConfig = {
DEV_LOGGER_SERVER_ENABLED: process.env.DEV_LOGGER_SERVER_ENABLED,
NEXT_PUBLIC_AUTH_REDIRECT_URL: process.env.NEXT_PUBLIC_AUTH_REDIRECT_URL,
},
+
+ transpilePackages: ["@wit-lab-llc/frontend-shared"],
+
+ turbopack: {
+ root: repoRoot,
+ },
async redirects() {
return generateFunnelRedirects();
diff --git a/package-lock.json b/package-lock.json
index ccee56a..b0ce4c5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,13 +20,14 @@
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@unleash/proxy-client-react": "^5.0.1",
+ "@wit-lab-llc/frontend-shared": "^1.0.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.2",
"idb": "^8.0.3",
"lucide-react": "^0.544.0",
"mongoose": "^8.18.2",
- "next": "15.5.7",
+ "next": "^15.5.9",
"react": "19.1.0",
"react-circular-progressbar": "^2.2.0",
"react-dom": "19.1.0",
@@ -1024,6 +1025,12 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@fingerprintjs/fingerprintjs": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-5.0.1.tgz",
+ "integrity": "sha512-KbaeE/rk2WL8MfpRP6jTI4lSr42SJPjvkyrjP3QU6uUDkOMWWYC2Ts1sNSYcegHC8avzOoYTHBj+2fTqvZWQBA==",
+ "license": "MIT"
+ },
"node_modules/@floating-ui/core": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
@@ -1734,9 +1741,9 @@
"license": "MIT"
},
"node_modules/@next/env": {
- "version": "15.5.7",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz",
- "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==",
+ "version": "15.5.9",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz",
+ "integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -4971,6 +4978,18 @@
"@xtuc/long": "4.2.2"
}
},
+ "node_modules/@wit-lab-llc/frontend-shared": {
+ "version": "1.0.4",
+ "resolved": "https://npm.pkg.github.com/download/@wit-lab-llc/frontend-shared/1.0.4/c342b071bc0716511d84bc3f3c9685aa7649ea5e",
+ "integrity": "sha512-9EZEpjWCdz+IP4RsgkVTPiUEKhm5LqnbgD/S/GJ07eG0Y10ulj3qXjDVXc7N9gURyZULshriDt350w6nhN7Z3w==",
+ "license": "UNLICENSED",
+ "dependencies": {
+ "@fingerprintjs/fingerprintjs": "^5.0.1"
+ },
+ "peerDependencies": {
+ "react": ">=18.0.0"
+ }
+ },
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -9046,12 +9065,12 @@
"peer": true
},
"node_modules/next": {
- "version": "15.5.7",
- "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz",
- "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==",
+ "version": "15.5.9",
+ "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz",
+ "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==",
"license": "MIT",
"dependencies": {
- "@next/env": "15.5.7",
+ "@next/env": "15.5.9",
"@swc/helpers": "0.5.15",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
@@ -10605,9 +10624,9 @@
}
},
"node_modules/storybook": {
- "version": "9.1.13",
- "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.13.tgz",
- "integrity": "sha512-G3KZ36EVzXyHds72B/qtWiJnhUpM0xOUeYlDcO9DSHL1bDTv15cW4+upBl+mcBZrDvU838cn7Bv4GpF+O5MCfw==",
+ "version": "9.1.17",
+ "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.17.tgz",
+ "integrity": "sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index baf00fd..7a93331 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,9 @@
"sync:funnels": "node scripts/sync-funnels-from-db.mjs",
"migrate:arrow-hint": "node scripts/migrate-trial-choice-arrow-hint.mjs",
"storybook": "storybook dev -p 6006 --ci",
- "build-storybook": "storybook build"
+ "build-storybook": "storybook build",
+ "link:shared": "npm link @wit-lab-llc/frontend-shared",
+ "unlink:shared": "npm unlink @wit-lab-llc/frontend-shared && npm install"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
@@ -33,13 +35,14 @@
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@unleash/proxy-client-react": "^5.0.1",
+ "@wit-lab-llc/frontend-shared": "^1.0.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.2",
"idb": "^8.0.3",
"lucide-react": "^0.544.0",
"mongoose": "^8.18.2",
- "next": "15.5.7",
+ "next": "^15.5.9",
"react": "19.1.0",
"react-circular-progressbar": "^2.2.0",
"react-dom": "19.1.0",
diff --git a/src/app/[funnelId]/layout.tsx b/src/app/[funnelId]/layout.tsx
index e8ba441..ca69b00 100644
--- a/src/app/[funnelId]/layout.tsx
+++ b/src/app/[funnelId]/layout.tsx
@@ -1,10 +1,12 @@
import type { ReactNode } from "react";
import { notFound } from "next/navigation";
-import { PixelsProvider } from "@/components/providers/PixelsProvider";
+import { AnalyticsProvider } from "@/components/providers/AnalyticsProvider";
+import { WitLibInitializer } from "@/components/providers/WitLibInitializer";
import { UnleashSessionProvider } from "@/lib/funnel/unleash";
import type { FunnelDefinition } from "@/lib/funnel/types";
import { BAKED_FUNNELS } from "@/lib/funnel/bakedFunnels";
import { IS_FULL_SYSTEM_BUILD } from "@/lib/runtime/buildVariant";
+import { fetchPixelsOnServer } from "@/lib/analytics/fetchPixels";
import {
PaymentPlacementProvider,
TrialVariantSelectionProvider,
@@ -72,21 +74,24 @@ export default async function FunnelLayout({
notFound();
}
+ // Prefetch analytics pixels на сервере
+ // Используем funnelId как source для API
+ const analyticsData = await fetchPixelsOnServer({ source: funnelId });
+
return (
-
-
+
-
-
- {children}
-
-
-
-
+
+
+
+ {children}
+
+
+
+
+
);
}
diff --git a/src/components/analytics/AnalyticsScripts.tsx b/src/components/analytics/AnalyticsScripts.tsx
new file mode 100644
index 0000000..0c19fc3
--- /dev/null
+++ b/src/components/analytics/AnalyticsScripts.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import Script from "next/script";
+
+interface AnalyticsData {
+ facebook_pixel?: string[];
+ google_analytics?: string[];
+ yandex_metrica?: string[];
+}
+
+interface AnalyticsScriptsProps {
+ data: AnalyticsData | null;
+ externalId?: string; // visitorId from fingerprint - used for matching across Pixel and CAPI
+}
+
+/**
+ * Компонент для инъекции аналитических скриптов через next/script
+ *
+ * Использует стратегию afterInteractive (default) - скрипты загружаются
+ * после частичной hydration страницы, что оптимально для аналитик.
+ *
+ * Преимущества над document.createElement:
+ * - Интеграция с Next.js lifecycle
+ * - Автоматическая дедупликация скриптов
+ * - Лучшая производительность через оптимизации Next.js
+ */
+export function AnalyticsScripts({ data, externalId }: AnalyticsScriptsProps) {
+ if (!data) return null;
+
+ const facebookPixels = data.facebook_pixel || [];
+ const googleAnalyticsIds = data.google_analytics || [];
+ const yandexMetrikaIds = data.yandex_metrica || [];
+
+ return (
+ <>
+ {/* Facebook Pixel Scripts */}
+ {facebookPixels.map((pixelId) => (
+
+ ))}
+
+ {/* Google Analytics Scripts */}
+ {googleAnalyticsIds.map((measurementId) => (
+
+ ))}
+
+ {/* Yandex Metrika Scripts */}
+ {yandexMetrikaIds.map((counterId) => (
+
+ ))}
+ >
+ );
+}
diff --git a/src/components/analytics/index.ts b/src/components/analytics/index.ts
index 5ec6c27..58919d1 100644
--- a/src/components/analytics/index.ts
+++ b/src/components/analytics/index.ts
@@ -2,3 +2,4 @@ export { FacebookPixels } from "./FacebookPixels";
export { GoogleAnalytics } from "./GoogleAnalytics";
export { YandexMetrika } from "./YandexMetrika";
export { PageViewTracker } from "./PageViewTracker";
+export { AnalyticsScripts } from "./AnalyticsScripts";
diff --git a/src/components/funnel/FunnelRuntime.tsx b/src/components/funnel/FunnelRuntime.tsx
index 46bf8fc..89f0e5f 100644
--- a/src/components/funnel/FunnelRuntime.tsx
+++ b/src/components/funnel/FunnelRuntime.tsx
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useMemo, useCallback } from "react";
-import { useRouter } from "next/navigation";
+import { useRouter, useSearchParams } from "next/navigation";
import { resolveNextScreenId, type UnleashChecker } from "@/lib/funnel/navigation";
import { resolveScreenVariant } from "@/lib/funnel/variants";
@@ -66,7 +66,9 @@ interface FunnelRuntimeProps {
export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
const router = useRouter();
- const { createSession, updateSession } = useSession({
+ const searchParams = useSearchParams();
+ // Session is already created by UnleashSessionProvider, we only need updateSession here
+ const { updateSession } = useSession({
funnelId: funnel.meta.id,
googleAnalyticsId: funnel.meta.googleAnalyticsId,
});
@@ -107,10 +109,6 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
// Флаги Unleash теперь обрабатываются автоматически через useUnleashAnalytics
// Нет необходимости собирать их вручную для отправки impression событий
- useEffect(() => {
- createSession();
- }, [createSession]);
-
useEffect(() => {
registerScreen(currentScreen.id);
}, [currentScreen.id, registerScreen]);
@@ -148,11 +146,24 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
return { current, total };
}, [historyWithCurrent.length, funnel, answers, unleashChecker]);
+ const funnelQueryString = useMemo(() => {
+ return searchParams.toString();
+ }, [searchParams]);
+
+ const buildScreenUrl = useCallback(
+ (screenId: string) => {
+ return funnelQueryString
+ ? `/${funnel.meta.id}/${screenId}?${funnelQueryString}`
+ : `/${funnel.meta.id}/${screenId}`;
+ },
+ [funnel.meta.id, funnelQueryString]
+ );
+
const goToScreen = (screenId: string | undefined) => {
if (!screenId) {
return;
}
- router.push(`/${funnel.meta.id}/${screenId}`);
+ router.push(buildScreenUrl(screenId));
};
const handleContinue = () => {
@@ -319,7 +330,7 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
if (backTarget) {
// Переназначаем назад на конкретный экран без роста истории
- router.replace(`/${funnel.meta.id}/${backTarget}`);
+ router.replace(buildScreenUrl(backTarget));
return;
}
@@ -373,7 +384,7 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
setTimeout(() => {
try {
// Выполняем редирект на целевой экран
- router.replace(`/${funnel.meta.id}/${currentBackTarget}`);
+ router.replace(buildScreenUrl(currentBackTarget));
// Сбрасываем флаг
setTimeout(() => {
@@ -382,7 +393,7 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
} catch (error) {
// Fallback: если router.replace не сработал, используем нативную навигацию
console.error('Router replace failed, using fallback navigation:', error);
- window.location.href = `/${funnel.meta.id}/${currentBackTarget}`;
+ window.location.href = buildScreenUrl(currentBackTarget);
}
}, 10);
};
@@ -391,7 +402,7 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
return () => {
window.removeEventListener("popstate", handlePopState);
};
- }, [currentScreen.id, currentScreen.navigation?.onBackScreenId, funnel.meta.id, router]);
+ }, [buildScreenUrl, currentScreen.id, currentScreen.navigation?.onBackScreenId, router]);
const canGoBack = historyWithCurrent.lastIndexOf(currentScreen.id) > 0;
diff --git a/src/components/providers/AnalyticsProvider.tsx b/src/components/providers/AnalyticsProvider.tsx
new file mode 100644
index 0000000..20c18e7
--- /dev/null
+++ b/src/components/providers/AnalyticsProvider.tsx
@@ -0,0 +1,195 @@
+"use client";
+
+import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
+import {
+ initWitLib,
+ isWitLibInitialized,
+ AnalyticsManager,
+ FingerprintCollector,
+ type AnalyticsState,
+} from "@wit-lab-llc/frontend-shared";
+import { PageViewTracker } from "@/components/analytics";
+import { AnalyticsScripts } from "@/components/analytics/AnalyticsScripts";
+import { getSourceByPathname } from "@/shared/utils/source";
+import type { AnalyticsData } from "@/lib/analytics/fetchPixels";
+
+interface AnalyticsContextValue {
+ manager: AnalyticsManager;
+ state: AnalyticsState;
+ isReady: boolean;
+}
+
+const AnalyticsContext = createContext(null);
+
+// Singleton instances
+let analyticsManagerInstance: AnalyticsManager | null = null;
+let fingerprintCollectorInstance: FingerprintCollector | null = null;
+
+function getAnalyticsManager(): AnalyticsManager {
+ if (!analyticsManagerInstance) {
+ analyticsManagerInstance = new AnalyticsManager();
+ }
+ return analyticsManagerInstance;
+}
+
+function getFingerprintCollector(): FingerprintCollector {
+ if (!fingerprintCollectorInstance) {
+ fingerprintCollectorInstance = new FingerprintCollector();
+ }
+ return fingerprintCollectorInstance;
+}
+
+interface AnalyticsProviderProps {
+ children: ReactNode;
+ /** Prefetched analytics data from server */
+ prefetchedData?: AnalyticsData | null;
+ apiBaseUrl?: string;
+ debug?: boolean;
+}
+
+/**
+ * Analytics Provider Component
+ *
+ * Инициализирует @wit-lab-llc/frontend-shared библиотеку и загружает все аналитики:
+ * - Facebook Pixel
+ * - Google Analytics
+ * - Yandex Metrika
+ *
+ * Flow:
+ * 1. Инициализирует библиотеку с baseUrl
+ * 2. Загружает данные аналитик с бэкенда через AnalyticsManager
+ * 3. Инъецирует скрипты в DOM
+ * 4. Предоставляет контекст для доступа к менеджеру
+ */
+export function AnalyticsProvider({
+ children,
+ prefetchedData,
+ apiBaseUrl,
+ debug = false,
+}: AnalyticsProviderProps) {
+ const [analyticsData, setAnalyticsData] = useState(prefetchedData || null);
+ const [state, setState] = useState({
+ isLoaded: !!prefetchedData,
+ isLoading: false,
+ data: null,
+ error: null,
+ });
+ const [isReady, setIsReady] = useState(!!prefetchedData);
+ const [manager] = useState(() => getAnalyticsManager());
+ const [fingerprintCollector] = useState(() => getFingerprintCollector());
+ const [externalId, setExternalId] = useState(undefined);
+
+ // Collect fingerprint for external_id
+ useEffect(() => {
+ fingerprintCollector.getOrCollect()
+ .then((result) => {
+ if (result?.visitorId) {
+ setExternalId(result.visitorId);
+ if (debug) {
+ console.log("✅ [AnalyticsProvider] Fingerprint collected, external_id:", result.visitorId);
+ }
+ }
+ })
+ .catch((error) => {
+ console.warn("[AnalyticsProvider] Failed to collect fingerprint:", error);
+ });
+ }, [fingerprintCollector, debug]);
+
+ useEffect(() => {
+ // Если есть prefetched данные - используем их, скрипты уже инъецируются через AnalyticsScripts
+ if (prefetchedData) {
+ if (debug) {
+ console.log("✅ [AnalyticsProvider] Using prefetched analytics data:", prefetchedData);
+ }
+ return;
+ }
+
+ // Fallback: загружаем на клиенте если нет prefetched данных
+ const initAndLoad = async () => {
+ try {
+ // Initialize library if not already initialized
+ if (!isWitLibInitialized()) {
+ const baseUrl = apiBaseUrl || process.env.NEXT_PUBLIC_API_URL || "";
+ if (!baseUrl) {
+ console.warn("[AnalyticsProvider] No API base URL configured");
+ setIsReady(true);
+ return;
+ }
+
+ initWitLib({
+ baseUrl,
+ debug,
+ });
+ }
+
+ // Load and inject analytics
+ const source = getSourceByPathname();
+ await manager.loadAndInject({ source });
+
+ setState(manager.getState());
+ setAnalyticsData(manager.getData() as AnalyticsData | null);
+ setIsReady(true);
+
+ if (debug) {
+ console.log("✅ [AnalyticsProvider] Analytics loaded via client:", {
+ facebook: manager.getFacebookPixels(),
+ google: manager.getGoogleAnalyticsIds(),
+ yandex: manager.getYandexMetrikaIds(),
+ });
+ }
+ } catch (error) {
+ console.warn("[AnalyticsProvider] Failed to load analytics:", error);
+ setIsReady(true);
+ }
+ };
+
+ initAndLoad();
+ }, [manager, apiBaseUrl, debug, prefetchedData]);
+
+ return (
+
+ {/* next/script для инъекции скриптов с оптимизациями Next.js */}
+
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to access AnalyticsManager
+ */
+export function useAnalytics(): AnalyticsContextValue {
+ const context = useContext(AnalyticsContext);
+ if (!context) {
+ throw new Error("useAnalytics must be used within AnalyticsProvider");
+ }
+ return context;
+}
+
+/**
+ * Hook to get Facebook Pixel IDs
+ */
+export function useFacebookPixels(): string[] {
+ const { manager, isReady } = useAnalytics();
+ if (!isReady) return [];
+ return manager.getFacebookPixels();
+}
+
+/**
+ * Hook to get Google Analytics IDs
+ */
+export function useGoogleAnalyticsIds(): string[] {
+ const { manager, isReady } = useAnalytics();
+ if (!isReady) return [];
+ return manager.getGoogleAnalyticsIds();
+}
+
+/**
+ * Hook to get Yandex Metrika IDs
+ */
+export function useYandexMetrikaIds(): string[] {
+ const { manager, isReady } = useAnalytics();
+ if (!isReady) return [];
+ return manager.getYandexMetrikaIds();
+}
diff --git a/src/components/providers/MetricsProvider.tsx b/src/components/providers/MetricsProvider.tsx
deleted file mode 100644
index 34aa777..0000000
--- a/src/components/providers/MetricsProvider.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-"use client";
-
-import { useEffect, type ReactNode } from "react";
-import ReactGA from "react-ga4";
-
-interface MetricsProviderProps {
- children: ReactNode;
- googleAnalyticsId?: string;
- yandexMetrikaId?: string;
- facebookPixels?: string[];
-}
-
-/**
- * Metrics Provider Component
- *
- * Инициализирует все метрики СИНХРОННО при загрузке приложения
- * по аналогии с aura-webapp для гарантии создания куки ДО авторизации.
- *
- * Метрики:
- * - Google Analytics (через react-ga4) - синхронная инициализация
- * - Yandex Metrika - синхронная загрузка скрипта
- * - Facebook Pixel - синхронная загрузка скрипта
- *
- * Это гарантирует что куки (_ga, _gid, _fbp, _fbc, _ym_uid) создаются
- * ДО того как пользователь достигнет экрана авторизации.
- */
-export function MetricsProvider({
- children,
- googleAnalyticsId,
- yandexMetrikaId,
- facebookPixels = []
-}: MetricsProviderProps) {
-
- // Инициализация Google Analytics (синхронно через react-ga4)
- useEffect(() => {
- if (!googleAnalyticsId) return;
-
- try {
- // Включаем debug mode для develop окружения
- const isDevelopEnvironment = typeof window !== 'undefined' &&
- window.location.hostname.includes('develop.funnel.witlab.us') ||
- window.location.hostname.includes('localhost');
-
- ReactGA.initialize(googleAnalyticsId, {
- gaOptions: {
- debug_mode: isDevelopEnvironment,
- },
- });
-
- 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 (официальный способ)
- useEffect(() => {
- if (!yandexMetrikaId) return;
-
- try {
- // Проверяем что скрипт еще не загружен
- if (typeof window.ym === 'function' && window.__YM_COUNTER_ID__) {
- console.log('[Metrics] Yandex Metrika already initialized');
- return;
- }
-
- // ✅ Официальный код инициализации Яндекс Метрики
- // Создает функцию-заглушку 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);
-
- // ✅ Вызываем 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);
- }
- }, [yandexMetrikaId]);
-
- // Инициализация Facebook Pixel (синхронно через скрипт)
- useEffect(() => {
- if (!facebookPixels || facebookPixels.length === 0) return;
-
- try {
- // Проверяем что fbq еще не загружен
- if (typeof window.fbq === 'function') {
- console.log('[Metrics] Facebook Pixel already loaded');
- return;
- }
-
- // Базовый код Facebook Pixel
- const fbqScript = `
- !function(f,b,e,v,n,t,s)
- {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
- n.callMethod.apply(n,arguments):n.queue.push(arguments)};
- if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
- n.queue=[];t=b.createElement(e);t.async=!0;
- t.src=v;s=b.getElementsByTagName(e)[0];
- s.parentNode.insertBefore(t,s)}(window, document,'script',
- 'https://connect.facebook.net/en_US/fbevents.js');
- `;
-
- // Выполняем базовый код
- const script = document.createElement('script');
- script.type = 'text/javascript';
- script.innerHTML = fbqScript;
- document.head.appendChild(script);
-
- // Инициализируем каждый пиксель
- if (typeof window.fbq === 'function') {
- const fbq = window.fbq as ((...args: unknown[]) => void);
- facebookPixels.forEach((pixelId) => {
- fbq('init', pixelId);
- console.log('[Metrics] Facebook Pixel initialized:', pixelId);
- });
-
- // Отправляем PageView для всех пикселей
- fbq('track', 'PageView');
- }
- } catch (error) {
- console.error('[Metrics] Failed to initialize Facebook Pixel:', error);
- }
- }, [facebookPixels]);
-
- return <>{children}>;
-}
diff --git a/src/components/providers/PixelsProvider.tsx b/src/components/providers/PixelsProvider.tsx
deleted file mode 100644
index a9bc824..0000000
--- a/src/components/providers/PixelsProvider.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-"use client";
-
-import { useEffect, useState, type ReactNode } from "react";
-import { PageViewTracker } from "@/components/analytics";
-import { getPixels } from "@/entities/session/actions";
-import { getSourceByPathname } from "@/shared/utils/source";
-import { MetricsProvider } from "./MetricsProvider";
-
-interface PixelsProviderProps {
- children: ReactNode;
- googleAnalyticsId?: string;
- yandexMetrikaId?: string;
-}
-
-/**
- * Pixels Provider Component
- *
- * Инициализирует все метрики синхронно через MetricsProvider
- * по аналогии с aura-webapp для гарантии создания куки ДО авторизации.
- *
- * IMPORTANT: This component should be placed in a layout (not in components that re-render on navigation)
- * to avoid duplicate API requests. Currently used in app/[funnelId]/layout.tsx
- *
- * Facebook Pixels are loaded from backend API (cached in localStorage).
- * Google Analytics and Yandex Metrika IDs come from funnel configuration.
- *
- * Flow:
- * 1. Check localStorage for cached FB pixels
- * 2. If not cached, request from backend (errors are handled gracefully)
- * 3. Save to localStorage if pixels received
- * 4. Pass all IDs to MetricsProvider for synchronous initialization
- */
-export function PixelsProvider({ children, googleAnalyticsId, yandexMetrikaId }: PixelsProviderProps) {
- const [pixels, setPixels] = useState([]);
-
- useEffect(() => {
- const loadPixels = async () => {
- try {
- // Check localStorage first
- const cachedPixels = localStorage.getItem("fb_pixels");
- if (cachedPixels) {
- const parsed = JSON.parse(cachedPixels);
- setPixels(parsed);
- return;
- }
-
- // Load from backend
- const locale = "en"; // TODO: Get from context or config
- const source = getSourceByPathname();
- const domain = window.location.hostname;
-
- const response = await getPixels({
- domain,
- source,
- locale,
- });
-
- const pixelIds = response?.data?.fb || [];
-
- // Save to localStorage only if we got pixels
- if (pixelIds.length > 0) {
- localStorage.setItem("fb_pixels", JSON.stringify(pixelIds));
- }
-
- setPixels(pixelIds);
- } catch (error) {
- // Silently handle errors - pixels are optional
- console.warn("Facebook pixels not available:", error instanceof Error ? error.message : error);
- setPixels([]);
- }
- };
-
- loadPixels();
- }, []);
-
- return (
-
-
- {children}
-
- );
-}
diff --git a/src/components/providers/WitLibInitializer.tsx b/src/components/providers/WitLibInitializer.tsx
new file mode 100644
index 0000000..dd13c3e
--- /dev/null
+++ b/src/components/providers/WitLibInitializer.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import { initWitLib, isWitLibInitialized } from "@wit-lab-llc/frontend-shared";
+
+interface WitLibInitializerProps {
+ children: React.ReactNode;
+ apiBaseUrl?: string;
+ debug?: boolean;
+}
+
+export function WitLibInitializer({
+ children,
+ apiBaseUrl,
+ debug = false,
+}: WitLibInitializerProps) {
+ // Initialize on first render (before useEffect)
+ if (typeof window !== "undefined" && !isWitLibInitialized()) {
+ const baseUrl = apiBaseUrl || process.env.NEXT_PUBLIC_API_URL || "";
+ if (baseUrl) {
+ initWitLib({
+ baseUrl,
+ debug,
+ });
+ if (debug) {
+ console.log("✅ [WitLibInitializer] Library initialized with baseUrl:", baseUrl);
+ }
+ }
+ }
+
+ return <>{children}>;
+}
diff --git a/src/entities/session/actions.ts b/src/entities/session/actions.ts
deleted file mode 100644
index 16f01ab..0000000
--- a/src/entities/session/actions.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { http } from "@/shared/api/httpClient";
-import {
- CreateSessionResponseSchema,
- ICreateSessionRequest,
- ICreateSessionResponse,
- IUpdateSessionRequest,
- IUpdateSessionResponse,
- UpdateSessionResponseSchema,
- IGetPixelsRequest,
- IGetPixelsResponse,
- GetPixelsResponseSchema,
-} from "./types";
-import { API_ROUTES } from "@/shared/constants/api-routes";
-
-export const createSession = async (
- payload: ICreateSessionRequest
-): Promise => {
- return http.post(API_ROUTES.session(), payload, {
- tags: ["session", "create"],
- schema: CreateSessionResponseSchema,
- revalidate: 0,
- });
-};
-
-export const updateSession = async (
- payload: IUpdateSessionRequest
-): Promise => {
- // Отправляем только data без вложенности
- return http.patch(
- API_ROUTES.session(payload.sessionId),
- payload.data,
- {
- tags: ["session", "update"],
- schema: UpdateSessionResponseSchema,
- revalidate: 0,
- }
- );
-};
-
-export const getPixels = async (
- payload: IGetPixelsRequest
-): Promise => {
- return http.get(API_ROUTES.sessionPixels(), {
- tags: ["session", "pixels"],
- schema: GetPixelsResponseSchema,
- revalidate: 3600, // Cache for 1 hour
- query: payload,
- });
-};
diff --git a/src/entities/session/types.ts b/src/entities/session/types.ts
index 5ff76ff..6cbb1fd 100644
--- a/src/entities/session/types.ts
+++ b/src/entities/session/types.ts
@@ -44,7 +44,9 @@ export const GetPixelsRequestSchema = z.object({
export const GetPixelsResponseSchema = z.object({
status: z.string(),
data: z.object({
- fb: z.array(z.string()).optional(),
+ facebook_pixel: z.array(z.string()).optional(),
+ google_analytics: z.array(z.string()).optional(),
+ yandex_metrica: z.array(z.string()).optional(),
}),
});
diff --git a/src/hooks/session/useSession.ts b/src/hooks/session/useSession.ts
index 6449e30..151737b 100644
--- a/src/hooks/session/useSession.ts
+++ b/src/hooks/session/useSession.ts
@@ -1,140 +1,269 @@
-import {
- ICreateSessionResponse,
- IUpdateSessionRequest,
-} from "@/entities/session/types";
-import {
- createSession as createSessionApi,
- updateSession as updateSessionApi,
-} from "@/entities/session/actions";
-import { getClientTimezone } from "@/shared/utils/locales";
-import { parseQueryParams } from "@/shared/utils/url";
-import { useCallback, useMemo, useState } from "react";
+import { IUpdateSessionRequest } from "@/entities/session/types";
+import { useCallback, useMemo, useState, useEffect } from "react";
import { setSessionIdToCookie } from "@/entities/session/serverActions";
import { metricService } from "@/services/analytics/metricService";
+import {
+ SessionCollector,
+ FingerprintCollector,
+ FacebookCollector,
+ parseUtmParams,
+ getOrCreateAnonymousId,
+ type CreateSessionResponse,
+ type UpdateSessionData,
+ type SessionFingerprintData,
+ type SessionFacebookData,
+} from "@wit-lab-llc/frontend-shared";
+import { parseQueryParams } from "@/shared/utils/url";
-// TODO
+// TODO: Get locale from context or i18n
const locale = "en";
+// Debug logging helper - only logs in development
+const isDev = process.env.NODE_ENV === "development";
+const debugLog = (message: string, ...args: unknown[]) => {
+ if (isDev) {
+ console.log(`🔍 [useSession] ${message}`, ...args);
+ }
+};
+
+// Singleton instances
+let sessionCollectorInstance: SessionCollector | null = null;
+let fingerprintCollectorInstance: FingerprintCollector | null = null;
+let facebookCollectorInstance: FacebookCollector | null = null;
+
+// Module-level flag to track if data collection has been done (persists across remounts)
+let dataCollectionDone = false;
+
+function getSessionCollector(): SessionCollector {
+ if (!sessionCollectorInstance) {
+ sessionCollectorInstance = new SessionCollector();
+ }
+ return sessionCollectorInstance;
+}
+
+function getFingerprintCollector(): FingerprintCollector {
+ if (!fingerprintCollectorInstance) {
+ fingerprintCollectorInstance = new FingerprintCollector();
+ }
+ return fingerprintCollectorInstance;
+}
+
+function getFacebookCollector(): FacebookCollector {
+ if (!facebookCollectorInstance) {
+ facebookCollectorInstance = new FacebookCollector();
+ }
+ return facebookCollectorInstance;
+}
+
interface IUseSessionProps {
funnelId: string;
googleAnalyticsId?: string;
}
export const useSession = ({ funnelId, googleAnalyticsId }: IUseSessionProps) => {
- const localStorageKey = `${funnelId}_sessionId`;
- const sessionId =
- typeof window === "undefined" ? "" : localStorage.getItem(localStorageKey);
-
- const timezone = getClientTimezone();
-
+ const [collector] = useState(() => getSessionCollector());
+ const [fingerprintCollector] = useState(() => getFingerprintCollector());
+ const [facebookCollector] = useState(() => getFacebookCollector());
const [isError, setIsError] = useState(false);
- const setSessionId = useCallback(
- async (sessionId: string) => {
- localStorage.setItem(localStorageKey, sessionId);
- localStorage.setItem("activeSessionId", sessionId);
- await setSessionIdToCookie("activeSessionId", sessionId);
- },
- [localStorageKey]
- );
+ // Get current session ID from collector
+ const sessionId = collector.getSessionId();
- const createSession =
- useCallback(async (): Promise => {
- if (typeof window === "undefined") {
- return {
- sessionId: "",
- status: "error",
- };
- }
- if (sessionId?.length) {
- setSessionId(sessionId);
- return {
- sessionId,
- status: "old",
- };
- }
+ // Collect fingerprint and facebook data on mount (only once per app lifecycle)
+ useEffect(() => {
+ if (typeof window === "undefined" || dataCollectionDone) return;
+ dataCollectionDone = true;
+
+ // Collect data in background
+ debugLog("Starting fingerprint and Facebook data collection...");
+ Promise.all([
+ fingerprintCollector.collect().catch((e) => {
+ debugLog("Fingerprint collection failed:", e);
+ return null;
+ }),
+ Promise.resolve(facebookCollector.collect()),
+ ]).then(([fpResult, fbResult]) => {
+ debugLog("✅ Data collection completed", {
+ fingerprint: fpResult ? { visitorId: fpResult.visitorId, confidence: fpResult.confidence } : null,
+ facebook: fbResult ? { fbp: fbResult.fbp, fbc: fbResult.fbc, fbclid: fbResult.fbclid } : null,
+ });
+ });
+ }, [fingerprintCollector, facebookCollector]);
+
+ const createSession = useCallback(async (): Promise => {
+ if (typeof window === "undefined") {
+ return { sessionId: "", status: "error" };
+ }
+
+ try {
+ const query = parseQueryParams();
+ const utm = parseUtmParams();
+ const anonymousId = getOrCreateAnonymousId();
+ const lastActivityAt = new Date().toISOString();
+
+ // Get fingerprint data for session
+ let fingerprint: SessionFingerprintData | undefined;
try {
- const utm = parseQueryParams();
- const sessionParams = {
- feature: "stripe",
- locale,
- timezone,
- source: funnelId,
- sign: false,
- utm,
- domain: window.location.hostname,
- };
- console.log("Creating session with parameters:", sessionParams);
- const sessionFromServer = await createSessionApi(sessionParams);
- console.log("Session creation response:", sessionFromServer);
- if (
- sessionFromServer?.sessionId?.length &&
- sessionFromServer?.status === "success"
- ) {
- await setSessionId(sessionFromServer.sessionId);
-
- // ✅ Отправляем sessionId в userParams (параметры посетителя)
- metricService.userParams({
- sessionId: sessionFromServer.sessionId,
+ debugLog("Getting fingerprint data for session...");
+ const fpData = await fingerprintCollector.getOrCollect();
+ if (fpData) {
+ const payload = fingerprintCollector.toServerPayload();
+ fingerprint = {
+ visitorId: fpData.visitorId,
+ confidence: fpData.confidence,
+ collectedAt: fpData.collectedAt,
+ ...payload,
+ } as SessionFingerprintData;
+ debugLog("✅ Fingerprint data ready", {
+ visitorId: fingerprint.visitorId,
+ confidence: fingerprint.confidence,
});
-
- // ✅ Отправляем контекст визита в params (параметры визита)
+ }
+ } catch (e) {
+ console.warn("[useSession] Failed to collect fingerprint:", e);
+ }
+
+ // Get facebook data for session
+ let facebookData: SessionFacebookData | undefined;
+ try {
+ debugLog("Getting Facebook data for session...");
+ const fbData = facebookCollector.getData() || facebookCollector.collect();
+ if (fbData) {
+ facebookData = {
+ fbp: fbData.fbp ?? undefined,
+ fbc: fbData.fbc ?? undefined,
+ fbclid: fbData.fbclid ?? undefined,
+ externalId: fingerprint?.visitorId, // Use visitorId as external_id
+ landingPage: fbData.landingPage ?? undefined,
+ referrer: fbData.referrer,
+ clientUserAgent: fbData.userAgent,
+ eventSourceUrl: fbData.currentUrl,
+ browserLanguage: fbData.browserLanguage,
+ screenResolution: fbData.screenResolution,
+ viewportSize: fbData.viewportSize,
+ colorDepth: fbData.colorDepth,
+ devicePixelRatio: fbData.devicePixelRatio,
+ touchSupport: fbData.touchSupport,
+ cookiesEnabled: fbData.cookiesEnabled,
+ doNotTrack: fbData.doNotTrack,
+ collectedAt: fbData.collectedAt,
+ };
+ debugLog("✅ Facebook data ready", {
+ fbp: facebookData.fbp,
+ fbc: facebookData.fbc,
+ fbclid: facebookData.fbclid,
+ externalId: facebookData.externalId,
+ landingPage: facebookData.landingPage,
+ });
+ }
+ } catch (e) {
+ console.warn("[useSession] Failed to collect Facebook data:", e);
+ }
+
+ debugLog("Creating session with data:", {
+ source: funnelId,
+ hasFingerprint: !!fingerprint,
+ hasFacebookData: !!facebookData,
+ utm,
+ });
+
+ const response = await collector.create({
+ source: funnelId,
+ feature: "stripe",
+ locale,
+ sign: false,
+ utm,
+ anonymousId,
+ query,
+ landingQuery: query,
+ lastActivityAt,
+ fingerprint,
+ facebookData,
+ });
+
+ debugLog("Session creation response:", response);
+
+ if (response.sessionId && response.status !== "error") {
+ // Set cookie for server-side access
+ await setSessionIdToCookie("activeSessionId", response.sessionId);
+
+ // For old sessions, update with current feature and data
+ if (response.status === "old") {
+ debugLog("Updating old session with current feature and data...");
+ await collector.update({
+ feature: "stripe",
+ anonymousId,
+ query,
+ lastActivityAt: new Date().toISOString(),
+ });
+ }
+
+ // Send analytics only for new sessions
+ if (response.status === "success") {
+ metricService.userParams({
+ sessionId: response.sessionId,
+ });
+
metricService.sendVisitContext({
- sessionId: sessionFromServer.sessionId,
+ sessionId: response.sessionId,
funnelId,
gaId: googleAnalyticsId,
});
-
- return sessionFromServer;
}
- console.error(
- "Session creation failed - invalid response:",
- sessionFromServer
- );
- setIsError(true);
- return {
- status: "error",
- sessionId: "",
- };
- } catch (error) {
- console.error("Session creation failed with error:", error);
- setIsError(true);
- return {
- status: "error",
- sessionId: "",
- };
+
+ return response;
}
- }, [sessionId, timezone, setSessionId, funnelId, googleAnalyticsId]);
+
+ setIsError(true);
+ return { status: "error", sessionId: "" };
+ } catch (error) {
+ debugLog("❌ Session creation failed:", error);
+ console.error("Session creation failed:", error);
+ setIsError(true);
+ return { status: "error", sessionId: "" };
+ }
+ }, [collector, funnelId, googleAnalyticsId, fingerprintCollector, facebookCollector]);
const updateSession = useCallback(
async (data: IUpdateSessionRequest["data"]) => {
try {
- let _sessionId = sessionId;
- if (!_sessionId) {
- const session = await createSession();
- _sessionId = session.sessionId;
- }
- const result = await updateSessionApi({
- sessionId: _sessionId,
- data: {
- feature: "stripe",
- ...data,
- },
+ const query = parseQueryParams();
+ const anonymousId = getOrCreateAnonymousId();
+
+ // Convert to library's UpdateSessionData format
+ const updateData: UpdateSessionData = {
+ feature: "stripe",
+ anonymousId,
+ query,
+ lastActivityAt: new Date().toISOString(),
+ ...data,
+ };
+
+ const result = await collector.update(updateData, {
+ source: funnelId,
+ feature: "stripe",
+ locale,
+ sign: false,
+ utm: parseUtmParams(),
+ anonymousId,
+ query,
+ landingQuery: query,
+ lastActivityAt: new Date().toISOString(),
});
+
return result;
} catch (error) {
- console.log(error);
+ debugLog("❌ Session update failed:", error);
+ console.error("Session update failed:", error);
+ return null;
}
},
- [sessionId, createSession]
+ [collector, funnelId]
);
- const deleteSession = useCallback(async () => {
- if (typeof window === "undefined") {
- return;
- }
- localStorage.removeItem(localStorageKey);
- }, [localStorageKey]);
+ const deleteSession = useCallback(() => {
+ collector.delete(funnelId);
+ }, [collector, funnelId]);
return useMemo(
() => ({
diff --git a/src/lib/analytics/fetchPixels.ts b/src/lib/analytics/fetchPixels.ts
new file mode 100644
index 0000000..a1c1b67
--- /dev/null
+++ b/src/lib/analytics/fetchPixels.ts
@@ -0,0 +1,74 @@
+import { headers } from "next/headers";
+
+export interface AnalyticsData {
+ facebook_pixel?: string[];
+ google_analytics?: string[];
+ yandex_metrica?: string[];
+}
+
+interface FetchPixelsParams {
+ source: string;
+ locale?: string;
+}
+
+/**
+ * Server-side функция для получения аналитических пикселей
+ *
+ * Вызывается в Server Component (layout.tsx) для prefetch данных
+ * до отправки HTML клиенту.
+ *
+ * @param params.source - ID воронки (funnelId)
+ * @param params.locale - Локаль (опционально, определяется из headers)
+ */
+export async function fetchPixelsOnServer(
+ params: FetchPixelsParams
+): Promise {
+ try {
+ const headersList = await headers();
+
+ // Получаем domain из headers
+ const host = headersList.get("host") || headersList.get("x-forwarded-host");
+ const domain = host?.split(":")[0] || "localhost";
+
+ // Получаем locale из headers или используем default
+ const acceptLanguage = headersList.get("accept-language");
+ const locale = params.locale || acceptLanguage?.split(",")[0]?.split("-")[0] || "en";
+
+ const apiUrl = process.env.NEXT_PUBLIC_API_URL;
+ if (!apiUrl) {
+ console.warn("[fetchPixelsOnServer] NEXT_PUBLIC_API_URL not configured");
+ return null;
+ }
+
+ const queryParams = new URLSearchParams({
+ domain,
+ source: params.source,
+ locale,
+ });
+
+ const response = await fetch(`${apiUrl}/v2/session/pixels?${queryParams}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ // Кэшируем на 5 минут для одинаковых параметров
+ next: { revalidate: 300 },
+ });
+
+ if (!response.ok) {
+ console.warn(`[fetchPixelsOnServer] API returned ${response.status}`);
+ return null;
+ }
+
+ const json = await response.json();
+
+ if (json.status === "success" && json.data) {
+ return json.data as AnalyticsData;
+ }
+
+ return null;
+ } catch (error) {
+ console.warn("[fetchPixelsOnServer] Failed to fetch pixels:", error);
+ return null;
+ }
+}
diff --git a/src/lib/funnel/unleash/UnleashSessionProvider.tsx b/src/lib/funnel/unleash/UnleashSessionProvider.tsx
index 0fb5f32..01affe3 100644
--- a/src/lib/funnel/unleash/UnleashSessionProvider.tsx
+++ b/src/lib/funnel/unleash/UnleashSessionProvider.tsx
@@ -1,6 +1,6 @@
"use client";
-import { type ReactNode, useEffect, useState } from "react";
+import { type ReactNode, useEffect, useState, useRef } from "react";
import { UnleashProvider } from "./UnleashProvider";
import { useSession } from "@/hooks/session/useSession";
@@ -21,28 +21,26 @@ export function UnleashSessionProvider({
const [sessionId, setSessionId] = useState(null);
const [isReady, setIsReady] = useState(false);
const { createSession } = useSession({ funnelId, googleAnalyticsId });
+ const initCalledRef = useRef(false);
useEffect(() => {
+ // Prevent multiple initializations
+ if (initCalledRef.current) return;
+ initCalledRef.current = true;
+
const initSession = async () => {
- const localStorageKey = `${funnelId}_sessionId`;
- let sid = localStorage.getItem(localStorageKey);
-
- if (!sid) {
- console.log(`[UnleashSessionProvider] Creating new session...`);
- const result = await createSession();
- sid = result.sessionId;
+ console.log(`[UnleashSessionProvider] Initializing session...`);
+ const result = await createSession();
+ if (result.sessionId) {
+ console.log(`[UnleashSessionProvider] Using sessionId: ${result.sessionId}`);
+ setSessionId(result.sessionId);
}
-
- if (sid) {
- console.log(`[UnleashSessionProvider] Using sessionId: ${sid}`);
- setSessionId(sid);
- }
-
setIsReady(true);
};
initSession();
- }, [funnelId, createSession]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [funnelId]);
// Показываем loading пока создается сессия
if (!isReady || !sessionId) {
diff --git a/src/lib/funnel/unleash/useUnleashAnalytics.ts b/src/lib/funnel/unleash/useUnleashAnalytics.ts
index 9ccb67a..dff92d3 100644
--- a/src/lib/funnel/unleash/useUnleashAnalytics.ts
+++ b/src/lib/funnel/unleash/useUnleashAnalytics.ts
@@ -84,7 +84,7 @@ export function useUnleashAnalytics() {
queued: !!window.ym.a, // true если событие в очереди, false если отправлено
});
} else if (!ymAvailable) {
- console.warn("⚠️ [Yandex Metrika] Not initialized - check MetricsProvider");
+ console.warn("⚠️ [Yandex Metrika] Not initialized - check AnalyticsProvider");
} else if (!counterId) {
console.warn("⚠️ [Yandex Metrika] Counter ID not set");
}