Files
Learn_System/backend/tests/client-errors.test.js
Maxim Dolgolyov 43df41287f 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>
2026-06-23 23:17:04 +03:00

68 lines
2.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* Integration: /api/client-errors — приём браузерных ошибок в error_log (level='client').
*/
const { describe, it, before, after } = require('node:test');
const assert = require('node:assert/strict');
const { app, inject, getToken, db, cleanup } = require('./setup');
app.use('/api/client-errors', require('../src/routes/clientErrors'));
after(() => cleanup());
describe('/api/client-errors', () => {
let student;
before(async () => { student = await getToken('student'); });
it('требует авторизацию (401)', async () => {
const res = await inject('POST', '/api/client-errors', { message: 'x' }, null);
assert.equal(res.status, 401);
});
it('пустое сообщение → 400', async () => {
const res = await inject('POST', '/api/client-errors', { message: ' ' }, student.token);
assert.equal(res.status, 400);
});
it('пишет ошибку в error_log с level=client', async () => {
const res = await inject('POST', '/api/client-errors', {
kind: 'error', message: 'TypeError: x is null',
stack: 'at foo (app.js:10:5)', source: '/js/app.js', line: 10, col: 5,
url: '/lab?sim=demo#x',
}, student.token);
assert.equal(res.status, 200);
assert.equal(res.body.ok, true);
const row = db.prepare(
"SELECT * FROM error_log WHERE level='client' AND user_id=? ORDER BY id DESC LIMIT 1"
).get(student.userId);
assert.ok(row, 'строка должна появиться');
assert.equal(row.message, 'TypeError: x is null');
assert.equal(row.route, '/lab?sim=demo#x');
assert.equal(row.method, 'error');
assert.match(row.stack, /app\.js:10:5/);
});
it('unhandledrejection → method=rejection, stack из source при отсутствии stack', async () => {
const res = await inject('POST', '/api/client-errors', {
kind: 'unhandledrejection', message: 'boom', source: '/js/x.js', line: 3, col: 1, url: '/dashboard',
}, student.token);
assert.equal(res.status, 200);
const row = db.prepare(
"SELECT * FROM error_log WHERE level='client' AND message='boom' ORDER BY id DESC LIMIT 1"
).get();
assert.equal(row.method, 'rejection');
assert.match(row.stack, /x\.js:3:1/);
});
it('длинные поля обрезаются (не падает)', async () => {
const res = await inject('POST', '/api/client-errors', {
message: 'M'.repeat(5000), stack: 'S'.repeat(20000), url: 'U'.repeat(2000),
}, student.token);
assert.equal(res.status, 200);
const row = db.prepare("SELECT * FROM error_log WHERE level='client' ORDER BY id DESC LIMIT 1").get();
assert.ok(row.message.length <= 1000);
assert.ok(row.stack.length <= 4000);
assert.ok(row.route.length <= 400);
});
});