PRIX / src /utils /errors.ts
yamxxx1's picture
Upload 139 files
7da7e2c verified
Raw
History Blame
6.17 kB
/**
* Centralized Error Handling
* Standardizes error handling across the codebase
*/
import pino from 'pino'
const logger = pino({ level: process.env.LOG_LEVEL || 'info' })
/**
* Error severity levels
*/
export type ErrorSeverity = 'critical' | 'high' | 'medium' | 'low' | 'info'
/**
* Standardized error structure
*/
export interface PRIXError {
message: string
severity: ErrorSeverity
code: string
context?: Record<string, any>
cause?: Error
}
/**
* Error codes for different failure modes
*/
export const ErrorCodes = {
// AI/LLM errors
AI_RATE_LIMIT: 'AI_RATE_LIMIT',
AI_CONTEXT_TOO_LARGE: 'AI_CONTEXT_TOO_LARGE',
AI_TIMEOUT: 'AI_TIMEOUT',
AI_AUTHENTICATION: 'AI_AUTHENTICATION',
AI_SERVICE_UNAVAILABLE: 'AI_SERVICE_UNAVAILABLE',
// GitHub API errors
GITHUB_RATE_LIMIT: 'GITHUB_RATE_LIMIT',
GITHUB_AUTHENTICATION: 'GITHUB_AUTHENTICATION',
GITHUB_NOT_FOUND: 'GITHUB_NOT_FOUND',
GITHUB_CONFLICT: 'GITHUB_CONFLICT',
GITHUB_TIMEOUT: 'GITHUB_TIMEOUT',
// File system errors
FS_READ_ERROR: 'FS_READ_ERROR',
FS_WRITE_ERROR: 'FS_WRITE_ERROR',
FS_NOT_FOUND: 'FS_NOT_FOUND',
// Parse/analysis errors
PARSE_ERROR: 'PARSE_ERROR',
AST_ANALYSIS_ERROR: 'AST_ANALYSIS_ERROR',
PATCH_PARSE_ERROR: 'PATCH_PARSE_ERROR',
// Context errors
CONTEXT_MISSING: 'CONTEXT_MISSING',
INVALID_PAYLOAD: 'INVALID_PAYLOAD',
// Circuit breaker
CIRCUIT_OPEN: 'CIRCUIT_OPEN',
// Unknown
UNKNOWN: 'UNKNOWN'
} as const
/**
* Create a standardized error
*/
export const createError = (
message: string,
severity: ErrorSeverity,
code: string,
context?: Record<string, any>,
cause?: Error
): PRIXError => ({
message,
severity,
code,
context,
cause
})
/**
* Log error with appropriate level based on severity
*/
export const logError = (error: PRIXError): void => {
const logData = {
code: error.code,
severity: error.severity,
context: error.context,
cause: error.cause?.message
}
switch (error.severity) {
case 'critical':
logger.fatal(logData, error.message)
break
case 'high':
logger.error(logData, error.message)
break
case 'medium':
logger.warn(logData, error.message)
break
case 'low':
logger.info(logData, error.message)
break
case 'info':
logger.debug(logData, error.message)
break
}
}
/**
* Safe wrapper for async functions
* Standardizes error handling
*/
export const safeExecute = async <T>(
fn: () => Promise<T>,
errorCode: string,
severity: ErrorSeverity = 'medium',
context?: Record<string, any>,
fallback?: T
): Promise<T | undefined> => {
try {
return await fn()
} catch (error) {
const prixError = createError(
error instanceof Error ? error.message : String(error),
severity,
errorCode,
context,
error instanceof Error ? error : undefined
)
logError(prixError)
return fallback
}
}
/**
* Safe wrapper for sync functions
*/
export const safeExecuteSync = <T>(
fn: () => T,
errorCode: string,
severity: ErrorSeverity = 'medium',
context?: Record<string, any>,
fallback?: T
): T | undefined => {
try {
return fn()
} catch (error) {
const prixError = createError(
error instanceof Error ? error.message : String(error),
severity,
errorCode,
context,
error instanceof Error ? error : undefined
)
logError(prixError)
return fallback
}
}
/**
* Classify error from external service
*/
export const classifyError = (error: any): PRIXError => {
// AI API errors
if (error?.status === 429) {
return createError('AI rate limit exceeded', 'high', ErrorCodes.AI_RATE_LIMIT, { status: 429 })
}
if (error?.status === 413) {
return createError('AI context too large', 'high', ErrorCodes.AI_CONTEXT_TOO_LARGE, { status: 413 })
}
if (error?.code === 'ETIMEDOUT' || error?.code === 'ECONNABORTED') {
return createError('AI timeout', 'medium', ErrorCodes.AI_TIMEOUT, { code: error.code })
}
if (error?.status === 401) {
return createError('AI authentication failed', 'critical', ErrorCodes.AI_AUTHENTICATION, { status: 401 })
}
if (error?.status === 503 || error?.status === 502) {
return createError('AI service unavailable', 'high', ErrorCodes.AI_SERVICE_UNAVAILABLE, { status: error.status })
}
// GitHub API errors
if (error?.status === 403 && error?.message?.includes('rate limit')) {
return createError('GitHub rate limit exceeded', 'high', ErrorCodes.GITHUB_RATE_LIMIT, { status: 403 })
}
if (error?.status === 401) {
return createError('GitHub authentication failed', 'critical', ErrorCodes.GITHUB_AUTHENTICATION, { status: 401 })
}
if (error?.status === 404) {
return createError('GitHub resource not found', 'medium', ErrorCodes.GITHUB_NOT_FOUND, { status: 404 })
}
if (error?.status === 409) {
return createError('GitHub conflict', 'medium', ErrorCodes.GITHUB_CONFLICT, { status: 409 })
}
// Default
return createError(
error?.message || 'Unknown error',
'medium',
ErrorCodes.UNKNOWN,
{ originalError: error }
)
}
/**
* Retry configuration
*/
export interface RetryConfig {
maxRetries: number
baseDelay: number
maxDelay: number
retryableCodes: string[]
}
/**
* Default retry configuration
*/
export const defaultRetryConfig: RetryConfig = {
maxRetries: 3,
baseDelay: 1000,
maxDelay: 30000,
retryableCodes: [
ErrorCodes.AI_RATE_LIMIT,
ErrorCodes.AI_TIMEOUT,
ErrorCodes.AI_SERVICE_UNAVAILABLE,
ErrorCodes.GITHUB_RATE_LIMIT,
ErrorCodes.GITHUB_TIMEOUT
]
}
/**
* Check if error should be retried
*/
export const shouldRetry = (error: PRIXError, config: RetryConfig = defaultRetryConfig): boolean => {
return config.retryableCodes.includes(error.code)
}
/**
* Calculate delay for retry with exponential backoff
*/
export const calculateRetryDelay = (attempt: number, config: RetryConfig = defaultRetryConfig): number => {
const delay = Math.min(
config.baseDelay * Math.pow(2, attempt),
config.maxDelay
)
// Add jitter to prevent thundering herd
return delay + Math.random() * 1000
}