/** * DC-071: Error tracking integration framework * * Provides an opt-in error tracking interface that can forward uncaught * errors to external services (Sentry, Bugsnag, etc.) when configured. * * In production, set ERROR_TRACKING_DSN environment variable to enable. * Without a DSN, errors are logged normally but not forwarded. * * Usage: * const { errorTracker } = require('./utilities/error-tracker'); * errorTracker.init({ dsn: process.env.ERROR_TRACKING_DSN, release: '1.15.0' }); * errorTracker.capture(error, { extra: { route: req.path } }); */ const os = require('os'); class ErrorTracker { constructor() { this.dsn = null; this.release = null; this.enabled = false; this.pendingFlush = Promise.resolve(); } /** * Initialize the error tracker. * If no DSN is provided, tracking is disabled (errors still log normally). */ init({ dsn, release, environment } = {}) { this.dsn = dsn || process.env.ERROR_TRACKING_DSN; this.release = release || process.env.npm_package_version || 'unknown'; this.environment = environment || process.env.NODE_ENV || 'production'; this.enabled = !!this.dsn; return this.enabled; } /** * Capture an error and forward to the tracking service. * Non-blocking — swallows network errors silently. */ capture(error, context = {}) { if (!this.enabled || !error) return; const payload = { event_id: `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`, timestamp: new Date().toISOString(), platform: 'node', level: 'error', release: this.release, environment: this.environment, message: error.message || String(error), stacktrace: error.stack || '', exception: { type: error.constructor.name, value: error.message, }, tags: { hostname: os.hostname(), node_version: process.version, ...context.tags, }, extra: { pid: process.pid, memory: process.memoryUsage().rss, uptime: process.uptime(), ...context.extra, }, request: context.request || undefined, user: context.user || undefined, }; // Fire-and-forget — don't block the event loop this.pendingFlush = this._send(payload).catch(() => { // Silent failure — tracking errors should never crash the app }); return payload.event_id; } /** * Capture a message (not an error) at the specified level. */ captureMessage(message, level = 'info', context = {}) { if (!this.enabled) return; return this.capture( Object.assign(new Error(message), { stack: '' }), { ...context, tags: { ...context.tags, level } } ); } /** * Send the payload to the tracking service DSN. * Currently implements the Sentry envelope format. */ async _send(payload) { if (!this.dsn) return; const url = new URL(this.dsn); const projectId = url.pathname.replace(/^\//, ''); const apiKey = url.username; const ingestUrl = `${url.protocol}//${url.host}/api/${projectId}/store/`; const body = JSON.stringify(payload); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); try { const response = await fetch(ingestUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Sentry-Auth': `Sentry sentry_key=${apiKey}`, }, body, signal: controller.signal, }); if (!response.ok) { // Non-OK response — silently ignore } } finally { clearTimeout(timeout); } } /** * Wait for all pending events to flush. */ async flush(timeoutMs = 2000) { await Promise.race([ this.pendingFlush, new Promise(resolve => setTimeout(resolve, timeoutMs)), ]); } /** * Express error-handling middleware that captures errors before * forwarding to the next error handler. */ middleware() { return (err, req, res, next) => { this.capture(err, { request: { url: req.url, method: req.method, headers: req.headers, }, extra: { requestId: req.id, path: req.path, }, }); next(err); }; } } module.exports = new ErrorTracker();