/** * 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;