отле́чь
1. разг. устар. то же, что = cfg.MIN_DWELL;
var focusOk = document.visibilityState === 'visible' && (now - startedAt) >= cfg.MIN_AFTER_FOCUS;
var inputOk = firstInputAt ? (now - firstInputAt) >= cfg.MIN_AFTER_INPUT : false;
var twoKindsOk = !cfg.needTwoKinds || eventsKindCount >= 2;
var storageOk = cfg.storageStrict
? (lsOk && ssOk)
: (lsOk || ssOk || perf.paints.length > 0 || perf.lcp > 0);
var lcpOk = !cfg.needLCP || perf.lcp > 0 || perf.paints.length > 0 || isSafari;
if (profile === 'soft') {
// Soft: два допустимых пути.
// 1) trustedPath — был реальный input, требуем sinceFirstInputMs ≥ MIN_AFTER_INPUT
// 2) silentPath — тихий читатель словаря: paints+storage(+canvas если FP включён)
var canvasOk = !FP_ENABLED || canvasHash !== '';
var trustedPath = hadTrusted && inputOk;
var silentPath = canvasOk && (perf.paints.length > 0) && (lsOk || ssOk);
return dwellOk && focusOk && twoKindsOk && (trustedPath || silentPath);
}
return dwellOk && focusOk && inputOk && hadTrusted && twoKindsOk && storageOk && lcpOk;
}
// ================================================================
// Отправка отчёта
// ================================================================
var submitted = false;
function removeHooks() {
var hookEvents = ['click', 'keydown', 'touchstart', 'touchend'];
for (var h = 0; h < hookEvents.length; h++) {
removeEventListener(hookEvents[h], gatedSubmit, true);
}
}
function gatedSubmit(e) {
if (e.isTrusted === true) trySubmit();
}
var hookEvents = ['click', 'keydown', 'touchstart', 'touchend', 'mousemove', 'scroll', 'wheel'];
for (var h = 0; h < hookEvents.length; h++) {
addEventListener(hookEvents[h], gatedSubmit, true);
}
function buildReportBody() {
var eventsArr = [];
for (var k in eventsKind) {
if (eventsKind.hasOwnProperty(k)) eventsArr.push(k);
}
return {
challenge: tokenMeta ? tokenMeta.content : '',
clientCid: CLIENT_CID,
report: {
profile: profile,
hasTrusted: hadTrusted,
hadFocus: hadFocus,
minDwellMs: Math.round(performance.now() - startedAt),
sinceFirstInputMs: firstInputAt ? Math.round(performance.now() - firstInputAt) : -1,
perf: perf,
ls: !!lsOk,
ss: !!ssOk,
eventsKind: eventsArr.slice(0, 4),
is_webdriver: navigator.webdriver === true,
canvas_hash: canvasHash,
webgl_renderer: webglRenderer,
screenW: screen.width || 0,
screenH: screen.height || 0,
colorDepth: screen.colorDepth || 0,
touchPoints: navigator.maxTouchPoints || 0,
timezone: (function() { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch(e) { return ''; } })()
}
};
}
function trySubmit() {
if (submitted || !meets() || !tokenMeta || !SOLVE_URL) return;
submitted = true;
removeHooks();
// Токен одноразовый — ретраев нет. Если сервер отказал, второй запрос он
// всё равно отклонит как replay, только раздувая счётчик rate-limit.
var xhr = new XMLHttpRequest();
xhr.open('POST', SOLVE_URL, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.withCredentials = true;
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
try {
var json = JSON.parse(xhr.responseText);
if (json && json.ok) {
loadMetrika();
try {
window.dispatchEvent(new CustomEvent('botcheck:passed'));
} catch (e) {}
}
} catch (e) {}
};
xhr.send(JSON.stringify(buildReportBody()));
}
// Beacon-вариант: отправка отчёта при уходе со страницы (visibilitychange:hidden).
// Гарантированно доставляется браузером даже после закрытия вкладки.
// Ответ не получаем — Метрика на этой странице не загрузится, но сервер
// выпишет cookie js_pass=1 для следующего визита, и юзер не «потеряется»
// в статистике как тихий читатель.
function trySubmitBeacon() {
if (submitted || !meets() || !tokenMeta || !SOLVE_URL) return;
if (typeof navigator.sendBeacon !== 'function') return;
submitted = true;
removeHooks();
try {
var blob = new Blob([JSON.stringify(buildReportBody())], { type: 'application/json' });
navigator.sendBeacon(SOLVE_URL, blob);
} catch (e) {}
}
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'hidden') {
trySubmitBeacon();
}
}, true);
// Авто-попытка: цикл с проверкой meets() каждые 100ms, начиная с MIN_DWELL.
// Это позволяет тихим читателям отправить отчёт, как только все сигналы
// (canvas, paint, storage) собраны, без ожидания trusted-event.
function tryAutoSubmit() {
if (submitted) return;
if (meets()) {
trySubmit();
} else {
setTimeout(tryAutoSubmit, 100);
}
}
setTimeout(tryAutoSubmit, cfg.MIN_DWELL);
// ================================================================
// Debug-режим: ?bcdebug=1
// ================================================================
if (/[?&]bcdebug=1(&|$)/.test(location.search)) {
console.log('[ABM] profile:', profile, 'cfg:', cfg);
if (FP_ENABLED) {
console.log('[ABM] canvas_hash:', canvasHash, 'webgl:', webglRenderer);
}
setInterval(function () {
console.log('[ABM] paints=', perf.paints.length, 'lcp=', perf.lcp,
'ls=', lsOk, 'ss=', ssOk, 'trusted=', hadTrusted,
'events=', Object.keys(eventsKind), 'submitted=', submitted);
}, 1000);
}
})();