mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
feat: implement centralized error handling and logging with ErrorBoundary component
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import React, { Component, ReactNode } from 'react'
|
||||
import { logError, LogLevel } from '@/lib/errors/log-error'
|
||||
import { Alert, Button, Box, Typography } from '@mui/material'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
fallback?: ReactNode
|
||||
onError?: (error: Error, errorInfo: React.ErrorInfo) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean
|
||||
error?: Error
|
||||
}
|
||||
|
||||
/**
|
||||
* Error Boundary component to catch React rendering errors
|
||||
* Logs errors and displays a fallback UI
|
||||
*/
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = { hasError: false }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||
// Log error with component stack
|
||||
logError(
|
||||
error,
|
||||
{
|
||||
component: 'ErrorBoundary',
|
||||
componentStack: errorInfo.componentStack,
|
||||
},
|
||||
LogLevel.ERROR
|
||||
)
|
||||
|
||||
// Call custom error handler if provided
|
||||
this.props.onError?.(error, errorInfo)
|
||||
}
|
||||
|
||||
handleReset = (): void => {
|
||||
this.setState({ hasError: false, error: undefined })
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.hasError) {
|
||||
// Use custom fallback if provided
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback
|
||||
}
|
||||
|
||||
// Default error UI
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Something went wrong
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||
{this.state.error?.message || 'An unexpected error occurred'}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={this.handleReset}
|
||||
size="small"
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</Alert>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
46
frontends/nextjs/src/lib/errors/format-error.ts
Normal file
46
frontends/nextjs/src/lib/errors/format-error.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { DBALError, DBALErrorCode } from '@/dbal/development/src/core/foundation/errors'
|
||||
import { getErrorMessage, isError } from '@/lib/types/guards'
|
||||
|
||||
export interface FormattedError {
|
||||
message: string
|
||||
code?: DBALErrorCode | number
|
||||
stack?: string
|
||||
details?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Format any error into a consistent structure
|
||||
*/
|
||||
export function formatError(error: unknown): FormattedError {
|
||||
// Handle DBALError
|
||||
if (error instanceof DBALError) {
|
||||
return {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
details: error.details,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle standard Error
|
||||
if (isError(error)) {
|
||||
return {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle error-like objects and other types
|
||||
return {
|
||||
message: getErrorMessage(error),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert error to user-friendly message (safe for display)
|
||||
*/
|
||||
export function toUserMessage(error: unknown): string {
|
||||
const formatted = formatError(error)
|
||||
|
||||
// Don't expose stack traces to users
|
||||
return formatted.message || 'An unexpected error occurred'
|
||||
}
|
||||
47
frontends/nextjs/src/lib/errors/handle-api-error.ts
Normal file
47
frontends/nextjs/src/lib/errors/handle-api-error.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { DBALError } from '@/dbal/development/src/core/foundation/errors'
|
||||
import { formatError } from './format-error'
|
||||
|
||||
export interface APIErrorResponse {
|
||||
error: string
|
||||
code?: number
|
||||
details?: Record<string, unknown>
|
||||
timestamp?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Centralized API error handler for consistent error responses
|
||||
* Maps DBALError codes to HTTP status codes and returns standardized error responses
|
||||
*/
|
||||
export function handleAPIError(error: unknown): NextResponse<APIErrorResponse> {
|
||||
const formatted = formatError(error)
|
||||
|
||||
// Determine HTTP status code
|
||||
let status = 500
|
||||
if (error instanceof DBALError) {
|
||||
// Map DBALErrorCode to HTTP status
|
||||
// Codes < 1000 are standard HTTP codes, use them directly
|
||||
status = error.code < 1000 ? error.code : 500
|
||||
}
|
||||
|
||||
// Create response
|
||||
const response: APIErrorResponse = {
|
||||
error: formatted.message,
|
||||
code: formatted.code,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
|
||||
// Add details in development only
|
||||
if (process.env.NODE_ENV === 'development' && formatted.details) {
|
||||
response.details = formatted.details
|
||||
}
|
||||
|
||||
// Log error server-side
|
||||
console.error('[API Error]', {
|
||||
message: formatted.message,
|
||||
code: formatted.code,
|
||||
stack: formatted.stack,
|
||||
})
|
||||
|
||||
return NextResponse.json(response, { status })
|
||||
}
|
||||
10
frontends/nextjs/src/lib/errors/index.ts
Normal file
10
frontends/nextjs/src/lib/errors/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { formatError, toUserMessage } from './format-error'
|
||||
export type { FormattedError } from './format-error'
|
||||
|
||||
export { handleAPIError } from './handle-api-error'
|
||||
export type { APIErrorResponse } from './handle-api-error'
|
||||
|
||||
export { logError, LogLevel } from './log-error'
|
||||
export type { LogContext } from './log-error'
|
||||
|
||||
export { ErrorBoundary } from '@/components/atoms/feedback/ErrorBoundary'
|
||||
57
frontends/nextjs/src/lib/errors/log-error.ts
Normal file
57
frontends/nextjs/src/lib/errors/log-error.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { formatError } from './format-error'
|
||||
|
||||
export enum LogLevel {
|
||||
ERROR = 'error',
|
||||
WARN = 'warn',
|
||||
INFO = 'info',
|
||||
DEBUG = 'debug',
|
||||
}
|
||||
|
||||
export interface LogContext {
|
||||
component?: string
|
||||
userId?: string
|
||||
action?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Centralized error logging with consistent format
|
||||
* Replaces scattered console.error calls throughout the codebase
|
||||
*/
|
||||
export function logError(
|
||||
error: unknown,
|
||||
context?: LogContext,
|
||||
level: LogLevel = LogLevel.ERROR
|
||||
): void {
|
||||
const formatted = formatError(error)
|
||||
|
||||
const logEntry = {
|
||||
level,
|
||||
timestamp: new Date().toISOString(),
|
||||
message: formatted.message,
|
||||
code: formatted.code,
|
||||
context,
|
||||
stack: formatted.stack,
|
||||
}
|
||||
|
||||
// Use appropriate console method
|
||||
switch (level) {
|
||||
case LogLevel.ERROR:
|
||||
console.error('[Error]', logEntry)
|
||||
break
|
||||
case LogLevel.WARN:
|
||||
console.warn('[Warning]', logEntry)
|
||||
break
|
||||
case LogLevel.INFO:
|
||||
console.log('[Info]', logEntry)
|
||||
break
|
||||
case LogLevel.DEBUG:
|
||||
console.log('[Debug]', logEntry)
|
||||
break
|
||||
}
|
||||
|
||||
// TODO: Send to error tracking service (Sentry, etc.)
|
||||
// if (process.env.NODE_ENV === 'production') {
|
||||
// sendToErrorTracker(logEntry)
|
||||
// }
|
||||
}
|
||||
Reference in New Issue
Block a user