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:
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
/* clientErrorController — приём ошибок из браузера пользователя.
|
||||
Пишем в общий error_log с level='client', чтобы они появились в админ-вкладке «Ошибки».
|
||||
Запись не должна ронять запрос — любые сбои глушим. */
|
||||
const db = require('../db/db');
|
||||
|
||||
const MAX_MSG = 1000, MAX_STACK = 4000, MAX_ROUTE = 400;
|
||||
const clamp = (v, n) => (v == null ? null : String(v).slice(0, n));
|
||||
|
||||
function report(req, res) {
|
||||
const b = req.body || {};
|
||||
const message = (clamp(b.message, MAX_MSG) || '').trim();
|
||||
if (!message) return res.status(400).json({ error: 'message required' });
|
||||
|
||||
const kind = b.kind === 'unhandledrejection' ? 'rejection' : 'error';
|
||||
const route = clamp(b.url || b.route, MAX_ROUTE);
|
||||
let stack = clamp(b.stack, MAX_STACK);
|
||||
// если стека нет — собираем источник:строка:колонка
|
||||
if (!stack && (b.source || b.line)) stack = `${b.source || ''}:${b.line || ''}:${b.col || ''}`;
|
||||
|
||||
try {
|
||||
db.prepare(
|
||||
'INSERT INTO error_log (level, message, stack, route, method, user_id) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
).run('client', message, stack, route, kind, req.user.id);
|
||||
} catch { /* лог не должен ломать ответ */ }
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
module.exports = { report };
|
||||
@@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const rateLimit = require('../middleware/rateLimit');
|
||||
const ctrl = require('../controllers/clientErrorController');
|
||||
|
||||
router.use(authMiddleware);
|
||||
// Не больше 20 отчётов в минуту с пользователя — защита от флуда циклящихся ошибок.
|
||||
router.post('/', rateLimit({ windowMs: 60_000, max: 20, byUser: true, message: 'Слишком много отчётов об ошибках' }), ctrl.report);
|
||||
|
||||
module.exports = router;
|
||||
@@ -199,6 +199,7 @@ app.use('/api/materials', require('./routes/materials'));
|
||||
app.use('/api/custom-sims', require('./routes/customSims'));
|
||||
app.use('/api/game', require('./routes/game'));
|
||||
app.use('/api/wishes', require('./routes/wishes'));
|
||||
app.use('/api/client-errors', require('./routes/clientErrors'));
|
||||
app.use('/api/prep', require('./routes/prep'));
|
||||
app.use('/api/dashboard', require('./routes/dashboard'));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user