w-funnel/src/services/analytics/metricService.ts
2025-10-30 02:43:16 +01:00

320 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Metric Service для Яндекс Метрики и Google Analytics
*
* Паттерн идентичен aura-webapp/src/services/metric/metricService.ts
*
* Основные методы:
* - setUserID: установить ID пользователя (для YM и GA)
* - userParams: отправить параметры ПОСЕТИТЕЛЯ (sessionId, email, age, gender и т.д.)
* - params: отправить параметры ВИЗИТА (AB тест варианты, источник и т.д.)
* - reachGoal: достижение цели
*/
/**
* Параметры посетителя (постоянные характеристики)
* Передаются через window.ym(counterId, "userParams", {...})
*
* Эти данные привязываются к ClientID и распространяются
* на всю историю визитов пользователя
*/
interface IUserParams {
UserID?: string | number;
sessionId?: string;
email?: string;
gender?: string;
age?: number;
partnerGender?: string;
partnerAge?: number;
}
/**
* Параметры визита (временные данные о визите)
* Передаются через window.ym(counterId, "params", {...})
*
* Эти данные привязываются к конкретному визиту
*/
interface IVisitParams {
email?: string;
userId?: string | number;
sessionId?: string;
gaId?: string; // Google Analytics Measurement ID (e.g., G-XXXXXXXXXX)
gaClientId?: string; // Google Analytics Client ID from _ga cookie
ymClientId?: string; // Yandex Metrika Client ID from _ym_uid cookie
funnelId?: string;
selectedProductId?: string;
[key: string]: string | number | boolean | undefined;
}
const checkIsAvailableYandexMetric = (): boolean => {
if (typeof window === 'undefined') return false;
if (typeof window.ym === 'undefined' || !window.__YM_COUNTER_ID__) {
console.warn('[MetricService] Yandex Metrika not initialized');
return false;
}
return true;
};
const checkIsAvailableGoogleAnalytics = (): boolean => {
if (typeof window === 'undefined') return false;
if (typeof window.gtag === 'undefined') {
console.warn('[MetricService] Google Analytics not initialized');
return false;
}
return true;
};
/**
* Установить ID пользователя
* Вызывается после авторизации
*/
const setUserID = (userId: string | number): void => {
console.log('[MetricService] setUserID:', userId);
// Yandex Metrika
if (checkIsAvailableYandexMetric() && window.__YM_COUNTER_ID__) {
window.ym(window.__YM_COUNTER_ID__, 'setUserID', String(userId));
console.log('✅ [Yandex Metrika] setUserID:', userId);
}
// Google Analytics
if (checkIsAvailableGoogleAnalytics()) {
window.gtag('config', 'GA_MEASUREMENT_ID', {
user_id: String(userId),
});
console.log('✅ [Google Analytics] setUserID:', userId);
}
};
/**
* Отправить параметры ПОСЕТИТЕЛЯ
*
* Эти параметры привязываются к ClientID и распространяются
* на всю историю визитов пользователя
*
* Примеры:
* - userParams({ sessionId: 'abc123' }) - при создании сессии
* - userParams({ email: 'user@example.com', UserID: 123 }) - после авторизации
* - userParams({ gender: 'female', age: 25 }) - после ввода данных
*/
const userParams = (parameters: Partial<IUserParams>): void => {
console.log('[MetricService] userParams:', parameters);
// Yandex Metrika
if (checkIsAvailableYandexMetric() && window.__YM_COUNTER_ID__) {
window.ym(window.__YM_COUNTER_ID__, 'userParams', parameters);
console.log('✅ [Yandex Metrika] userParams sent:', parameters);
}
// Google Analytics
if (checkIsAvailableGoogleAnalytics()) {
window.gtag('config', 'GA_MEASUREMENT_ID', {
send_page_view: false,
...parameters,
});
console.log('✅ [Google Analytics] config sent:', parameters);
}
};
/**
* Отправить параметры ВИЗИТА
*
* Эти параметры привязываются к конкретному визиту
*
* Примеры:
* - params({ ab_test_button: 'v1' }) - AB тест вариант
* - params({ source: 'facebook' }) - источник трафика
*/
const params = (parameters: IVisitParams): void => {
console.log('[MetricService] params:', parameters);
// Yandex Metrika
if (checkIsAvailableYandexMetric() && window.__YM_COUNTER_ID__) {
window.ym(window.__YM_COUNTER_ID__, 'params', parameters);
console.log('✅ [Yandex Metrika] params sent:', parameters);
}
};
/**
* Достижение цели
*/
const reachGoal = (goal: string, params?: Record<string, unknown>): void => {
console.log('[MetricService] reachGoal:', goal, params);
// Yandex Metrika
if (checkIsAvailableYandexMetric() && window.__YM_COUNTER_ID__) {
window.ym(window.__YM_COUNTER_ID__, 'reachGoal', goal, params);
console.log('✅ [Yandex Metrika] reachGoal sent:', goal);
}
// Google Analytics
if (checkIsAvailableGoogleAnalytics()) {
window.gtag('event', goal, params);
console.log('✅ [Google Analytics] event sent:', goal);
}
};
/**
* Отправляет данные из sessionData в userParams
* Использует те же данные что собираются через registrationFieldKey
* Дополнительно вычисляет age из birthdate полей
*/
const sendSessionDataToMetrics = (sessionData: Record<string, unknown>): void => {
if (Object.keys(sessionData).length === 0) return;
const metrics: Record<string, string | number> = {};
// Рекурсивная функция для извлечения всех полей
const extractFields = (obj: Record<string, unknown>, prefix = ''): void => {
Object.entries(obj).forEach(([key, value]) => {
const fieldKey = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === 'object' && !Array.isArray(value)) {
// Рекурсивно обрабатываем вложенные объекты
extractFields(value as Record<string, unknown>, fieldKey);
} else if (typeof value === 'string' || typeof value === 'number') {
// Для birthdate полей вычисляем age
if (key === 'birthdate' && typeof value === 'string') {
const age = calculateAge(value);
if (age) {
const ageKey = prefix ? `${prefix.split('.').pop()}Age` : 'age';
metrics[ageKey] = age;
}
}
// Добавляем все остальные поля как есть
metrics[key] = value;
}
});
};
extractFields(sessionData);
// Отправляем только если есть данные
if (Object.keys(metrics).length > 0) {
userParams(metrics as Partial<IUserParams>);
}
};
/**
* Вычисляет возраст из даты рождения
* @param birthdate - дата в формате YYYY-MM-DD HH:mm
*/
const calculateAge = (birthdate: string): number | undefined => {
try {
const date = new Date(birthdate);
if (isNaN(date.getTime())) return undefined;
const today = new Date();
let age = today.getFullYear() - date.getFullYear();
const monthDiff = today.getMonth() - date.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < date.getDate())) {
age--;
}
return age > 0 && age < 150 ? age : undefined;
} catch {
return undefined;
}
};
/**
* Извлекает Google Analytics Client ID из куки _ga
* Формат куки: GA1.1.XXXXXXXXXX.YYYYYYYYYY
* Возвращает: XXXXXXXXXX.YYYYYYYYYY
*/
const getGoogleAnalyticsClientId = (): string | undefined => {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return undefined;
}
try {
const gaCookie = document.cookie
.split('; ')
.find(row => row.startsWith('_ga='));
if (!gaCookie) return undefined;
const cookieValue = gaCookie.split('=')[1];
// Формат: GA1.1.XXXXXXXXXX.YYYYYYYYYY
// Извлекаем последние две части
const parts = cookieValue.split('.');
if (parts.length >= 3) {
return parts.slice(2).join('.');
}
return undefined;
} catch {
return undefined;
}
};
/**
* Извлекает Yandex Metrika Client ID из куки _ym_uid
* Возвращает значение напрямую
*/
const getYandexMetrikaClientId = (): string | undefined => {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return undefined;
}
try {
const ymCookie = document.cookie
.split('; ')
.find(row => row.startsWith('_ym_uid='));
if (!ymCookie) return undefined;
return ymCookie.split('=')[1];
} catch {
return undefined;
}
};
/**
* Отправляет контекст визита в Яндекс Метрику
* Это параметры визита (params), а не посетителя (userParams)
*
* Вызывается при:
* - Создании сессии (sessionId, funnelId, gaId, gaClientId)
* - Авторизации (email, userId)
* - Выборе продукта на экране оплаты (selectedProductId)
*/
const sendVisitContext = (context: Partial<IVisitParams>): void => {
// Автоматически добавляем GA Client ID и YM Client ID если их нет в контексте
const gaClientId = context.gaClientId || getGoogleAnalyticsClientId();
const ymClientId = context.ymClientId || getYandexMetrikaClientId();
const contextWithClientIds = {
...context,
...(gaClientId ? { gaClientId } : {}),
...(ymClientId ? { ymClientId } : {}),
};
console.log('[MetricService] sendVisitContext:', contextWithClientIds);
// Фильтруем undefined значения
const filteredContext = Object.entries(contextWithClientIds).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = value;
}
return acc;
}, {} as IVisitParams);
if (Object.keys(filteredContext).length > 0) {
params(filteredContext);
}
};
export const metricService = {
setUserID,
userParams,
params,
reachGoal,
sendSessionDataToMetrics,
sendVisitContext,
};