mirror of
https://github.com/codeflash-ai/codeflash-internal.git
synced 2026-05-04 18:25:18 +00:00
417 lines
13 KiB
TypeScript
417 lines
13 KiB
TypeScript
import { Request, Response, NextFunction } from "express"
|
|
import * as Sentry from "@sentry/node"
|
|
import { logger, LogContext } from "../utils/logger.js"
|
|
|
|
// Simple error handling without categories
|
|
|
|
// Simple error classes
|
|
export class ApiError extends Error {
|
|
public readonly statusCode: number
|
|
public readonly isOperational: boolean
|
|
public readonly context?: Record<string, any>
|
|
|
|
constructor(
|
|
message: string,
|
|
statusCode: number = 500,
|
|
isOperational: boolean = true,
|
|
context?: Record<string, any>,
|
|
) {
|
|
super(message)
|
|
this.name = this.constructor.name
|
|
this.statusCode = statusCode
|
|
this.isOperational = isOperational
|
|
this.context = context
|
|
|
|
// Maintains proper stack trace for where our error was thrown
|
|
Error.captureStackTrace(this, this.constructor)
|
|
}
|
|
}
|
|
|
|
// Validation errors
|
|
export class ValidationError extends ApiError {
|
|
constructor(message: string, context?: Record<string, any>) {
|
|
super(message, 400, true, context)
|
|
}
|
|
}
|
|
|
|
// Authentication errors
|
|
export class AuthenticationError extends ApiError {
|
|
constructor(message: string = "Authentication failed", context?: Record<string, any>) {
|
|
super(message, 401, true, context)
|
|
}
|
|
}
|
|
|
|
// Authorization errors
|
|
export class AuthorizationError extends ApiError {
|
|
constructor(message: string = "Access denied", context?: Record<string, any>) {
|
|
super(message, 403, true, context)
|
|
}
|
|
}
|
|
|
|
// Not found errors
|
|
export class NotFoundError extends ApiError {
|
|
constructor(message: string = "Resource not found", context?: Record<string, any>) {
|
|
super(message, 404, true, context)
|
|
}
|
|
}
|
|
|
|
// Rate limit errors
|
|
export class RateLimitError extends ApiError {
|
|
constructor(message: string = "Rate limit exceeded", context?: Record<string, any>) {
|
|
super(message, 429, true, context)
|
|
}
|
|
}
|
|
|
|
// External service errors
|
|
export class ExternalServiceError extends ApiError {
|
|
constructor(service: string, message?: string, context?: Record<string, any>) {
|
|
super(message || `External service error: ${service}`, 502, true, { service, ...context })
|
|
}
|
|
}
|
|
|
|
// Database errors
|
|
export class DatabaseError extends ApiError {
|
|
constructor(message: string, context?: Record<string, any>) {
|
|
super(message, 500, false, context)
|
|
}
|
|
}
|
|
|
|
// Configuration errors
|
|
export class ConfigurationError extends ApiError {
|
|
constructor(message: string, context?: Record<string, any>) {
|
|
super(message, 500, false, context)
|
|
}
|
|
}
|
|
|
|
// Business logic errors
|
|
export class BusinessLogicError extends ApiError {
|
|
constructor(message: string, context?: Record<string, any>) {
|
|
super(message, 400, true, context)
|
|
}
|
|
}
|
|
|
|
// Error response interface
|
|
interface ErrorResponse {
|
|
error: {
|
|
message: string
|
|
code: string
|
|
statusCode: number
|
|
requestId?: string
|
|
timestamp: string
|
|
context?: Record<string, any>
|
|
}
|
|
}
|
|
|
|
// Global error handler class
|
|
export class GlobalErrorHandler {
|
|
/**
|
|
* Main error handling middleware
|
|
* Should be registered as the last middleware in the Express app
|
|
*/
|
|
static handle(error: Error, req: Request, res: Response, next: NextFunction): void {
|
|
// If response was already sent, delegate to default Express error handler
|
|
if (res.headersSent) {
|
|
return next(error)
|
|
}
|
|
|
|
// Create error context for logging
|
|
const errorContext: LogContext = {
|
|
requestId: req.requestId,
|
|
traceId: req.traceId,
|
|
userId: (req as any).userId,
|
|
endpoint: req.path,
|
|
operation: "error_handling",
|
|
}
|
|
|
|
// Handle different types of errors
|
|
// ==========================================
|
|
// ERROR TYPE CLASSIFICATION & SENTRY BEHAVIOR:
|
|
// ==========================================
|
|
//
|
|
// 1. ApiError (Custom Error Classes):
|
|
// - ValidationError, AuthenticationError, DatabaseError, etc.
|
|
// - Sentry: ✅ 5xx errors sent to Sentry, ❌ 4xx errors NOT sent
|
|
// - Production Logs: ERROR level with statusCode, duration, error details
|
|
// - Examples: DatabaseError(500) → Sentry, ValidationError(400) → No Sentry
|
|
//
|
|
// 2. Operational Errors (Expected Errors):
|
|
// - User input errors, business logic errors, expected failures
|
|
// - Sentry: ❌ NEVER sent to Sentry (expected behavior)
|
|
// - Production Logs: WARN level with basic error info
|
|
// - Examples: Invalid JSON, missing required fields, business rule violations
|
|
//
|
|
// 3. Programming Errors (Unexpected Errors):
|
|
// - Unhandled exceptions, bugs, infrastructure failures
|
|
// - Sentry: ✅ ALWAYS sent to Sentry (critical issues)
|
|
// - Production Logs: ERROR level with full stack trace and error details
|
|
// - Examples: Null pointer exceptions, unhandled promise rejections, system crashes
|
|
//
|
|
// PRODUCTION LOG SUMMARY:
|
|
// - ApiError 5xx: ERROR + Sentry (Critical infrastructure issues)
|
|
// - ApiError 4xx: ERROR + No Sentry (Expected client errors)
|
|
// - Operational: WARN + No Sentry (Expected business logic issues)
|
|
// - Programming: ERROR + Sentry (Unexpected system failures)
|
|
|
|
if (error instanceof ApiError) {
|
|
// Custom error classes (ValidationError, DatabaseError, etc.)
|
|
// Sentry: 5xx errors YES, 4xx errors NO
|
|
// Production: ERROR level with statusCode and error details
|
|
GlobalErrorHandler.handleApiError(error, req, res, errorContext)
|
|
} else if (GlobalErrorHandler.isOperationalError(error)) {
|
|
// Expected errors (user input, business logic)
|
|
// Sentry: NO (expected behavior)
|
|
// Production: WARN level with basic error info
|
|
GlobalErrorHandler.handleOperationalError(error, req, res, errorContext)
|
|
} else {
|
|
// Unexpected errors (bugs, infrastructure failures)
|
|
// Sentry: YES (critical issues)
|
|
// Production: ERROR level with full stack trace
|
|
GlobalErrorHandler.handleProgrammingError(error, req, res, errorContext)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle custom API errors (ValidationError, DatabaseError, etc.)
|
|
*
|
|
* SENTRY BEHAVIOR:
|
|
* - ✅ 5xx errors (500, 502, 503) → Sent to Sentry (critical infrastructure)
|
|
* - ❌ 4xx errors (400, 401, 403, 404, 429) → NOT sent to Sentry (expected client errors)
|
|
*
|
|
* PRODUCTION LOGS:
|
|
* - Level: ERROR
|
|
* - Content: timestamp, level, message, requestId, endpoint, userId, statusCode, duration, error details
|
|
* - Example: {"timestamp":"2024-01-01T10:00:00.000Z","level":"ERROR","message":"Database connection failed","requestId":"req-123","endpoint":"/cfapi/optimize","userId":"user-456","statusCode":500,"duration":2500,"error":{"name":"DatabaseError","message":"Connection timeout","stack":"..."}}
|
|
*/
|
|
private static handleApiError(
|
|
error: ApiError,
|
|
req: Request,
|
|
res: Response,
|
|
context: LogContext,
|
|
): void {
|
|
const errorResponse = GlobalErrorHandler.formatErrorResponse(error, req)
|
|
|
|
// Use errorWithSentry for critical errors, error for operational errors
|
|
if (error.statusCode >= 500 || !error.isOperational) {
|
|
logger.errorWithSentry(
|
|
`API Error: ${error.message}`,
|
|
context,
|
|
{
|
|
errorType: error.constructor.name,
|
|
statusCode: error.statusCode,
|
|
isOperational: error.isOperational,
|
|
context: error.context,
|
|
},
|
|
error,
|
|
)
|
|
} else {
|
|
logger.error(
|
|
`API Error: ${error.message}`,
|
|
context,
|
|
{
|
|
errorType: error.constructor.name,
|
|
statusCode: error.statusCode,
|
|
isOperational: error.isOperational,
|
|
context: error.context,
|
|
},
|
|
error,
|
|
)
|
|
}
|
|
|
|
// Send response
|
|
res.status(error.statusCode).json(errorResponse)
|
|
}
|
|
|
|
/**
|
|
* Handle operational errors (expected errors like user input validation)
|
|
*
|
|
* SENTRY BEHAVIOR:
|
|
* - ❌ NEVER sent to Sentry (these are expected user/business logic errors)
|
|
*
|
|
* PRODUCTION LOGS:
|
|
* - Level: WARN
|
|
* - Content: timestamp, level, message, requestId, endpoint, userId, errorType, statusCode
|
|
* - Example: {"timestamp":"2024-01-01T10:00:00.000Z","level":"WARN","message":"Operational Error: Invalid email format","requestId":"req-123","endpoint":"/cfapi/create-user","userId":"user-456","errorType":"ValidationError","statusCode":400}
|
|
*
|
|
* EXAMPLES:
|
|
* - Invalid JSON in request body
|
|
* - Missing required fields
|
|
* - Business rule violations
|
|
* - User permission issues
|
|
*/
|
|
private static handleOperationalError(
|
|
error: Error,
|
|
req: Request,
|
|
res: Response,
|
|
context: LogContext,
|
|
): void {
|
|
const apiError = new ApiError(error.message, 400, true)
|
|
const errorResponse = GlobalErrorHandler.formatErrorResponse(apiError, req)
|
|
|
|
// Log as warning for operational errors (no Sentry)
|
|
logger.warn(
|
|
`Operational Error: ${error.message}`,
|
|
context,
|
|
{
|
|
errorType: error.constructor.name,
|
|
statusCode: 400,
|
|
},
|
|
error,
|
|
)
|
|
|
|
res.status(400).json(errorResponse)
|
|
}
|
|
|
|
/**
|
|
* Handle programming errors (unexpected errors like bugs and infrastructure failures)
|
|
*
|
|
* SENTRY BEHAVIOR:
|
|
* - ✅ ALWAYS sent to Sentry (these are critical system issues that need immediate attention)
|
|
*
|
|
* PRODUCTION LOGS:
|
|
* - Level: ERROR
|
|
* - Content: timestamp, level, message, requestId, endpoint, userId, errorType, statusCode, isOperational, stack
|
|
* - Example: {"timestamp":"2024-01-01T10:00:00.000Z","level":"ERROR","message":"Programming Error: Cannot read property 'id' of undefined","requestId":"req-123","endpoint":"/cfapi/optimize","userId":"user-456","errorType":"TypeError","statusCode":500,"isOperational":false,"stack":"TypeError: Cannot read property 'id' of undefined\n at UserService.getUser (/app/services/user.js:45:12)\n at ..."}
|
|
*
|
|
* EXAMPLES:
|
|
* - Null pointer exceptions
|
|
* - Unhandled promise rejections
|
|
* - System crashes
|
|
* - Infrastructure failures
|
|
* - Memory leaks
|
|
* - Uncaught exceptions
|
|
*/
|
|
private static handleProgrammingError(
|
|
error: Error,
|
|
req: Request,
|
|
res: Response,
|
|
context: LogContext,
|
|
): void {
|
|
const apiError = new ApiError(
|
|
process.env.NODE_ENV === "production" ? "Internal server error" : error.message,
|
|
500,
|
|
false,
|
|
)
|
|
const errorResponse = GlobalErrorHandler.formatErrorResponse(apiError, req)
|
|
|
|
// Log as error for programming errors (always send to Sentry)
|
|
logger.errorWithSentry(
|
|
`Programming Error: ${error.message}`,
|
|
context,
|
|
{
|
|
errorType: error.constructor.name,
|
|
statusCode: 500,
|
|
isOperational: false,
|
|
stack: error.stack,
|
|
},
|
|
error,
|
|
)
|
|
|
|
res.status(500).json(errorResponse)
|
|
}
|
|
|
|
/**
|
|
* Format error response for API consumers
|
|
*/
|
|
private static formatErrorResponse(error: ApiError, req: Request): ErrorResponse {
|
|
return {
|
|
error: {
|
|
message: error.message,
|
|
code: error.constructor.name,
|
|
statusCode: error.statusCode,
|
|
requestId: req.requestId,
|
|
timestamp: new Date().toISOString(),
|
|
...(error.context && { context: error.context }),
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine if an error is operational (expected) or programming (unexpected)
|
|
*/
|
|
private static isOperationalError(error: Error): boolean {
|
|
if (error instanceof ApiError) {
|
|
return error.isOperational
|
|
}
|
|
|
|
// Common operational errors
|
|
const operationalErrors = [
|
|
"ValidationError",
|
|
"CastError",
|
|
"JsonWebTokenError",
|
|
"TokenExpiredError",
|
|
"MulterError",
|
|
]
|
|
|
|
return operationalErrors.includes(error.name)
|
|
}
|
|
|
|
/**
|
|
* Handle unhandled promise rejections
|
|
*
|
|
* SENTRY BEHAVIOR:
|
|
* - ✅ ALWAYS sent to Sentry for monitoring
|
|
*
|
|
* SYSTEM BEHAVIOR:
|
|
* - Does NOT exit the process
|
|
* - Unhandled rejections don't corrupt process state
|
|
* - Service continues running after logging
|
|
*/
|
|
static async handleUnhandledRejection(reason: any, promise: Promise<any>): Promise<void> {
|
|
const context: LogContext = {
|
|
operation: "unhandled_rejection",
|
|
}
|
|
|
|
const error = reason instanceof Error ? reason : new Error(String(reason))
|
|
|
|
logger.errorWithSentry(
|
|
"Unhandled Promise Rejection",
|
|
context,
|
|
{
|
|
reason: reason?.toString(),
|
|
promise: promise.toString(),
|
|
isOperational: false,
|
|
},
|
|
error,
|
|
)
|
|
|
|
// Do NOT exit - unhandled rejections don't corrupt process state
|
|
}
|
|
|
|
/**
|
|
* Handle uncaught exceptions
|
|
*
|
|
* SENTRY BEHAVIOR:
|
|
* - ✅ ALWAYS sent to Sentry for monitoring
|
|
*
|
|
* SYSTEM BEHAVIOR:
|
|
* - Does NOT exit the process
|
|
* - Modern Node.js error handling makes process exit less necessary
|
|
* - Service continues running after logging
|
|
*/
|
|
static async handleUncaughtException(error: Error): Promise<void> {
|
|
const context: LogContext = {
|
|
operation: "uncaught_exception",
|
|
}
|
|
|
|
logger.errorWithSentry(
|
|
"Uncaught Exception",
|
|
context,
|
|
{
|
|
stack: error.stack,
|
|
isOperational: false,
|
|
},
|
|
error,
|
|
)
|
|
|
|
// Do NOT exit - keep service running
|
|
}
|
|
}
|
|
|
|
// Convenience function for creating API errors
|
|
export function createApiError(
|
|
message: string,
|
|
statusCode: number = 500,
|
|
context?: Record<string, any>,
|
|
): ApiError {
|
|
return new ApiError(message, statusCode, true, context)
|
|
}
|