docs(changeset): feat(error-manager): implement centralized error reporting system
This commit is contained in:
194
src/ErrorBoundary/ErrorManager.ts
Normal file
194
src/ErrorBoundary/ErrorManager.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user