@flavoai/fastfold
Version:
Flavo frontend package
275 lines • 10 kB
JavaScript
// ============================================================================
// 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