UNPKG

@flavoai/fastfold

Version:

Flavo frontend package

275 lines 10 kB
// ============================================================================ // FASTFOLD SERVER OBSERVABILITY - Unified Error Tracking // ============================================================================ import crypto from 'crypto'; // ============================================================================ // ERROR MIDDLEWARE // ============================================================================ /** * Creates Express error middleware that captures backend errors * and sends them to the observability collector. * * @example * import { observabilityErrorMiddleware } from '@flavoai/fastfold'; * * app.use(observabilityErrorMiddleware({ * enabled: true, * appId: process.env.APP_ID, * endpoint: 'https://superbuilder.app/api/observe' * })); */ export function observabilityErrorMiddleware(config) { if (!config.enabled || config.trackErrors === false) { // Return a no-op middleware if disabled return (_err, _req, _res, next) => { next(_err); }; } return (err, req, res, next) => { // Extract session/visitor IDs from headers (set by Fastfold client) const sessionId = req.headers['x-session-id'] || 'unknown'; const visitorId = req.headers['x-visitor-id'] || 'unknown'; const userId = req.user?.id; // Determine status code const statusCode = res.statusCode && res.statusCode >= 400 ? res.statusCode : 500; const event = { event_id: crypto.randomUUID(), app_id: config.appId, session_id: sessionId, visitor_id: visitorId, user_id: userId, event_type: 'error_backend', timestamp: new Date().toISOString(), url: req.originalUrl || req.url, user_agent: req.headers['user-agent'] || '', error: { message: err.message, stack: err.stack, endpoint: req.originalUrl || req.url, status_code: statusCode, severity: 'error', category: 'backend', }, }; // Fire and forget to collector (don't block response) sendToCollector(config.endpoint, [event]).catch(() => { // Silently fail - observability should not break the app }); // Continue to next error handler next(err); }; } /** * Send events to the observability collector */ async function sendToCollector(endpoint, events) { try { await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(events), }); } catch { // Silently fail - observability should not break the app } } // ============================================================================ // UNIFIED ERROR TRACKING // ============================================================================ // Global config reference for standalone tracking functions let globalConfig = null; /** * Initialize observability config for standalone tracking * Call this once during app initialization */ export function initObservability(config) { globalConfig = config; } /** * Get the current observability config */ export function getObservabilityConfig() { return globalConfig; } /** * Unified error tracking function - works for all error types * * @example * // AI error * trackError(error, 'ai', { * provider: 'openai', * model: 'gpt-4o', * operation: 'chat', * latencyMs: 1234 * }); * * // Integration error * trackError(error, 'integration', { * integration: 'stripe', * operation: 'createPaymentIntent', * customerId: 'cus_123' * }); * * // Generic backend error * trackError(error, 'backend', { endpoint: '/api/users' }); */ export function trackError(error, category, metadata, context) { const severity = classifyErrorSeverity(error, category, metadata); const statusCode = getErrorStatusCode(error); const endpoint = context?.endpoint || metadata?.operation ? `/api/${category}/${metadata?.operation || 'unknown'}` : '/unknown'; // Always log locally for debugging const logPrefix = metadata?.integration || metadata?.provider || category; const logOperation = metadata?.operation || ''; console.error(`[${severity.toUpperCase()}] [${logPrefix}${logOperation ? '/' + logOperation : ''}] ${error.message}`, metadata ? { ...metadata } : undefined); // Send to collector if configured if (!globalConfig?.enabled) { return; } const event = { event_id: crypto.randomUUID(), app_id: globalConfig.appId, session_id: context?.sessionId || 'unknown', visitor_id: context?.visitorId || 'unknown', user_id: context?.userId, event_type: `error_${category}`, timestamp: new Date().toISOString(), url: endpoint, user_agent: context?.userAgent || '', error: { message: error.message, stack: error.stack, endpoint, status_code: statusCode, severity, category, }, metadata, }; // Fire and forget sendToCollector(globalConfig.endpoint, [event]).catch(() => { }); } /** * Classify error severity based on error message and category */ function classifyErrorSeverity(error, category, metadata) { const message = error.message.toLowerCase(); // Critical: authentication, quota, billing, permission issues if (message.includes('authentication') || message.includes('unauthorized') || message.includes('invalid api key') || message.includes('invalid key') || message.includes('quota exceeded') || message.includes('billing') || message.includes('insufficient_quota') || message.includes('forbidden') || message.includes('permission denied')) { return 'critical'; } // Warning: rate limiting, temporary issues, validation errors if (message.includes('rate limit') || message.includes('too many requests') || message.includes('overloaded') || message.includes('capacity') || message.includes('timeout') || message.includes('temporarily') || category === 'validation') { return 'warning'; } // Default: error return 'error'; } /** * Map errors to HTTP-like status codes for consistency */ function getErrorStatusCode(error) { const message = error.message.toLowerCase(); if (message.includes('authentication') || message.includes('invalid api key') || message.includes('unauthorized')) { return 401; } if (message.includes('forbidden') || message.includes('permission')) { return 403; } if (message.includes('not found')) { return 404; } if (message.includes('validation') || message.includes('invalid')) { return 400; } if (message.includes('rate limit') || message.includes('too many requests')) { return 429; } if (message.includes('timeout') || message.includes('deadline exceeded')) { return 504; } if (message.includes('quota') || message.includes('billing')) { return 402; // Payment Required } return 500; } // ============================================================================ // CONVENIENCE ALIASES (for backward compatibility and clarity) // ============================================================================ /** * Track AI-specific errors (convenience wrapper) */ export function trackAIError(error, metadata, context) { trackError(error, 'ai', metadata, context); } /** * Track integration errors (convenience wrapper) */ export function trackIntegrationError(error, integration, operation, metadata, context) { trackError(error, 'integration', { ...metadata, integration, operation }, context); } /** * Optional middleware to track all requests (for debugging/analytics) * Note: This generates a lot of data, use carefully in production */ export function observabilityRequestMiddleware(config) { if (!config.enabled || !config.trackRequests) { return (_req, _res, next) => next(); } const excludePaths = config.excludePaths || ['/health', '/docs', '/api/observe']; return (req, res, next) => { // Skip excluded paths if (excludePaths.some(path => req.path.startsWith(path))) { return next(); } const startTime = Date.now(); // Track response res.on('finish', () => { const duration = Date.now() - startTime; const sessionId = req.headers['x-session-id'] || 'unknown'; const visitorId = req.headers['x-visitor-id'] || 'unknown'; // Only track errors (4xx, 5xx) or optionally all requests if (res.statusCode >= 400) { const event = { event_id: crypto.randomUUID(), app_id: config.appId, session_id: sessionId, visitor_id: visitorId, user_id: req.user?.id, event_type: 'error_backend', timestamp: new Date().toISOString(), url: req.originalUrl || req.url, user_agent: req.headers['user-agent'] || '', error: { message: `HTTP ${res.statusCode} - ${req.method} ${req.path}`, endpoint: req.originalUrl || req.url, status_code: res.statusCode, severity: res.statusCode >= 500 ? 'error' : 'warning', category: 'backend', }, }; sendToCollector(config.endpoint, [event]).catch(() => { }); } }); next(); }; } //# sourceMappingURL=observability.js.map