320 lines
10 KiB
TypeScript
320 lines
10 KiB
TypeScript
/**
|
||
* 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,
|
||
};
|