Game-Server/API/utils/ErrorReporter.js
2026-03-10 13:06:33 -03:00

120 lines
4.6 KiB
JavaScript

/**
* GSO Error Reporter & Analytics (GDD Phase 3 — v3.2 lightweight implementation)
* Logs structured errors to file + provides in-memory analytics counters.
* Can be wired to external services (Sentry, Datadog) by replacing _sendToExternal().
*/
const fs = require('fs');
const path = require('path');
class ErrorReporter {
constructor(options = {}) {
this.logDir = options.logDir || path.join(__dirname, '../logs');
this.service = options.service || 'GameServer';
this.env = process.env.NODE_ENV || 'development';
this.counters = {}; // event_type → count
this.errors = []; // last 100 errors in memory
this.maxErrors = 100;
// Ensure logs directory exists
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
}
// ── Error capture ──────────────────────────────────────────────────────────
captureError(err, context = {}) {
const entry = {
ts: new Date().toISOString(),
service: this.service,
env: this.env,
message: err?.message || String(err),
stack: err?.stack || null,
context,
};
// Store in memory ring buffer
this.errors.push(entry);
if (this.errors.length > this.maxErrors) this.errors.shift();
// Write to daily log file (non-blocking)
const logFile = path.join(this.logDir, `errors-${new Date().toISOString().slice(0,10)}.log`);
const line = JSON.stringify(entry) + '\n';
fs.appendFile(logFile, line, () => {}); // fire-and-forget
// Increment counter
this.increment('errors.total');
// Hook for external service (Sentry, etc.)
this._sendToExternal(entry);
return entry;
}
// ── Analytics event tracking ───────────────────────────────────────────────
track(event, data = {}) {
this.increment(event);
if (this.env === 'development') {
const logFile = path.join(this.logDir, `analytics-${new Date().toISOString().slice(0,10)}.log`);
fs.appendFile(logFile, JSON.stringify({ ts: new Date().toISOString(), event, data }) + '\n', () => {});
}
}
increment(key) {
this.counters[key] = (this.counters[key] || 0) + 1;
}
// ── Metrics snapshot ───────────────────────────────────────────────────────
getMetrics() {
return {
service: this.service,
uptime: Math.round(process.uptime()),
memoryMB: Math.round(process.memoryUsage().heapUsed / 1048576),
counters: { ...this.counters },
recentErrors: this.errors.slice(-10).map(e => ({ ts: e.ts, message: e.message, context: e.context })),
};
}
// ── Express middleware ─────────────────────────────────────────────────────
requestMiddleware() {
return (req, res, next) => {
const start = Date.now();
this.increment('http.requests');
res.on('finish', () => {
const ms = Date.now() - start;
this.increment(`http.${res.statusCode >= 400 ? 'errors' : 'success'}`);
if (ms > 2000) this.track('http.slow_request', { path: req.path, ms });
});
next();
};
}
errorMiddleware() {
return (err, req, res, next) => {
this.captureError(err, { path: req.path, method: req.method });
res.status(500).json({ error: 'Internal server error' });
};
}
// ── Socket.IO event tracking helper ───────────────────────────────────────
trackSocketEvent(eventName, userId) {
this.increment(`socket.${eventName}`);
this.increment('socket.total');
}
// ── External service stub ──────────────────────────────────────────────────
_sendToExternal(entry) {
// Replace with: Sentry.captureException(new Error(entry.message))
// or: axios.post(process.env.ERROR_WEBHOOK_URL, entry)
// Currently: no-op in development; log to console in production
if (this.env === 'production') {
console.error('[ErrorReporter]', entry.message, entry.context);
}
}
}
// Singleton
const reporter = new ErrorReporter({ service: 'GameServer' });
module.exports = reporter;