commit
be0d0952fb
@ -10,6 +10,7 @@ const nextConfig: NextConfig = {
|
||||
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
|
||||
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
|
||||
NEXT_PUBLIC_AUTH_REDIRECT_URL: process.env.NEXT_PUBLIC_AUTH_REDIRECT_URL,
|
||||
NEXT_PUBLIC_YM_ID: process.env.NEXT_PUBLIC_YM_ID,
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
|
||||
@ -9,10 +9,11 @@ import { hasLocale, NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages } from "next-intl/server";
|
||||
import clsx from "clsx";
|
||||
|
||||
import YandexMetrika from "@/components/analytics/YandexMetrika";
|
||||
import { PageViewTracker,YandexMetrika } from "@/components/analytics";
|
||||
import { loadChatsList } from "@/entities/chats/loaders";
|
||||
import { loadUser, loadUserId } from "@/entities/user/loaders";
|
||||
import { routing } from "@/i18n/routing";
|
||||
import { AnalyticsProvider } from "@/providers/analytics-provider";
|
||||
import { AppUiStoreProvider } from "@/providers/app-ui-store-provider";
|
||||
import { AudioProvider } from "@/providers/audio-provider";
|
||||
import { ChatsInitializationProvider } from "@/providers/chats-initialization-provider";
|
||||
@ -68,24 +69,29 @@ export default async function RootLayout({
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<body className={clsx(inter.variable, styles.body)}>
|
||||
{/* Analytics Components */}
|
||||
<YandexMetrika />
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<UserProvider user={user}>
|
||||
<SocketProvider userId={userId}>
|
||||
<RetainingStoreProvider>
|
||||
<AudioProvider>
|
||||
<ChatsInitializationProvider>
|
||||
<ChatsProvider initialChats={chats}>
|
||||
<ToastProvider maxVisible={3}>
|
||||
<AppUiStoreProvider>{children}</AppUiStoreProvider>
|
||||
</ToastProvider>
|
||||
</ChatsProvider>
|
||||
</ChatsInitializationProvider>
|
||||
</AudioProvider>
|
||||
</RetainingStoreProvider>
|
||||
</SocketProvider>
|
||||
</UserProvider>
|
||||
</NextIntlClientProvider>
|
||||
|
||||
<AnalyticsProvider>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<UserProvider user={user}>
|
||||
<SocketProvider userId={userId}>
|
||||
<RetainingStoreProvider>
|
||||
<AudioProvider>
|
||||
<ChatsInitializationProvider>
|
||||
<ChatsProvider initialChats={chats}>
|
||||
<ToastProvider maxVisible={3}>
|
||||
<PageViewTracker />
|
||||
<AppUiStoreProvider>{children}</AppUiStoreProvider>
|
||||
</ToastProvider>
|
||||
</ChatsProvider>
|
||||
</ChatsInitializationProvider>
|
||||
</AudioProvider>
|
||||
</RetainingStoreProvider>
|
||||
</SocketProvider>
|
||||
</UserProvider>
|
||||
</NextIntlClientProvider>
|
||||
</AnalyticsProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
54
src/components/analytics/PageViewTracker.tsx
Normal file
54
src/components/analytics/PageViewTracker.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
|
||||
/**
|
||||
* Wait for Yandex Metrika to be loaded
|
||||
* Retry mechanism with timeout to handle async script loading
|
||||
*/
|
||||
async function waitForYandexMetrika(maxAttempts = 10, delayMs = 100): Promise<boolean> {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
if (typeof window !== "undefined" &&
|
||||
typeof window.ym === "function" &&
|
||||
window.__YM_COUNTER_ID__) {
|
||||
return true;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page View Tracker Component
|
||||
*
|
||||
* Tracks page views in 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 Yandex Metrika (with retry logic)
|
||||
const trackYandexMetrika = async () => {
|
||||
const isYmAvailable = await waitForYandexMetrika();
|
||||
|
||||
if (isYmAvailable && typeof window.ym === "function") {
|
||||
const counterId = window.__YM_COUNTER_ID__;
|
||||
if (counterId) {
|
||||
window.ym(counterId, "hit", url);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Execute YM tracking
|
||||
trackYandexMetrika();
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -1,79 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import Script from "next/script";
|
||||
|
||||
const YANDEX_METRIKA_ID = 103412914;
|
||||
|
||||
export default function YandexMetrika() {
|
||||
useEffect(() => {
|
||||
// Initialize Yandex.Metrika after script loads
|
||||
const initializeYandexMetrika = () => {
|
||||
if (typeof window.ym === "function") {
|
||||
try {
|
||||
window.ym(YANDEX_METRIKA_ID, "init", {
|
||||
webvisor: true,
|
||||
clickmap: true,
|
||||
accurateTrackBounce: true,
|
||||
trackLinks: true,
|
||||
});
|
||||
} catch {
|
||||
// Silently handle initialization errors
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check if ym is already available or wait for it
|
||||
if (typeof window.ym === "function") {
|
||||
initializeYandexMetrika();
|
||||
} else {
|
||||
// Wait for script to load
|
||||
const checkYm = setInterval(() => {
|
||||
if (typeof window.ym === "function") {
|
||||
initializeYandexMetrika();
|
||||
clearInterval(checkYm);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Cleanup interval after 10 seconds
|
||||
setTimeout(() => {
|
||||
clearInterval(checkYm);
|
||||
}, 10000);
|
||||
|
||||
return () => clearInterval(checkYm);
|
||||
}
|
||||
}, []);
|
||||
import { YANDEX_METRIKA_ID } from "@/shared/constants/analytics";
|
||||
|
||||
/**
|
||||
* Yandex Metrika Integration Component
|
||||
*
|
||||
* Loads Yandex Metrika tracking script dynamically using static counter ID.
|
||||
*
|
||||
* Initializes with: clickmap, trackLinks, accurateTrackBounce, webvisor.
|
||||
* Page views are tracked by PageViewTracker component on route changes.
|
||||
*/
|
||||
export function YandexMetrika() {
|
||||
return (
|
||||
<>
|
||||
{/* Yandex.Metrika counter */}
|
||||
<Script
|
||||
id="yandex-metrika-site-wide"
|
||||
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://mc.yandex.ru/metrika/tag.js', 'ym');
|
||||
(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__ = ${YANDEX_METRIKA_ID};
|
||||
|
||||
ym(${YANDEX_METRIKA_ID}, "init", {
|
||||
clickmap: true,
|
||||
trackLinks: true,
|
||||
accurateTrackBounce: true,
|
||||
webvisor: true
|
||||
});
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Noscript fallback */}
|
||||
<noscript>
|
||||
<div>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={`https://mc.yandex.ru/watch/${YANDEX_METRIKA_ID}`}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "-9999px",
|
||||
}}
|
||||
style={{ position: "absolute", left: "-9999px" }}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1 +1 @@
|
||||
export { default } from "./YandexMetrika";
|
||||
export { YandexMetrika } from "./YandexMetrika";
|
||||
|
||||
2
src/components/analytics/index.ts
Normal file
2
src/components/analytics/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { PageViewTracker } from "./PageViewTracker";
|
||||
export { YandexMetrika } from "./YandexMetrika/YandexMetrika";
|
||||
24
src/providers/analytics-provider.tsx
Normal file
24
src/providers/analytics-provider.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { type ReactNode,useEffect } from "react";
|
||||
|
||||
import { analyticsService } from "@/services/analytics";
|
||||
|
||||
interface AnalyticsProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analytics Provider Component
|
||||
*
|
||||
* Initializes analytics service with user data when the app loads.
|
||||
* Provides analytics context to the entire application.
|
||||
*/
|
||||
export function AnalyticsProvider({ children }: AnalyticsProviderProps) {
|
||||
useEffect(() => {
|
||||
// Initialize analytics service
|
||||
analyticsService.initialize();
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
140
src/services/analytics/analyticsService.ts
Normal file
140
src/services/analytics/analyticsService.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import type { IMeResponse, IUser } from "@/entities/user/types";
|
||||
import { http } from "@/shared/api/httpClient";
|
||||
import { API_ROUTES } from "@/shared/constants/api-routes";
|
||||
|
||||
/**
|
||||
* Analytics Service
|
||||
*
|
||||
* Provides methods for tracking user parameters and events
|
||||
* in Yandex Metrika.
|
||||
*/
|
||||
export class AnalyticsService {
|
||||
private static instance: AnalyticsService;
|
||||
private user: IUser | null = null;
|
||||
private isInitialized = false;
|
||||
|
||||
static getInstance(): AnalyticsService {
|
||||
if (!AnalyticsService.instance) {
|
||||
AnalyticsService.instance = new AnalyticsService();
|
||||
}
|
||||
return AnalyticsService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize analytics service with user data
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await http.get<IMeResponse>(API_ROUTES.usersMe());
|
||||
this.user = response.user;
|
||||
|
||||
// Set user parameters in analytics
|
||||
await this.setUserParameters();
|
||||
|
||||
this.isInitialized = true;
|
||||
} catch {
|
||||
// Initialization failed silently
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user parameters in Yandex Metrika
|
||||
*/
|
||||
private async setUserParameters(): Promise<void> {
|
||||
if (!this.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userParams = this.extractUserParameters();
|
||||
|
||||
// Set parameters in Yandex Metrika
|
||||
if (typeof window.ym === "function" && window.__YM_COUNTER_ID__) {
|
||||
try {
|
||||
window.ym(window.__YM_COUNTER_ID__, "userParams", userParams);
|
||||
} catch {
|
||||
// Failed to set user parameters silently
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract relevant user parameters for analytics
|
||||
*/
|
||||
private extractUserParameters(): Record<string, unknown> {
|
||||
if (!this.user) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const params: Record<string, unknown> = {
|
||||
user_id: this.user._id,
|
||||
email: this.user.email,
|
||||
locale: this.user.locale,
|
||||
timezone: this.user.timezone,
|
||||
source: this.user.source,
|
||||
has_sign: this.user.sign,
|
||||
created_at: this.user.createdAt,
|
||||
};
|
||||
|
||||
// Add profile information if available
|
||||
if (this.user.profile) {
|
||||
params.profile_name = this.user.profile.name;
|
||||
params.profile_gender = this.user.profile.gender;
|
||||
params.profile_age = this.user.profile.age;
|
||||
params.profile_sign = this.user.profile.sign;
|
||||
params.profile_birthdate = this.user.profile.birthdate;
|
||||
}
|
||||
|
||||
// Add partner information if available
|
||||
if (this.user.partner) {
|
||||
params.partner_gender = this.user.partner.gender;
|
||||
params.partner_age = this.user.partner.age;
|
||||
params.partner_sign = this.user.partner.sign;
|
||||
params.partner_birthdate = this.user.partner.birthdate;
|
||||
}
|
||||
|
||||
// Add location information if available
|
||||
if (this.user.ipLookup) {
|
||||
params.country = this.user.ipLookup.country;
|
||||
params.region = this.user.ipLookup.region;
|
||||
params.city = this.user.ipLookup.city;
|
||||
params.is_eu = this.user.ipLookup.eu;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track custom event in Yandex Metrika
|
||||
*/
|
||||
trackEvent(eventName: string, parameters?: Record<string, unknown>): void {
|
||||
// Track in Yandex Metrika
|
||||
if (typeof window.ym === "function" && window.__YM_COUNTER_ID__) {
|
||||
try {
|
||||
window.ym(window.__YM_COUNTER_ID__, "reachGoal", eventName, parameters);
|
||||
} catch {
|
||||
// Failed to track event silently
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user data
|
||||
*/
|
||||
getUser(): IUser | null {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if service is initialized
|
||||
*/
|
||||
isReady(): boolean {
|
||||
return this.isInitialized;
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const analyticsService = AnalyticsService.getInstance();
|
||||
1
src/services/analytics/index.ts
Normal file
1
src/services/analytics/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { AnalyticsService,analyticsService } from "./analyticsService";
|
||||
1
src/shared/constants/analytics.ts
Normal file
1
src/shared/constants/analytics.ts
Normal file
@ -0,0 +1 @@
|
||||
export const YANDEX_METRIKA_ID = process.env.NEXT_PUBLIC_YM_ID || "103412914";
|
||||
@ -36,8 +36,8 @@ declare global {
|
||||
fbq: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
CollectJS: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
gtag: any;
|
||||
|
||||
__YM_COUNTER_ID__: number;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user