From 793d12cae9cfa6144fa9db3cb4a5f879571e9e3d Mon Sep 17 00:00:00 2001 From: "dev.daminik00" Date: Mon, 27 Oct 2025 21:58:00 +0100 Subject: [PATCH] add new scripts logic --- package-lock.json | 7 + package.json | 1 + src/components/providers/MetricsProvider.tsx | 132 +++++++++++++++++++ src/components/providers/PixelsProvider.tsx | 23 ++-- 4 files changed, 151 insertions(+), 12 deletions(-) create mode 100644 src/components/providers/MetricsProvider.tsx diff --git a/package-lock.json b/package-lock.json index c53c0a5..9c7a379 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "react": "19.1.0", "react-circular-progressbar": "^2.2.0", "react-dom": "19.1.0", + "react-ga4": "^2.1.0", "react-hook-form": "^7.63.0", "recharts": "^2.15.4", "tailwind-merge": "^3.3.1", @@ -9718,6 +9719,12 @@ "react": "^19.1.0" } }, + "node_modules/react-ga4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-ga4/-/react-ga4-2.1.0.tgz", + "integrity": "sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==", + "license": "MIT" + }, "node_modules/react-hook-form": { "version": "7.63.0", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", diff --git a/package.json b/package.json index 899d30e..ba5017f 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "react": "19.1.0", "react-circular-progressbar": "^2.2.0", "react-dom": "19.1.0", + "react-ga4": "^2.1.0", "react-hook-form": "^7.63.0", "recharts": "^2.15.4", "tailwind-merge": "^3.3.1", diff --git a/src/components/providers/MetricsProvider.tsx b/src/components/providers/MetricsProvider.tsx new file mode 100644 index 0000000..c40cf18 --- /dev/null +++ b/src/components/providers/MetricsProvider.tsx @@ -0,0 +1,132 @@ +"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 { + ReactGA.initialize(googleAnalyticsId); + console.log('[Metrics] Google Analytics initialized:', googleAnalyticsId); + } catch (error) { + console.error('[Metrics] Failed to initialize Google Analytics:', error); + } + }, [googleAnalyticsId]); + + // Инициализация Yandex Metrika (синхронно через скрипт) + useEffect(() => { + if (!yandexMetrikaId) return; + + try { + // Проверяем что скрипт еще не загружен + if (typeof window.ym === 'function') { + console.log('[Metrics] Yandex Metrika already loaded'); + 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); + } + }; + + document.head.appendChild(script); + } 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 index 3578f40..a9bc824 100644 --- a/src/components/providers/PixelsProvider.tsx +++ b/src/components/providers/PixelsProvider.tsx @@ -1,9 +1,10 @@ "use client"; import { useEffect, useState, type ReactNode } from "react"; -import { FacebookPixels, GoogleAnalytics, YandexMetrika, PageViewTracker } from "@/components/analytics"; +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; @@ -14,7 +15,8 @@ interface PixelsProviderProps { /** * Pixels Provider Component * - * Loads tracking scripts for Facebook Pixels, Google Analytics, and Yandex Metrika. + * Инициализирует все метрики синхронно через 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 @@ -26,11 +28,10 @@ interface PixelsProviderProps { * 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. Render GA and YM if IDs provided in funnel config + * 4. Pass all IDs to MetricsProvider for synchronous initialization */ export function PixelsProvider({ children, googleAnalyticsId, yandexMetrikaId }: PixelsProviderProps) { const [pixels, setPixels] = useState([]); - const [isLoading, setIsLoading] = useState(true); useEffect(() => { const loadPixels = async () => { @@ -40,7 +41,6 @@ export function PixelsProvider({ children, googleAnalyticsId, yandexMetrikaId }: if (cachedPixels) { const parsed = JSON.parse(cachedPixels); setPixels(parsed); - setIsLoading(false); return; } @@ -67,8 +67,6 @@ export function PixelsProvider({ children, googleAnalyticsId, yandexMetrikaId }: // Silently handle errors - pixels are optional console.warn("Facebook pixels not available:", error instanceof Error ? error.message : error); setPixels([]); - } finally { - setIsLoading(false); } }; @@ -76,12 +74,13 @@ export function PixelsProvider({ children, googleAnalyticsId, yandexMetrikaId }: }, []); return ( - <> - {!isLoading && } - - + {children} - + ); }