Files
gridpilot.gg/apps/website/lib/infrastructure/EnhancedErrorReporter.ts
2025-12-31 21:29:56 +01:00

338 lines
9.8 KiB
TypeScript

/**
* Enhanced Error Reporter with user notifications and environment-specific handling
*/
import { ErrorReporter } from '../interfaces/ErrorReporter';
import { Logger } from '../interfaces/Logger';
import { ApiError } from '../api/base/ApiError';
import { connectionMonitor } from '../api/base/ApiConnectionMonitor';
// Import notification system (will be used if available)
let notificationSystem: any = null;
try {
// Dynamically import to avoid circular dependencies
import('@/components/notifications/NotificationProvider').then(module => {
notificationSystem = module;
}).catch(() => {
// Notification system not available yet
});
} catch {
// Silent fail - notification system may not be initialized
}
export interface EnhancedErrorReporterOptions {
/**
* Whether to show user-facing notifications
*/
showUserNotifications?: boolean;
/**
* Whether to log to console (always true in dev)
*/
logToConsole?: boolean;
/**
* Whether to report to external service (e.g., Sentry)
*/
reportToExternal?: boolean;
/**
* Custom error handler for specific error types
*/
customHandlers?: Record<string, (error: ApiError) => void>;
}
export class EnhancedErrorReporter implements ErrorReporter {
private options: EnhancedErrorReporterOptions;
private logger: Logger;
private errorBuffer: Array<{ error: ApiError; context: unknown }> = [];
private readonly MAX_BUFFER_SIZE = 50;
constructor(logger: Logger, options: EnhancedErrorReporterOptions = {}) {
this.logger = logger;
this.options = {
showUserNotifications: options.showUserNotifications ?? true,
logToConsole: options.logToConsole ?? true,
reportToExternal: options.reportToExternal ?? false,
customHandlers: options.customHandlers || {},
};
}
/**
* Main error reporting method
*/
report(error: Error, context?: unknown): void {
// Only handle ApiError instances for enhanced reporting
if (!(error instanceof ApiError)) {
// For non-API errors, use basic logging
if (this.options.logToConsole) {
console.error('Non-API Error:', error, context);
}
return;
}
// Add to buffer for potential batch reporting
this.addToBuffer(error, context);
// Log based on environment and severity
this.logError(error, context);
// Handle custom error types
this.handleCustomHandlers(error);
// Show user notifications if enabled
if (this.options.showUserNotifications) {
this.showUserNotification(error);
}
// Report to external services if configured
if (this.options.reportToExternal) {
this.reportToExternal(error, context);
}
// Update connection monitor
if (error.isConnectivityIssue()) {
connectionMonitor.recordFailure(error);
}
}
/**
* Log error with appropriate severity
*/
private logError(error: ApiError, context: unknown): void {
if (!this.options.logToConsole) return;
const isDev = process.env.NODE_ENV === 'development';
const severity = error.getSeverity();
const message = isDev ? error.getDeveloperMessage() : error.getUserMessage();
const contextObj = typeof context === 'object' && context !== null ? context : {};
const errorContextObj = typeof error.context === 'object' && error.context !== null ? error.context : {};
const logContext = {
...errorContextObj,
...contextObj,
type: error.type,
isRetryable: error.isRetryable(),
isConnectivity: error.isConnectivityIssue(),
};
if (severity === 'error') {
this.logger.error(message, error, logContext);
if (isDev) {
console.error(`[API-ERROR] ${message}`, { error, context: logContext });
}
} else if (severity === 'warn') {
this.logger.warn(message, logContext);
if (isDev) {
console.warn(`[API-WARN] ${message}`, { context: logContext });
}
} else {
this.logger.info(message, logContext);
if (isDev) {
console.log(`[API-INFO] ${message}`, { context: logContext });
}
}
}
/**
* Show user-facing notification
*/
private showUserNotification(error: ApiError): void {
const isDev = process.env.NODE_ENV === 'development';
// In development, we might want to show more details
if (isDev) {
// Use console notification in dev
console.log(`%c[USER-NOTIFICATION] ${error.getUserMessage()}`,
'background: #222; color: #bada55; padding: 4px 8px; border-radius: 4px;'
);
return;
}
// In production, use the notification system if available
// This is a deferred import to avoid circular dependencies
if (typeof window !== 'undefined') {
setTimeout(() => {
try {
// Try to access notification context if available
const notificationEvent = new CustomEvent('gridpilot-notification', {
detail: {
type: 'error',
title: this.getNotificationTitle(error),
message: error.getUserMessage(),
variant: error.isConnectivityIssue() ? 'modal' : 'toast',
autoDismiss: !error.isConnectivityIssue(),
}
});
window.dispatchEvent(notificationEvent);
} catch (e) {
// Fallback to browser alert if notification system unavailable
if (error.isConnectivityIssue()) {
console.warn('API Error:', error.getUserMessage());
}
}
}, 100);
}
}
/**
* Get appropriate notification title
*/
private getNotificationTitle(error: ApiError): string {
switch (error.type) {
case 'NETWORK_ERROR':
return 'Connection Lost';
case 'TIMEOUT_ERROR':
return 'Request Timed Out';
case 'AUTH_ERROR':
return 'Authentication Required';
case 'SERVER_ERROR':
return 'Server Error';
case 'RATE_LIMIT_ERROR':
return 'Rate Limit Reached';
default:
return 'Something Went Wrong';
}
}
/**
* Handle custom error type handlers
*/
private handleCustomHandlers(error: ApiError): void {
if (this.options.customHandlers && this.options.customHandlers[error.type]) {
try {
this.options.customHandlers[error.type]!(error);
} catch (handlerError) {
console.error('Custom error handler failed:', handlerError);
}
}
}
/**
* Report to external services (placeholder for future integration)
*/
private reportToExternal(error: ApiError, context: unknown): void {
// Placeholder for external error reporting (e.g., Sentry, LogRocket)
// In a real implementation, this would send to your error tracking service
if (process.env.NODE_ENV === 'development') {
const contextObj = typeof context === 'object' && context !== null ? context : {};
console.log('[EXTERNAL-REPORT] Would report:', {
type: error.type,
message: error.message,
context: { ...error.context, ...contextObj },
timestamp: new Date().toISOString(),
});
}
}
/**
* Add error to buffer for potential batch reporting
*/
private addToBuffer(error: ApiError, context: unknown): void {
this.errorBuffer.push({ error, context });
// Keep buffer size in check
if (this.errorBuffer.length > this.MAX_BUFFER_SIZE) {
this.errorBuffer.shift();
}
}
/**
* Get buffered errors
*/
getBufferedErrors(): Array<{ error: ApiError; context: unknown }> {
return [...this.errorBuffer];
}
/**
* Clear error buffer
*/
clearBuffer(): void {
this.errorBuffer = [];
}
/**
* Batch report buffered errors
*/
flush(): void {
if (this.errorBuffer.length === 0) return;
if (this.options.logToConsole) {
console.groupCollapsed(`[API-REPORT] Flushing ${this.errorBuffer.length} buffered errors`);
this.errorBuffer.forEach(({ error, context }) => {
console.log(`${error.type}: ${error.message}`, { error, context });
});
console.groupEnd();
}
// In production, this would batch send to external service
if (this.options.reportToExternal) {
const batch = this.errorBuffer.map(({ error, context }) => {
const contextObj = typeof context === 'object' && context !== null ? context : {};
return {
type: error.type,
message: error.message,
context: { ...error.context, ...contextObj },
timestamp: new Date().toISOString(),
};
});
console.log('[EXTERNAL-REPORT] Batch:', batch);
}
this.clearBuffer();
}
/**
* Update options dynamically
*/
updateOptions(newOptions: Partial<EnhancedErrorReporterOptions>): void {
this.options = { ...this.options, ...newOptions };
}
}
/**
* Global error reporter instance
*/
let globalReporter: EnhancedErrorReporter | null = null;
export function getGlobalErrorReporter(): EnhancedErrorReporter {
if (!globalReporter) {
// Import the console logger
const { ConsoleLogger } = require('./ConsoleLogger');
globalReporter = new EnhancedErrorReporter(new ConsoleLogger(), {
showUserNotifications: true,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
}
return globalReporter;
}
/**
* Helper function to report API errors easily
*/
export function reportApiError(
error: ApiError,
context?: unknown,
reporter?: EnhancedErrorReporter
): void {
const rep = reporter || getGlobalErrorReporter();
rep.report(error, context);
}
/**
* React hook for error reporting
*/
export function useErrorReporter() {
const reporter = getGlobalErrorReporter();
return {
report: (error: Error, context?: unknown) => reporter.report(error, context),
flush: () => reporter.flush(),
getBuffered: () => reporter.getBufferedErrors(),
updateOptions: (opts: Partial<EnhancedErrorReporterOptions>) => reporter.updateOptions(opts),
};
}