feat: 新增客户端错误收集与admin监控接口
- POST /api/client-errors: 批量存储客户端错误(10分钟去重合并) - GET /api/client-errors: admin分页查看错误列表 - DELETE /api/client-errors: admin清空错误记录 - 72小时自动清理旧数据 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
const crypto = require('node:crypto');
|
||||
const { pool, requireAuth, requireAdmin } = require('./context');
|
||||
|
||||
const MAX_ERRORS_PER_PAGE = 50;
|
||||
const ERROR_RETENTION_HOURS = 72;
|
||||
|
||||
async function initTable(client) {
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS client_errors (
|
||||
id SERIAL PRIMARY KEY,
|
||||
message TEXT NOT NULL,
|
||||
stack TEXT,
|
||||
source VARCHAR(32),
|
||||
url TEXT,
|
||||
user_agent TEXT,
|
||||
session_id TEXT,
|
||||
user_id INTEGER,
|
||||
count INTEGER DEFAULT 1,
|
||||
first_seen TIMESTAMP DEFAULT NOW(),
|
||||
last_seen TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_client_errors_last_seen ON client_errors(last_seen DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_client_errors_user ON client_errors(user_id);
|
||||
`);
|
||||
}
|
||||
|
||||
async function storeError(client, error) {
|
||||
const normalizedMessage = error.message.slice(0, 1000);
|
||||
const { rows } = await client.query(
|
||||
`SELECT id, count FROM client_errors WHERE message = $1 AND url = $2 AND source = $3 AND last_seen > NOW() - INTERVAL '10 minutes'`,
|
||||
[normalizedMessage, error.url || '', error.source || 'manual']
|
||||
);
|
||||
if (rows.length > 0) {
|
||||
await client.query(
|
||||
`UPDATE client_errors SET count = count + 1, last_seen = NOW(), stack = COALESCE($1, stack) WHERE id = $2`,
|
||||
[error.stack, rows[0].id]
|
||||
);
|
||||
} else {
|
||||
await client.query(
|
||||
`INSERT INTO client_errors (message, stack, source, url, user_agent, session_id, user_id) VALUES ($1,$2,$3,$4,$5,$6,$7)`,
|
||||
[normalizedMessage, error.stack, error.source, error.url, error.userAgent, error.sessionId, error.userId || null]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function mountClientErrorRoutes(router) {
|
||||
router.post('/client-errors', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { errors } = req.body;
|
||||
if (!Array.isArray(errors) || !errors.length) return res.json({ ok: true });
|
||||
const { pool: db } = require('./context');
|
||||
const client = await db.connect();
|
||||
try {
|
||||
await initTable(client);
|
||||
for (const err of errors.slice(0, 10)) {
|
||||
await storeError(client, { ...err, userId: req.user?.id });
|
||||
}
|
||||
} finally { client.release(); }
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client-errors', requireAuth, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const page = Math.max(1, parseInt(req.query.page) || 1);
|
||||
const { pool: db } = require('./context');
|
||||
const client = await db.connect();
|
||||
try {
|
||||
await initTable(client);
|
||||
await client.query(`DELETE FROM client_errors WHERE last_seen < NOW() - INTERVAL '$1 hours'`, [ERROR_RETENTION_HOURS]);
|
||||
const { rows } = await client.query(
|
||||
`SELECT * FROM client_errors ORDER BY last_seen DESC LIMIT $1 OFFSET $2`,
|
||||
[MAX_ERRORS_PER_PAGE, (page - 1) * MAX_ERRORS_PER_PAGE]
|
||||
);
|
||||
const { rows: countRows } = await client.query('SELECT COUNT(*) as total FROM client_errors');
|
||||
res.json({ items: rows, total: parseInt(countRows[0]?.total) || 0, page, pageSize: MAX_ERRORS_PER_PAGE });
|
||||
} finally { client.release(); }
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/client-errors', requireAuth, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { pool: db } = require('./context');
|
||||
await db.query('DELETE FROM client_errors');
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
};
|
||||
+2
-1
@@ -17,7 +17,8 @@ const { registerConversationRoutes } = require('./conversations')
|
||||
const { registerReportRoutes } = require('./reports')
|
||||
const { registerAssetRoutes } = require('./assets')
|
||||
const { registerNotificationRoutes } = require('./notifications')
|
||||
const { registerDraftRoutes } = require('./drafts')
|
||||
const { registerDraftRoutes } = require('./drafts');
|
||||
const mountClientErrorRoutes = require('./clientErrors')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user