import type { CustomAPIConfig, ErrorManagerConfig, ErrorReport, ErrorReporter, SentryConfig, } from './ErrorManager.types'; class ErrorManager { private config: ErrorManagerConfig = { enabled: true }; private reporters: ErrorReporter[] = []; private sentryInstance: any = null; configure(config: ErrorManagerConfig) { this.config = { ...this.config, ...config }; this.reporters = []; if (config.sentry) { this.setupSentry(config.sentry); } if (config.customAPI) { this.setupCustomAPI(config.customAPI); } if (config.reporters) { this.reporters.push(...config.reporters); } } destroy() { this.reporters = []; this.sentryInstance = null; this.config = { enabled: true }; } getReporters(): ErrorReporter[] { return [...this.reporters]; } isEnabled(): boolean { return Boolean(this.config.enabled); } async reportError( error: Error, errorInfo?: any, context?: ErrorReport['context'] ): Promise { if (!this.config.enabled) { return; } let report: ErrorReport = { context, error, errorInfo, severity: 'error', timestamp: Date.now(), }; if (this.config.beforeReport) { const modifiedReport = this.config.beforeReport(report); if (!modifiedReport) { return; } report = modifiedReport; } const reportPromises = this.reporters .filter((reporter) => reporter.isEnabled()) .map(async (reporter) => { try { await reporter.captureError(report); } catch (error) { console.error(`Error reporter "${reporter.name}" failed:`, error); if (this.config.onReportFailure) { this.config.onReportFailure(error as Error, report); } } }); try { await Promise.all(reportPromises); if (this.config.onReportSuccess) { this.config.onReportSuccess(report); } } catch (error) { console.error('Error reporting failed:', error); } } private setupCustomAPI(config: CustomAPIConfig) { const customAPIReporter: ErrorReporter = { captureError: async (report: ErrorReport) => { try { const payload = config.transformPayload ? config.transformPayload(report) : { context: report.context, message: report.error.message, severity: report.severity || 'error', stack: report.error.stack, timestamp: report.timestamp || Date.now(), }; const response = await fetch(config.endpoint, { body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json', ...config.headers, }, method: config.method || 'POST', }); if (!response.ok) { throw new Error(`API request failed: ${response.statusText}`); } } catch (error) { console.error('Failed to send error to custom API:', error); } }, isEnabled: () => Boolean(config.endpoint), name: 'CustomAPI', }; this.reporters.push(customAPIReporter); } private setupSentry(config: SentryConfig) { const sentryReporter: ErrorReporter = { captureError: async (report: ErrorReport) => { if (!this.sentryInstance) { try { const Sentry = await import('@sentry/react'); Sentry.init({ beforeSend: config.beforeSend, dsn: config.dsn, environment: config.environment, ignoreErrors: config.ignoreErrors, release: config.release, tracesSampleRate: config.sampleRate || 1.0, }); this.sentryInstance = Sentry; } catch (error) { console.error('Failed to initialize Sentry:', error); return; } } // eslint-disable-next-line @typescript-eslint/no-explicit-any this.sentryInstance.withScope((scope: any) => { if (report.severity) { scope.setLevel(report.severity); } if (report.context?.namespace) { scope.setTag('namespace', report.context.namespace); } if (report.context?.tags) { Object.entries(report.context.tags).forEach(([key, value]) => { scope.setTag(key, value); }); } if (report.context?.user) { scope.setUser(report.context.user); } if (report.context?.extra) { scope.setExtras(report.context.extra); } if (report.context?.componentStack) { scope.setContext('react', { componentStack: report.context.componentStack, }); } this.sentryInstance.captureException(report.error); }); }, isEnabled: () => Boolean(this.sentryInstance), name: 'Sentry', }; this.reporters.push(sentryReporter); } } export const errorManager = new ErrorManager(); export default errorManager;