Files
oranguru/src/ErrorBoundary/ErrorManager.ts

195 lines
5.1 KiB
TypeScript

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<void> {
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;