add pixel and metrika
This commit is contained in:
parent
fb7c21a7f5
commit
8c4a5f0ebe
@ -3,7 +3,8 @@
|
|||||||
"id": "soulmate",
|
"id": "soulmate",
|
||||||
"title": "Новая воронка",
|
"title": "Новая воронка",
|
||||||
"description": "Описание новой воронки",
|
"description": "Описание новой воронки",
|
||||||
"firstScreenId": "onboarding"
|
"firstScreenId": "onboarding",
|
||||||
|
"yandexMetrikaId": "104471567"
|
||||||
},
|
},
|
||||||
"defaultTexts": {
|
"defaultTexts": {
|
||||||
"nextButton": "Continue"
|
"nextButton": "Continue"
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
"id": "soulmate_prod",
|
"id": "soulmate_prod",
|
||||||
"title": "Soulmate V1",
|
"title": "Soulmate V1",
|
||||||
"description": "Soulmate",
|
"description": "Soulmate",
|
||||||
"firstScreenId": "onboarding"
|
"firstScreenId": "onboarding",
|
||||||
|
"yandexMetrikaId": "104471567"
|
||||||
},
|
},
|
||||||
"defaultTexts": {
|
"defaultTexts": {
|
||||||
"nextButton": "Next",
|
"nextButton": "Next",
|
||||||
|
|||||||
77
src/app/[funnelId]/layout.tsx
Normal file
77
src/app/[funnelId]/layout.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { PixelsProvider } from "@/components/providers/PixelsProvider";
|
||||||
|
import type { FunnelDefinition } from "@/lib/funnel/types";
|
||||||
|
import { BAKED_FUNNELS } from "@/lib/funnel/bakedFunnels";
|
||||||
|
import { IS_FULL_SYSTEM_BUILD } from "@/lib/runtime/buildVariant";
|
||||||
|
|
||||||
|
// Функция для загрузки воронки из базы данных
|
||||||
|
async function loadFunnelFromDatabase(
|
||||||
|
funnelId: string
|
||||||
|
): Promise<FunnelDefinition | null> {
|
||||||
|
if (!IS_FULL_SYSTEM_BUILD) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { default: connectMongoDB } = await import("@/lib/mongodb");
|
||||||
|
const { default: FunnelModel } = await import("@/lib/models/Funnel");
|
||||||
|
|
||||||
|
await connectMongoDB();
|
||||||
|
|
||||||
|
const funnel = await FunnelModel.findOne({
|
||||||
|
"funnelData.meta.id": funnelId,
|
||||||
|
status: { $in: ["published", "draft"] },
|
||||||
|
}).lean();
|
||||||
|
|
||||||
|
if (funnel) {
|
||||||
|
return funnel.funnelData as FunnelDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to load funnel '${funnelId}' from database:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FunnelLayoutProps {
|
||||||
|
children: ReactNode;
|
||||||
|
params: Promise<{
|
||||||
|
funnelId: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function FunnelLayout({
|
||||||
|
children,
|
||||||
|
params,
|
||||||
|
}: FunnelLayoutProps) {
|
||||||
|
const { funnelId } = await params;
|
||||||
|
|
||||||
|
let funnel: FunnelDefinition | null = null;
|
||||||
|
|
||||||
|
// Сначала пытаемся загрузить из базы данных
|
||||||
|
funnel = await loadFunnelFromDatabase(funnelId);
|
||||||
|
|
||||||
|
// Если не найдено в базе, пытаемся загрузить из JSON файлов
|
||||||
|
if (!funnel) {
|
||||||
|
funnel = BAKED_FUNNELS[funnelId] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если воронка не найдена ни в базе, ни в файлах
|
||||||
|
if (!funnel) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PixelsProvider
|
||||||
|
googleAnalyticsId={funnel.meta.googleAnalyticsId}
|
||||||
|
yandexMetrikaId={funnel.meta.yandexMetrikaId}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PixelsProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -409,6 +409,27 @@ export function BuilderSidebar() {
|
|||||||
</label>
|
</label>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Аналитика" description="Настройки трекинга">
|
||||||
|
<TextInput
|
||||||
|
label="Google Analytics ID"
|
||||||
|
placeholder="G-XXXXXXXXXX"
|
||||||
|
value={state.meta.googleAnalyticsId ?? ""}
|
||||||
|
onChange={(event) =>
|
||||||
|
handleMetaChange("googleAnalyticsId", event.target.value)
|
||||||
|
}
|
||||||
|
className="placeholder:text-sm"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Yandex Metrika ID"
|
||||||
|
placeholder="95799066"
|
||||||
|
value={state.meta.yandexMetrikaId ?? ""}
|
||||||
|
onChange={(event) =>
|
||||||
|
handleMetaChange("yandexMetrikaId", event.target.value)
|
||||||
|
}
|
||||||
|
className="placeholder:text-sm"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
title="Дефолтные тексты"
|
title="Дефолтные тексты"
|
||||||
description="Текст кнопок и баннеров"
|
description="Текст кнопок и баннеров"
|
||||||
|
|||||||
@ -1,97 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
|
||||||
import {
|
|
||||||
useBuilderDispatch,
|
|
||||||
useBuilderState,
|
|
||||||
} from "@/lib/admin/builder/context";
|
|
||||||
import { Section } from "./Section";
|
|
||||||
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
|
||||||
|
|
||||||
export function FunnelSettingsPanel() {
|
|
||||||
const state = useBuilderState();
|
|
||||||
const dispatch = useBuilderDispatch();
|
|
||||||
|
|
||||||
const screenOptions = useMemo(
|
|
||||||
() =>
|
|
||||||
state.screens.map((screen: BuilderScreen) => ({
|
|
||||||
id: screen.id,
|
|
||||||
title: screen.title?.text,
|
|
||||||
})),
|
|
||||||
[state.screens]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMetaChange = (field: keyof typeof state.meta, value: string) => {
|
|
||||||
dispatch({ type: "set-meta", payload: { [field]: value } });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFirstScreenChange = (value: string) => {
|
|
||||||
dispatch({ type: "set-meta", payload: { firstScreenId: value } });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDefaultTextsChange = (
|
|
||||||
field: keyof NonNullable<typeof state.defaultTexts>,
|
|
||||||
value: string
|
|
||||||
) => {
|
|
||||||
dispatch({ type: "set-default-texts", payload: { [field]: value } });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Section title="Настройки воронки">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<TextInput
|
|
||||||
label="ID воронки"
|
|
||||||
value={state.meta.id}
|
|
||||||
onChange={(e) => handleMetaChange("id", e.target.value)}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label="Название"
|
|
||||||
value={state.meta.title || ""}
|
|
||||||
onChange={(e) => handleMetaChange("title", e.target.value)}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label="Описание"
|
|
||||||
value={state.meta.description || ""}
|
|
||||||
onChange={(e) => handleMetaChange("description", e.target.value)}
|
|
||||||
/>
|
|
||||||
<label className="flex flex-col gap-2 text-sm">
|
|
||||||
<span className="text-muted-foreground">Первый экран</span>
|
|
||||||
<select
|
|
||||||
className="rounded-lg border border-border bg-background px-3 py-2"
|
|
||||||
value={state.meta.firstScreenId || ""}
|
|
||||||
onChange={(e) => handleFirstScreenChange(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="">—</option>
|
|
||||||
{screenOptions.map((screen) => (
|
|
||||||
<option key={screen.id} value={screen.id}>
|
|
||||||
{screen.title}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section title="Дефолтные тексты">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<TextInput
|
|
||||||
label='Кнопка "Next"'
|
|
||||||
placeholder="Next"
|
|
||||||
value={state.defaultTexts?.nextButton || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleDefaultTextsChange("nextButton", e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label='Кнопка "Continue"'
|
|
||||||
placeholder="Continue"
|
|
||||||
value={state.defaultTexts?.continueButton || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleDefaultTextsChange("continueButton", e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
47
src/components/analytics/GoogleAnalytics.tsx
Normal file
47
src/components/analytics/GoogleAnalytics.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
|
interface GoogleAnalyticsProps {
|
||||||
|
measurementId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Google Analytics Integration Component
|
||||||
|
*
|
||||||
|
* Loads Google Analytics (GA4) tracking script dynamically based on measurement ID
|
||||||
|
* received from the funnel configuration.
|
||||||
|
*
|
||||||
|
* Page views are tracked by PageViewTracker component on route changes.
|
||||||
|
*
|
||||||
|
* @param measurementId - Google Analytics Measurement ID (e.g., "G-XXXXXXXXXX")
|
||||||
|
*/
|
||||||
|
export function GoogleAnalytics({ measurementId }: GoogleAnalyticsProps) {
|
||||||
|
if (!measurementId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Script
|
||||||
|
id="google-analytics"
|
||||||
|
strategy="afterInteractive"
|
||||||
|
src={`https://www.googletagmanager.com/gtag/js?id=${measurementId}`}
|
||||||
|
/>
|
||||||
|
<Script
|
||||||
|
id="google-analytics-init"
|
||||||
|
strategy="afterInteractive"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
gtag('config', '${measurementId}', {
|
||||||
|
send_page_view: false
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
src/components/analytics/PageViewTracker.tsx
Normal file
42
src/components/analytics/PageViewTracker.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { usePathname, useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page View Tracker Component
|
||||||
|
*
|
||||||
|
* Tracks page views in Google Analytics and Yandex Metrika
|
||||||
|
* when route changes occur (client-side navigation).
|
||||||
|
*
|
||||||
|
* Must be included in the app layout or root component.
|
||||||
|
*/
|
||||||
|
export function PageViewTracker() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const url = pathname + (searchParams?.toString() ? `?${searchParams.toString()}` : "");
|
||||||
|
|
||||||
|
// Track page view in Google Analytics
|
||||||
|
if (typeof window !== "undefined" && typeof window.gtag === "function") {
|
||||||
|
window.gtag("event", "page_view", {
|
||||||
|
page_path: url,
|
||||||
|
page_location: window.location.href,
|
||||||
|
page_title: document.title,
|
||||||
|
});
|
||||||
|
console.log(`[GA] Page view tracked: ${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track page view in Yandex Metrika
|
||||||
|
if (typeof window !== "undefined" && typeof window.ym === "function") {
|
||||||
|
const counterId = window.__YM_COUNTER_ID__;
|
||||||
|
if (counterId) {
|
||||||
|
window.ym(counterId, "hit", url);
|
||||||
|
console.log(`[YM] Page view tracked: ${url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [pathname, searchParams]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
65
src/components/analytics/YandexMetrika.tsx
Normal file
65
src/components/analytics/YandexMetrika.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
|
interface YandexMetrikaProps {
|
||||||
|
counterId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yandex Metrika Integration Component
|
||||||
|
*
|
||||||
|
* Loads Yandex Metrika tracking script dynamically based on counter ID
|
||||||
|
* received from the funnel configuration.
|
||||||
|
*
|
||||||
|
* Initializes with: clickmap, trackLinks, accurateTrackBounce, webvisor.
|
||||||
|
* Page views are tracked by PageViewTracker component on route changes.
|
||||||
|
*
|
||||||
|
* @param counterId - Yandex Metrika Counter ID (e.g., "95799066")
|
||||||
|
*/
|
||||||
|
export function YandexMetrika({ counterId }: YandexMetrikaProps) {
|
||||||
|
if (!counterId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Script
|
||||||
|
id="yandex-metrika"
|
||||||
|
strategy="afterInteractive"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
(function(m,e,t,r,i,k,a){
|
||||||
|
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
||||||
|
m[i].l=1*new Date();
|
||||||
|
for (var j = 0; j < document.scripts.length; j++) {
|
||||||
|
if (document.scripts[j].src === r) { return; }
|
||||||
|
}
|
||||||
|
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
|
||||||
|
})(window, document, "script", "https://cdn.jsdelivr.net/npm/yandex-metrica-watch/tag.js", "ym");
|
||||||
|
|
||||||
|
// Store counter ID for analyticsService
|
||||||
|
window.__YM_COUNTER_ID__ = ${counterId};
|
||||||
|
|
||||||
|
ym(${counterId}, "init", {
|
||||||
|
clickmap: true,
|
||||||
|
trackLinks: true,
|
||||||
|
accurateTrackBounce: true,
|
||||||
|
webvisor: true
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<noscript>
|
||||||
|
<div>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={`https://mc.yandex.ru/watch/${counterId}`}
|
||||||
|
style={{ position: "absolute", left: "-9999px" }}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1 +1,4 @@
|
|||||||
export { FacebookPixels } from "./FacebookPixels";
|
export { FacebookPixels } from "./FacebookPixels";
|
||||||
|
export { GoogleAnalytics } from "./GoogleAnalytics";
|
||||||
|
export { YandexMetrika } from "./YandexMetrika";
|
||||||
|
export { PageViewTracker } from "./PageViewTracker";
|
||||||
|
|||||||
@ -3,16 +3,11 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { FunnelProvider } from "@/lib/funnel/FunnelProvider";
|
import { FunnelProvider } from "@/lib/funnel/FunnelProvider";
|
||||||
import { PixelsProvider } from "@/components/providers/PixelsProvider";
|
|
||||||
|
|
||||||
interface AppProvidersProps {
|
interface AppProvidersProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppProviders({ children }: AppProvidersProps) {
|
export function AppProviders({ children }: AppProvidersProps) {
|
||||||
return (
|
return <FunnelProvider>{children}</FunnelProvider>;
|
||||||
<PixelsProvider>
|
|
||||||
<FunnelProvider>{children}</FunnelProvider>
|
|
||||||
</PixelsProvider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +1,34 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, type ReactNode } from "react";
|
import { useEffect, useState, type ReactNode } from "react";
|
||||||
import { FacebookPixels } from "@/components/analytics/FacebookPixels";
|
import { FacebookPixels, GoogleAnalytics, YandexMetrika, PageViewTracker } from "@/components/analytics";
|
||||||
import { getPixels } from "@/entities/session/actions";
|
import { getPixels } from "@/entities/session/actions";
|
||||||
import { getSourceByPathname } from "@/shared/utils/source";
|
import { getSourceByPathname } from "@/shared/utils/source";
|
||||||
|
|
||||||
interface PixelsProviderProps {
|
interface PixelsProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
googleAnalyticsId?: string;
|
||||||
|
yandexMetrikaId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pixels Provider Component
|
* Pixels Provider Component
|
||||||
*
|
*
|
||||||
* Loads Facebook pixels from backend and renders them.
|
* Loads tracking scripts for Facebook Pixels, Google Analytics, and Yandex Metrika.
|
||||||
* Pixels are loaded once on mount and cached in localStorage.
|
*
|
||||||
|
* 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:
|
* Flow:
|
||||||
* 1. Check localStorage for cached pixels
|
* 1. Check localStorage for cached FB pixels
|
||||||
* 2. If not cached, request from backend
|
* 2. If not cached, request from backend (errors are handled gracefully)
|
||||||
* 3. Save to localStorage and render
|
* 3. Save to localStorage if pixels received
|
||||||
|
* 4. Render GA and YM if IDs provided in funnel config
|
||||||
*/
|
*/
|
||||||
export function PixelsProvider({ children }: PixelsProviderProps) {
|
export function PixelsProvider({ children, googleAnalyticsId, yandexMetrikaId }: PixelsProviderProps) {
|
||||||
const [pixels, setPixels] = useState<string[]>([]);
|
const [pixels, setPixels] = useState<string[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
@ -49,12 +57,15 @@ export function PixelsProvider({ children }: PixelsProviderProps) {
|
|||||||
|
|
||||||
const pixelIds = response?.data?.fb || [];
|
const pixelIds = response?.data?.fb || [];
|
||||||
|
|
||||||
// Save to localStorage
|
// Save to localStorage only if we got pixels
|
||||||
localStorage.setItem("fb_pixels", JSON.stringify(pixelIds));
|
if (pixelIds.length > 0) {
|
||||||
|
localStorage.setItem("fb_pixels", JSON.stringify(pixelIds));
|
||||||
|
}
|
||||||
|
|
||||||
setPixels(pixelIds);
|
setPixels(pixelIds);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load Facebook pixels:", error);
|
// Silently handle errors - pixels are optional
|
||||||
|
console.warn("Facebook pixels not available:", error instanceof Error ? error.message : error);
|
||||||
setPixels([]);
|
setPixels([]);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -67,6 +78,9 @@ export function PixelsProvider({ children }: PixelsProviderProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isLoading && <FacebookPixels pixels={pixels} />}
|
{!isLoading && <FacebookPixels pixels={pixels} />}
|
||||||
|
<GoogleAnalytics measurementId={googleAnalyticsId} />
|
||||||
|
<YandexMetrika counterId={yandexMetrikaId} />
|
||||||
|
<PageViewTracker />
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { ICreateAuthorizeRequest } from "@/entities/user/types";
|
|||||||
import { filterNullKeysOfObject } from "@/shared/utils/filter-object";
|
import { filterNullKeysOfObject } from "@/shared/utils/filter-object";
|
||||||
import { createAuthorization } from "@/entities/user/actions";
|
import { createAuthorization } from "@/entities/user/actions";
|
||||||
import { setAuthTokenToCookie } from "@/entities/user/serverActions";
|
import { setAuthTokenToCookie } from "@/entities/user/serverActions";
|
||||||
|
import analyticsService, { AnalyticsEvent, AnalyticsPlatform } from "@/services/analytics/analyticsService";
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
const locale = "en";
|
const locale = "en";
|
||||||
@ -84,49 +85,40 @@ export const useAuth = ({ funnelId, registrationData }: IUseAuthProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload = getAuthorizationPayload(email);
|
const payload = getAuthorizationPayload(email);
|
||||||
const {
|
const { token, userId } = await createAuthorization(payload);
|
||||||
token,
|
|
||||||
// userId: userIdFromApi,
|
// Track registration events in analytics
|
||||||
// generatingVideo,
|
// Send EnteredEmail to Yandex Metrika and Google Analytics
|
||||||
// videoId,
|
analyticsService.trackEvent(
|
||||||
// authCode,
|
AnalyticsEvent.ENTERED_EMAIL,
|
||||||
} = await createAuthorization(payload);
|
[AnalyticsPlatform.YANDEX_METRIKA, AnalyticsPlatform.GOOGLE_ANALYTICS]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send Lead to Facebook Pixel
|
||||||
|
analyticsService.trackEvent(
|
||||||
|
AnalyticsEvent.LEAD,
|
||||||
|
[AnalyticsPlatform.FACEBOOK]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set user ID and properties in analytics
|
||||||
|
if (userId) {
|
||||||
|
analyticsService.setUserId(userId);
|
||||||
|
analyticsService.setUserProperties({
|
||||||
|
email,
|
||||||
|
source: funnelId,
|
||||||
|
UserID: userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await setAuthTokenToCookie(token);
|
await setAuthTokenToCookie(token);
|
||||||
return token;
|
return token;
|
||||||
// const { user: userMe } = await api.getMe({ token });
|
|
||||||
// const userId = userIdFromApi || userMe?._id;
|
|
||||||
// if (userId?.length) {
|
|
||||||
// dispatch(actions.userId.update({ userId }));
|
|
||||||
// metricService.userParams({
|
|
||||||
// hasPersonalVideo: generatingVideo || false,
|
|
||||||
// email: user?.email,
|
|
||||||
// UserID: userId,
|
|
||||||
// });
|
|
||||||
// metricService.setUserID(userId);
|
|
||||||
// }
|
|
||||||
// signUp(token, userMe, isAnonymous);
|
|
||||||
// setToken(token);
|
|
||||||
// dispatch(actions.userConfig.setAuthCode(authCode || ""));
|
|
||||||
// dispatch(
|
|
||||||
// actions.personalVideo.updateStatus({
|
|
||||||
// generatingVideo: generatingVideo || false,
|
|
||||||
// videoId: videoId || "",
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// if (generatingVideo) {
|
|
||||||
// metricService.reachGoal(EGoals.ROSE_VIDEO_CREATION_START, [
|
|
||||||
// EMetrics.YANDEX,
|
|
||||||
// EMetrics.KLAVIYO,
|
|
||||||
// ]);
|
|
||||||
// }
|
|
||||||
// dispatch(actions.status.update("registred"));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError((error as Error).message);
|
setError((error as Error).message);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[getAllCookies, getAuthorizationPayload, updateSession]
|
[getAllCookies, getAuthorizationPayload, updateSession, funnelId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
|
|||||||
@ -11,7 +11,8 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
|
|||||||
"id": "soulmate_prod",
|
"id": "soulmate_prod",
|
||||||
"title": "Soulmate V1",
|
"title": "Soulmate V1",
|
||||||
"description": "Soulmate",
|
"description": "Soulmate",
|
||||||
"firstScreenId": "onboarding"
|
"firstScreenId": "onboarding",
|
||||||
|
"yandexMetrikaId": "104471567"
|
||||||
},
|
},
|
||||||
"defaultTexts": {
|
"defaultTexts": {
|
||||||
"nextButton": "Next",
|
"nextButton": "Next",
|
||||||
@ -2697,7 +2698,8 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
|
|||||||
"id": "soulmate",
|
"id": "soulmate",
|
||||||
"title": "Новая воронка",
|
"title": "Новая воронка",
|
||||||
"description": "Описание новой воронки",
|
"description": "Описание новой воронки",
|
||||||
"firstScreenId": "onboarding"
|
"firstScreenId": "onboarding",
|
||||||
|
"yandexMetrikaId": "104471567"
|
||||||
},
|
},
|
||||||
"defaultTexts": {
|
"defaultTexts": {
|
||||||
"nextButton": "Continue"
|
"nextButton": "Continue"
|
||||||
|
|||||||
@ -517,6 +517,16 @@ export interface FunnelMetaDefinition {
|
|||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
firstScreenId?: string;
|
firstScreenId?: string;
|
||||||
|
/**
|
||||||
|
* Google Analytics Measurement ID (e.g., "G-XXXXXXXXXX")
|
||||||
|
* If provided, Google Analytics will be loaded for this funnel
|
||||||
|
*/
|
||||||
|
googleAnalyticsId?: string;
|
||||||
|
/**
|
||||||
|
* Yandex Metrika Counter ID (e.g., "95799066")
|
||||||
|
* If provided, Yandex Metrika will be loaded for this funnel
|
||||||
|
*/
|
||||||
|
yandexMetrikaId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunnelDefinition {
|
export interface FunnelDefinition {
|
||||||
|
|||||||
@ -222,6 +222,8 @@ const FunnelMetaSchema = new Schema(
|
|||||||
title: String,
|
title: String,
|
||||||
description: String,
|
description: String,
|
||||||
firstScreenId: String,
|
firstScreenId: String,
|
||||||
|
googleAnalyticsId: String,
|
||||||
|
yandexMetrikaId: String,
|
||||||
},
|
},
|
||||||
{ _id: false }
|
{ _id: false }
|
||||||
);
|
);
|
||||||
|
|||||||
201
src/services/analytics/analyticsService.ts
Normal file
201
src/services/analytics/analyticsService.ts
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
/**
|
||||||
|
* Analytics Service
|
||||||
|
*
|
||||||
|
* Centralized service for sending events to multiple analytics platforms:
|
||||||
|
* - Facebook Pixel
|
||||||
|
* - Google Analytics (GA4)
|
||||||
|
* - Yandex Metrika
|
||||||
|
*
|
||||||
|
* Page views are tracked automatically by GA4 and Yandex Metrika.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* analyticsService.trackEvent(AnalyticsEvent.ENTERED_EMAIL);
|
||||||
|
* analyticsService.setUserId(userId);
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available analytics events
|
||||||
|
*/
|
||||||
|
export enum AnalyticsEvent {
|
||||||
|
// Registration events
|
||||||
|
ENTERED_EMAIL = "EnteredEmail",
|
||||||
|
LEAD = "Lead", // Facebook-specific event name for EnteredEmail
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analytics platforms
|
||||||
|
*/
|
||||||
|
export enum AnalyticsPlatform {
|
||||||
|
FACEBOOK = "Facebook",
|
||||||
|
GOOGLE_ANALYTICS = "GoogleAnalytics",
|
||||||
|
YANDEX_METRIKA = "YandexMetrika",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventOptions {
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Yandex Metrika is available
|
||||||
|
*/
|
||||||
|
function isYandexMetrikaAvailable(): boolean {
|
||||||
|
return typeof window !== "undefined" && typeof window.ym === "function";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Google Analytics is available
|
||||||
|
*/
|
||||||
|
function isGoogleAnalyticsAvailable(): boolean {
|
||||||
|
return typeof window !== "undefined" && typeof window.gtag === "function";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Facebook Pixel is available
|
||||||
|
*/
|
||||||
|
function isFacebookPixelAvailable(): boolean {
|
||||||
|
return typeof window !== "undefined" && typeof window.fbq === "function";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track event in Yandex Metrika
|
||||||
|
*/
|
||||||
|
function trackYandexMetrikaEvent(
|
||||||
|
event: AnalyticsEvent,
|
||||||
|
options?: EventOptions
|
||||||
|
): void {
|
||||||
|
if (!isYandexMetrikaAvailable()) {
|
||||||
|
console.warn("Yandex Metrika is not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get counter ID from the page (injected by YandexMetrika component)
|
||||||
|
const counterId = window.__YM_COUNTER_ID__;
|
||||||
|
if (!counterId) {
|
||||||
|
console.warn("Yandex Metrika counter ID not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ym(counterId, "reachGoal", event, options);
|
||||||
|
console.log(`[YM] Event: ${event}`, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track event in Google Analytics
|
||||||
|
*/
|
||||||
|
function trackGoogleAnalyticsEvent(
|
||||||
|
event: AnalyticsEvent,
|
||||||
|
options?: EventOptions
|
||||||
|
): void {
|
||||||
|
if (!isGoogleAnalyticsAvailable()) {
|
||||||
|
console.warn("Google Analytics is not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gtag("event", event, options);
|
||||||
|
console.log(`[GA] Event: ${event}`, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track event in Facebook Pixel
|
||||||
|
*/
|
||||||
|
function trackFacebookPixelEvent(
|
||||||
|
event: AnalyticsEvent,
|
||||||
|
options?: EventOptions
|
||||||
|
): void {
|
||||||
|
if (!isFacebookPixelAvailable()) {
|
||||||
|
console.warn("Facebook Pixel is not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map EnteredEmail to Lead for Facebook
|
||||||
|
const fbEvent = event === AnalyticsEvent.ENTERED_EMAIL ? AnalyticsEvent.LEAD : event;
|
||||||
|
|
||||||
|
window.fbq("track", fbEvent, options);
|
||||||
|
console.log(`[FB] Event: ${fbEvent}`, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track event across multiple platforms
|
||||||
|
*
|
||||||
|
* @param event - Event to track
|
||||||
|
* @param platforms - Platforms to send event to (defaults to all available)
|
||||||
|
* @param options - Additional event data
|
||||||
|
*/
|
||||||
|
export function trackEvent(
|
||||||
|
event: AnalyticsEvent,
|
||||||
|
platforms?: AnalyticsPlatform[],
|
||||||
|
options?: EventOptions
|
||||||
|
): void {
|
||||||
|
// If no platforms specified, send to all available
|
||||||
|
const targetPlatforms = platforms || [
|
||||||
|
AnalyticsPlatform.FACEBOOK,
|
||||||
|
AnalyticsPlatform.GOOGLE_ANALYTICS,
|
||||||
|
AnalyticsPlatform.YANDEX_METRIKA,
|
||||||
|
];
|
||||||
|
|
||||||
|
targetPlatforms.forEach((platform) => {
|
||||||
|
switch (platform) {
|
||||||
|
case AnalyticsPlatform.YANDEX_METRIKA:
|
||||||
|
trackYandexMetrikaEvent(event, options);
|
||||||
|
break;
|
||||||
|
case AnalyticsPlatform.GOOGLE_ANALYTICS:
|
||||||
|
trackGoogleAnalyticsEvent(event, options);
|
||||||
|
break;
|
||||||
|
case AnalyticsPlatform.FACEBOOK:
|
||||||
|
trackFacebookPixelEvent(event, options);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set user ID across all analytics platforms
|
||||||
|
*/
|
||||||
|
export function setUserId(userId: string): void {
|
||||||
|
// Yandex Metrika
|
||||||
|
if (isYandexMetrikaAvailable()) {
|
||||||
|
const counterId = window.__YM_COUNTER_ID__;
|
||||||
|
if (counterId) {
|
||||||
|
window.ym(counterId, "setUserID", userId);
|
||||||
|
console.log(`[YM] User ID set: ${userId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google Analytics - set user_id property for all subsequent events
|
||||||
|
if (isGoogleAnalyticsAvailable()) {
|
||||||
|
window.gtag("set", { user_id: userId });
|
||||||
|
console.log(`[GA] User ID set: ${userId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set user properties/parameters
|
||||||
|
*/
|
||||||
|
export function setUserProperties(properties: Record<string, unknown>): void {
|
||||||
|
// Yandex Metrika
|
||||||
|
if (isYandexMetrikaAvailable()) {
|
||||||
|
const counterId = window.__YM_COUNTER_ID__;
|
||||||
|
if (counterId) {
|
||||||
|
window.ym(counterId, "userParams", properties);
|
||||||
|
console.log(`[YM] User params set:`, properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google Analytics
|
||||||
|
if (isGoogleAnalyticsAvailable()) {
|
||||||
|
window.gtag("set", "user_properties", properties);
|
||||||
|
console.log(`[GA] User properties set:`, properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const analyticsService = {
|
||||||
|
trackEvent,
|
||||||
|
setUserId,
|
||||||
|
setUserProperties,
|
||||||
|
AnalyticsEvent,
|
||||||
|
AnalyticsPlatform,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default analyticsService;
|
||||||
1
src/services/analytics/index.ts
Normal file
1
src/services/analytics/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as analyticsService, AnalyticsEvent, AnalyticsPlatform } from "./analyticsService";
|
||||||
25
src/services/analytics/types.ts
Normal file
25
src/services/analytics/types.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Analytics Window Extensions
|
||||||
|
*
|
||||||
|
* Type definitions for analytics scripts injected into window object
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
// Yandex Metrika
|
||||||
|
ym: (counterId: number | string, method: string, ...args: any[]) => void;
|
||||||
|
__YM_COUNTER_ID__?: number | string;
|
||||||
|
|
||||||
|
// Google Analytics (GA4)
|
||||||
|
gtag: (command: string, ...args: any[]) => void;
|
||||||
|
dataLayer: any[];
|
||||||
|
|
||||||
|
// Facebook Pixel
|
||||||
|
fbq: (command: string, event: string, params?: Record<string, any>) => void;
|
||||||
|
_fbq: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
export {};
|
||||||
Loading…
Reference in New Issue
Block a user