feat(errors): сбор клиентских (браузерных) ошибок в админ-вкладку «Ошибки»
Глобальный репортер в api.js (грузится на всех страницах) ловит необработанные JS-ошибки
(window 'error') и rejected-промисы ('unhandledrejection') в браузере пользователя и шлёт
в POST /api/client-errors. Дедуп по сигнатуре + лимит 15/страницу, только для залогиненных,
keepalive, не флудит и сам не падает.
Бэкенд: routes/clientErrors (auth + rate-limit 20/мин на юзера) → clientErrorController
пишет в общий error_log с level='client' (message/stack/route=url/method=kind/user_id),
поля обрезаются. Появляются в существующей админ-вкладке «Ошибки» с бейджем «БРАУЗЕР»
(фиолетовый акцент vs розовый у серверных). Тест client-errors.test.js 5/5.
lint:routes 0; node --check всех файлов.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1959,3 +1959,55 @@ async function adminGamGetUser(id) { return req('GET', `/gamifi
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
/* ── Глобальный репортер клиентских ошибок ───────────────────────────────
|
||||
Ловит необработанные JS-ошибки и rejected-промисы в браузере пользователя
|
||||
и шлёт в /api/client-errors → они появляются в админ-вкладке «Ошибки».
|
||||
Дедуп + лимит на загрузку страницы (не флудим), только для залогиненных. */
|
||||
(function initClientErrorReporter() {
|
||||
const seen = new Set();
|
||||
let sent = 0; const MAX_PER_PAGE = 15;
|
||||
let inFlight = false;
|
||||
|
||||
function send(payload) {
|
||||
try {
|
||||
if (!isLoggedIn()) return; // отчёты только от залогиненных
|
||||
if (sent >= MAX_PER_PAGE) return; // не флудим повторами
|
||||
const sig = (payload.message || '') + '|' + (payload.source || '') + ':' + (payload.line || '');
|
||||
if (seen.has(sig)) return;
|
||||
seen.add(sig); sent++;
|
||||
if (inFlight) return;
|
||||
inFlight = true;
|
||||
const token = getToken();
|
||||
fetch(API + '/client-errors', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json' }, token ? { Authorization: 'Bearer ' + token } : {}),
|
||||
body: JSON.stringify(payload),
|
||||
keepalive: true, // долетит даже при закрытии вкладки
|
||||
}).catch(function () {}).finally(function () { inFlight = false; });
|
||||
} catch (e) { inFlight = false; /* репортер не должен сам падать */ }
|
||||
}
|
||||
|
||||
window.addEventListener('error', function (e) {
|
||||
// Пропускаем ошибки загрузки ресурсов (img/script) — у них нет message/error.
|
||||
if (!e || (!e.message && !e.error)) return;
|
||||
send({
|
||||
kind: 'error',
|
||||
message: e.message || (e.error && (e.error.message || String(e.error))) || 'Script error',
|
||||
stack: e.error && e.error.stack ? String(e.error.stack) : null,
|
||||
source: e.filename || null, line: e.lineno || null, col: e.colno || null,
|
||||
url: location.pathname + location.search + location.hash,
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', function (e) {
|
||||
const r = e && e.reason;
|
||||
let msg = 'Unhandled promise rejection';
|
||||
let stack = null;
|
||||
if (r) {
|
||||
if (typeof r === 'string') msg = r;
|
||||
else { msg = r.message || (r.toString && r.toString()) || msg; stack = r.stack ? String(r.stack) : null; }
|
||||
}
|
||||
send({ kind: 'unhandledrejection', message: msg, stack: stack, url: location.pathname + location.search + location.hash });
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user