add pixel and metrika
This commit is contained in:
parent
fb7c21a7f5
commit
8c4a5f0ebe
@ -3,7 +3,8 @@
|
||||
"id": "soulmate",
|
||||
"title": "Новая воронка",
|
||||
"description": "Описание новой воронки",
|
||||
"firstScreenId": "onboarding"
|
||||
"firstScreenId": "onboarding",
|
||||
"yandexMetrikaId": "104471567"
|
||||
},
|
||||
"defaultTexts": {
|
||||
"nextButton": "Continue"
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
"id": "soulmate_prod",
|
||||
"title": "Soulmate V1",
|
||||
"description": "Soulmate",
|
||||
"firstScreenId": "onboarding"
|
||||
"firstScreenId": "onboarding",
|
||||
"yandexMetrikaId": "104471567"
|
||||
},
|
||||
"defaultTexts": {
|
||||
"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>
|
||||
</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
|
||||
title="Дефолтные тексты"
|
||||
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 { GoogleAnalytics } from "./GoogleAnalytics";
|
||||
export { YandexMetrika } from "./YandexMetrika";
|
||||
export { PageViewTracker } from "./PageViewTracker";
|
||||
|
||||
@ -3,16 +3,11 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { FunnelProvider } from "@/lib/funnel/FunnelProvider";
|
||||
import { PixelsProvider } from "@/components/providers/PixelsProvider";
|
||||
|
||||
interface AppProvidersProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function AppProviders({ children }: AppProvidersProps) {
|
||||
return (
|
||||
<PixelsProvider>
|
||||
<FunnelProvider>{children}</FunnelProvider>
|
||||
</PixelsProvider>
|
||||
);
|
||||
return <FunnelProvider>{children}</FunnelProvider>;
|
||||
}
|
||||
|
||||
@ -1,26 +1,34 @@
|
||||
"use client";
|
||||
|
||||
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 { getSourceByPathname } from "@/shared/utils/source";
|
||||
|
||||
interface PixelsProviderProps {
|
||||
children: ReactNode;
|
||||
googleAnalyticsId?: string;
|
||||
yandexMetrikaId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pixels Provider Component
|
||||
*
|
||||
* Loads Facebook pixels from backend and renders them.
|
||||
* Pixels are loaded once on mount and cached in localStorage.
|
||||
* Loads tracking scripts for Facebook Pixels, Google Analytics, and Yandex Metrika.
|
||||
*
|
||||
* 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 pixels
|
||||
* 2. If not cached, request from backend
|
||||
* 3. Save to localStorage and render
|
||||
* 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
|
||||
*/
|
||||
export function PixelsProvider({ children }: PixelsProviderProps) {
|
||||
export function PixelsProvider({ children, googleAnalyticsId, yandexMetrikaId }: PixelsProviderProps) {
|
||||
const [pixels, setPixels] = useState<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
@ -49,12 +57,15 @@ export function PixelsProvider({ children }: PixelsProviderProps) {
|
||||
|
||||
const pixelIds = response?.data?.fb || [];
|
||||
|
||||
// Save to localStorage
|
||||
// Save to localStorage only if we got pixels
|
||||
if (pixelIds.length > 0) {
|
||||
localStorage.setItem("fb_pixels", JSON.stringify(pixelIds));
|
||||
}
|
||||
|
||||
setPixels(pixelIds);
|
||||
} 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([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@ -67,6 +78,9 @@ export function PixelsProvider({ children }: PixelsProviderProps) {
|
||||
return (
|
||||
<>
|
||||
{!isLoading && <FacebookPixels pixels={pixels} />}
|
||||
<GoogleAnalytics measurementId={googleAnalyticsId} />
|
||||
<YandexMetrika counterId={yandexMetrikaId} />
|
||||
<PageViewTracker />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -7,6 +7,7 @@ import { ICreateAuthorizeRequest } from "@/entities/user/types";
|
||||
import { filterNullKeysOfObject } from "@/shared/utils/filter-object";
|
||||
import { createAuthorization } from "@/entities/user/actions";
|
||||
import { setAuthTokenToCookie } from "@/entities/user/serverActions";
|
||||
import analyticsService, { AnalyticsEvent, AnalyticsPlatform } from "@/services/analytics/analyticsService";
|
||||
|
||||
// TODO
|
||||
const locale = "en";
|
||||
@ -84,49 +85,40 @@ export const useAuth = ({ funnelId, registrationData }: IUseAuthProps) => {
|
||||
}
|
||||
|
||||
const payload = getAuthorizationPayload(email);
|
||||
const {
|
||||
token,
|
||||
// userId: userIdFromApi,
|
||||
// generatingVideo,
|
||||
// videoId,
|
||||
// authCode,
|
||||
} = await createAuthorization(payload);
|
||||
const { token, userId } = await createAuthorization(payload);
|
||||
|
||||
// Track registration events in analytics
|
||||
// Send EnteredEmail to Yandex Metrika and Google Analytics
|
||||
analyticsService.trackEvent(
|
||||
AnalyticsEvent.ENTERED_EMAIL,
|
||||
[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);
|
||||
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) {
|
||||
setError((error as Error).message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[getAllCookies, getAuthorizationPayload, updateSession]
|
||||
[getAllCookies, getAuthorizationPayload, updateSession, funnelId]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
|
||||
@ -11,7 +11,8 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
|
||||
"id": "soulmate_prod",
|
||||
"title": "Soulmate V1",
|
||||
"description": "Soulmate",
|
||||
"firstScreenId": "onboarding"
|
||||
"firstScreenId": "onboarding",
|
||||
"yandexMetrikaId": "104471567"
|
||||
},
|
||||
"defaultTexts": {
|
||||
"nextButton": "Next",
|
||||
@ -2697,7 +2698,8 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
|
||||
"id": "soulmate",
|
||||
"title": "Новая воронка",
|
||||
"description": "Описание новой воронки",
|
||||
"firstScreenId": "onboarding"
|
||||
"firstScreenId": "onboarding",
|
||||
"yandexMetrikaId": "104471567"
|
||||
},
|
||||
"defaultTexts": {
|
||||
"nextButton": "Continue"
|
||||
|
||||
@ -517,6 +517,16 @@ export interface FunnelMetaDefinition {
|
||||
title?: string;
|
||||
description?: 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 {
|
||||
|
||||
@ -222,6 +222,8 @@ const FunnelMetaSchema = new Schema(
|
||||
title: String,
|
||||
description: String,
|
||||
firstScreenId: String,
|
||||
googleAnalyticsId: String,
|
||||
yandexMetrikaId: String,
|
||||
},
|
||||
{ _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