/**
* ROIBooster Tracker v1.0
* Минимизированный трекер для идентификации возвратов пользователей
*/
(function() {
'use strict';
// ========== КОНФИГУРАЦИЯ ==========
const CONFIG = {
// API endpoint для отправки данных
apiEndpoint: 'https://tg.of-crimea.ru/bt/api.php',
// Задержка после загрузки страницы (мс)
startDelay: 3000,
// Отладочный режим (выводить в консоль)
debug: false,
// Отправлять данные только 1 раз за сессию (true/false)
sendOncePerSession: true,
// Названия скрытых полей в формах
fieldNames: {
yandex: 'ya_id',
fingerprint: 'fp_id'
},
// Ключи для sessionStorage
storageKeys: {
sent: 'rb_sent',
firstReferrer: 'rb_first_ref'
}
};
// ========== УТИЛИТЫ ==========
function log(...args) {
if (CONFIG.debug) {
console.log('[ROIBooster]', ...args);
}
}
function error(...args) {
if (CONFIG.debug) {
console.error('[ROIBooster ERROR]', ...args);
}
}
// ========== ГЕНЕРАТОР CANVAS FINGERPRINT ==========
async function generateCanvasFingerprint() {
try {
log('Генерация Canvas fingerprint...');
const canvas = document.createElement('canvas');
canvas.width = 240;
canvas.height = 60;
const ctx = canvas.getContext('2d');
// Множественные техники рендеринга
ctx.textBaseline = 'top';
ctx.font = '16px "Arial"';
// Цветной прямоугольник
ctx.fillStyle = '#f60';
ctx.fillRect(125, 1, 62, 20);
// Линейный градиент
const gradient = ctx.createLinearGradient(0, 0, 240, 60);
gradient.addColorStop(0, '#069');
gradient.addColorStop(0.5, '#f60');
gradient.addColorStop(1, '#0c9');
ctx.fillStyle = gradient;
ctx.fillText('ROIBooster', 2, 15);
// Эмоджи
ctx.font = '20px Arial';
ctx.fillText('🎯🔒', 2, 35);
// Полупрозрачный текст
ctx.font = '14px "Arial"';
ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
ctx.fillText('Fingerprint', 80, 38);
// Фигура с наложением
ctx.globalCompositeOperation = 'multiply';
ctx.fillStyle = 'rgb(255, 0, 255)';
ctx.beginPath();
ctx.arc(60, 30, 18, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
// Дуга с градиентной обводкой
ctx.globalCompositeOperation = 'source-over';
const strokeGradient = ctx.createRadialGradient(180, 30, 5, 180, 30, 20);
strokeGradient.addColorStop(0, '#ff0');
strokeGradient.addColorStop(1, '#00f');
ctx.strokeStyle = strokeGradient;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(180, 30, 15, 0, Math.PI * 1.5);
ctx.stroke();
// Получаем данные Canvas
const canvasData = canvas.toDataURL();
// Хешируем через SHA-256
const msgBuffer = new TextEncoder().encode(canvasData);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const fingerprint = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
log('Canvas fingerprint сгенерирован:', fingerprint);
return fingerprint;
} catch (err) {
error('Ошибка генерации fingerprint:', err);
return null;
}
}
// ========== ПОЛУЧЕНИЕ ПЕРВОГО REFERRER ==========
function getFirstReferrer() {
try {
// Проверяем sessionStorage
const stored = sessionStorage.getItem(CONFIG.storageKeys.firstReferrer);
if (stored) {
log('Первый referrer из sessionStorage:', stored);
return stored;
}
// Получаем текущий referrer
const currentRef = document.referrer;
// Проверяем - это внешний или внутренний переход
const currentDomain = window.location.hostname;
const isExternal = currentRef && !currentRef.includes(currentDomain);
if (isExternal) {
// Внешний referrer - сохраняем как первый
sessionStorage.setItem(CONFIG.storageKeys.firstReferrer, currentRef);
log('Сохранен первый внешний referrer:', currentRef);
return currentRef;
} else if (!currentRef) {
// Прямой заход (нет referrer)
const direct = 'direct';
sessionStorage.setItem(CONFIG.storageKeys.firstReferrer, direct);
log('Прямой заход (нет referrer)');
return direct;
} else {
// Внутренний переход - ищем сохраненный
log('Внутренний переход, первый referrer:', stored || 'direct');
return stored || 'direct';
}
} catch (err) {
error('Ошибка получения referrer:', err);
return document.referrer || 'direct';
}
}
// ========== ПРОВЕРКА: УЖЕ ОТПРАВЛЯЛИ ДАННЫЕ? ==========
function wasAlreadySent() {
try {
if (!CONFIG.sendOncePerSession) {
return false; // Отправляем всегда
}
const sent = sessionStorage.getItem(CONFIG.storageKeys.sent);
return sent === 'true';
} catch (err) {
error('Ошибка проверки отправки:', err);
return false;
}
}
// ========== ПОМЕТИТЬ КАК ОТПРАВЛЕННОЕ ==========
function markAsSent() {
try {
if (CONFIG.sendOncePerSession) {
sessionStorage.setItem(CONFIG.storageKeys.sent, 'true');
log('Помечено как отправленное в этой сессии');
}
} catch (err) {
error('Ошибка записи в sessionStorage:', err);
}
}
// ========== ПОЛУЧЕНИЕ YANDEX CLIENT ID ==========
function getYandexClientId() {
try {
// Ищем cookie _ym_uid
const matches = document.cookie.match(/_ym_uid=([^;]+)/);
const clientId = matches ? matches[1] : null;
log('Yandex ClientID:', clientId || 'не найден');
return clientId;
} catch (err) {
error('Ошибка получения Yandex ClientID:', err);
return null;
}
}
// ========== ДОБАВЛЕНИЕ СКРЫТЫХ ПОЛЕЙ В ФОРМЫ ==========
function injectHiddenFields(yandexId, fingerprint) {
try {
log('Добавление скрытых полей в формы...');
const forms = document.querySelectorAll('form');
let injectedCount = 0;
forms.forEach((form, index) => {
// Проверяем, не добавлены ли уже поля
if (form.querySelector(`input[name="${CONFIG.fieldNames.yandex}"]`)) {
return;
}
// Создаем скрытое поле для Yandex ClientID
const yandexInput = document.createElement('input');
yandexInput.type = 'hidden';
yandexInput.name = CONFIG.fieldNames.yandex;
yandexInput.value = yandexId || '';
form.appendChild(yandexInput);
// Создаем скрытое поле для Fingerprint
const fpInput = document.createElement('input');
fpInput.type = 'hidden';
fpInput.name = CONFIG.fieldNames.fingerprint;
fpInput.value = fingerprint || '';
form.appendChild(fpInput);
injectedCount++;
log(`Поля добавлены в форму #${index + 1}`, form);
});
log(`Всего обработано форм: ${injectedCount}`);
} catch (err) {
error('Ошибка добавления полей в формы:', err);
}
}
// ========== ОТПРАВКА ДАННЫХ НА СЕРВЕР ==========
async function sendToServer(yandexId, fingerprint) {
try {
// Проверяем - уже отправляли в этой сессии?
if (wasAlreadySent()) {
log('Данные уже отправлены в этой сессии, пропускаем');
return;
}
log('Отправка данных на сервер...');
// Получаем первый внешний referrer
const firstReferrer = getFirstReferrer();
const data = {
ya_id: yandexId,
fp_id: fingerprint,
url: window.location.href,
referrer: firstReferrer,
timestamp: Date.now()
};
log('Данные для отправки:', data);
// Используем fetch с keepalive для надежности
const response = await fetch(CONFIG.apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
keepalive: true
});
if (response.ok) {
log('Данные успешно отправлены');
markAsSent(); // Помечаем что отправили
} else {
error('Ошибка ответа сервера:', response.status);
}
} catch (err) {
error('Ошибка отправки на сервер:', err);
// Fallback: отправка через pixel
try {
const firstReferrer = getFirstReferrer();
const img = new Image();
const params = new URLSearchParams({
ya_id: yandexId || '',
fp_id: fingerprint || '',
url: window.location.href,
referrer: firstReferrer
});
img.src = `${CONFIG.apiEndpoint}?${params}`;
log('Использован fallback через pixel');
markAsSent();
} catch (pixelErr) {
error('Ошибка fallback отправки:', pixelErr);
}
}
}
// ========== НАБЛЮДАТЕЛЬ ЗА НОВЫМИ ФОРМАМИ ==========
function observeForms(yandexId, fingerprint) {
try {
log('Запуск наблюдателя за формами...');
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
// Проверяем, является ли добавленный элемент формой
if (node.nodeName === 'FORM') {
injectHiddenFields(yandexId, fingerprint);
}
// Проверяем, содержит ли добавленный элемент формы
if (node.querySelectorAll) {
const forms = node.querySelectorAll('form');
if (forms.length > 0) {
injectHiddenFields(yandexId, fingerprint);
}
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
log('Наблюдатель активен');
} catch (err) {
error('Ошибка запуска наблюдателя:', err);
}
}
// ========== ГЛАВНАЯ ФУНКЦИЯ ИНИЦИАЛИЗАЦИИ ==========
async function init() {
try {
log('=== ROIBooster Tracker запущен ===');
log(`Задержка старта: ${CONFIG.startDelay}ms`);
// Генерируем Canvas fingerprint
const fingerprint = await generateCanvasFingerprint();
// Получаем Yandex ClientID
const yandexId = getYandexClientId();
// Добавляем скрытые поля во все существующие формы
injectHiddenFields(yandexId, fingerprint);
// Запускаем наблюдатель за новыми формами
observeForms(yandexId, fingerprint);
// Отправляем данные на сервер (пассивный трекинг)
await sendToServer(yandexId, fingerprint);
log('=== Инициализация завершена ===');
log('Fingerprint:', fingerprint);
log('Yandex ClientID:', yandexId);
} catch (err) {
error('Критическая ошибка инициализации:', err);
}
}
// ========== ЗАПУСК ПОСЛЕ ЗАГРУЗКИ СТРАНИЦЫ ==========
if (document.readyState === 'loading') {
// Страница еще загружается
document.addEventListener('DOMContentLoaded', () => {
setTimeout(init, CONFIG.startDelay);
});
} else {
// Страница уже загружена
setTimeout(init, CONFIG.startDelay);
}
// Экспортируем для доступа извне (опционально)
window.ROIBooster = {
version: '1.0',
// Публичный метод для ручного запуска
init: init
};
})();