/** * Свой учёт посетителей портала (spec 2026-07-13). Работает параллельно с Метрикой, * её не заменяет и не трогает. Пишем только ИМЯ экрана — без содержимого (ПДн не уходят). */ const sentOnce = new Set(); function send(event: string, extra: Record = {}): void { void fetch('/api/track', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', keepalive: true, body: JSON.stringify({ event, host: location.hostname, path: location.pathname, referrer: document.referrer || null, ...extra, }), }).catch(() => undefined); } /** Первый заход в портал + «человек шевельнулся». Идемпотентно. */ export function trackVisit(): void { if (sentOnce.has('view')) return; sentOnce.add('view'); send('view'); const events = ['scroll', 'touchstart', 'mousemove', 'keydown']; const onFirst = () => { if (!sentOnce.has('alive')) { sentOnce.add('alive'); send('alive'); } events.forEach((e) => window.removeEventListener(e, onFirst)); }; events.forEach((e) => window.addEventListener(e, onFirst, { once: true, passive: true })); } /** Открыт экран кабинета — только имя маршрута. */ export function trackScreen(screen: string): void { send('portal_screen', { screen }); } /** Открыт экран регистрации. */ export function trackRegisterOpen(): void { if (sentOnce.has('register_open')) return; sentOnce.add('register_open'); send('register_open'); }