docs(changeset): feat(error-manager): implement centralized error reporting system
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React, { type PropsWithChildren } from 'react';
|
||||
|
||||
import errorManager from './ErrorManager';
|
||||
|
||||
interface ErrorBoundaryProps extends PropsWithChildren {
|
||||
namespace?: string;
|
||||
onReportClick?: () => void;
|
||||
@@ -43,7 +46,12 @@ export class ReactBasicErrorBoundary extends React.PureComponent<
|
||||
errorInfo,
|
||||
try: false,
|
||||
});
|
||||
// You can also log error messages to an error reporting service here
|
||||
|
||||
// Report error to error manager (Sentry, custom API, etc.)
|
||||
errorManager.reportError(error, errorInfo, {
|
||||
componentStack: errorInfo?.componentStack,
|
||||
namespace: this.props.namespace,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Button, Code, Collapse, Group, Paper, rem, Text } from '@mantine/core';
|
||||
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||
import React, { type PropsWithChildren } from 'react';
|
||||
|
||||
import errorManager from './ErrorManager';
|
||||
|
||||
let ErrorBoundaryOptions = {
|
||||
disabled: false,
|
||||
onError: undefined,
|
||||
@@ -68,7 +71,12 @@ export class ReactErrorBoundary extends React.Component<ErrorBoundaryProps, Erro
|
||||
if (typeof GetErrorBoundaryOptions()?.onError === 'function') {
|
||||
GetErrorBoundaryOptions()?.onError?.(error, errorInfo);
|
||||
}
|
||||
// You can also log error messages to an error reporting service here
|
||||
|
||||
// Report error to error manager (Sentry, custom API, etc.)
|
||||
errorManager.reportError(error, errorInfo, {
|
||||
componentStack: errorInfo?.componentStack,
|
||||
namespace: this.props.namespace,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -202,6 +210,19 @@ export class ReactErrorBoundary extends React.Component<ErrorBoundaryProps, Erro
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Manually report error if user clicks report button
|
||||
if (this.state.error && this.state.errorInfo) {
|
||||
await errorManager.reportError(this.state.error, this.state.errorInfo, {
|
||||
componentStack: this.state.errorInfo?.componentStack,
|
||||
namespace: this.props.namespace,
|
||||
tags: { reportedBy: 'user' },
|
||||
});
|
||||
|
||||
this.setState(() => ({
|
||||
reported: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
|
||||
166
src/ErrorBoundary/ErrorManager.README.md
Normal file
166
src/ErrorBoundary/ErrorManager.README.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# ErrorManager
|
||||
|
||||
Centralized error reporting for ErrorBoundary components.
|
||||
|
||||
## Setup
|
||||
|
||||
### Sentry Integration
|
||||
|
||||
```typescript
|
||||
import { errorManager } from './ErrorBoundary';
|
||||
|
||||
errorManager.configure({
|
||||
enabled: true,
|
||||
sentry: {
|
||||
dsn: 'https://your-sentry-dsn@sentry.io/project-id',
|
||||
environment: 'production',
|
||||
release: '1.0.0',
|
||||
sampleRate: 1.0,
|
||||
ignoreErrors: ['ResizeObserver loop limit exceeded'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Custom API Integration
|
||||
|
||||
```typescript
|
||||
errorManager.configure({
|
||||
enabled: true,
|
||||
customAPI: {
|
||||
endpoint: 'https://api.yourapp.com/errors',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer token',
|
||||
},
|
||||
transformPayload: (report) => ({
|
||||
message: report.error.message,
|
||||
stack: report.error.stack,
|
||||
level: report.severity,
|
||||
timestamp: report.timestamp,
|
||||
}),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Reporter
|
||||
|
||||
```typescript
|
||||
errorManager.configure({
|
||||
enabled: true,
|
||||
reporters: [
|
||||
{
|
||||
name: 'CustomLogger',
|
||||
isEnabled: () => true,
|
||||
captureError: async (report) => {
|
||||
console.error('Error:', report.error);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Multiple Reporters
|
||||
|
||||
```typescript
|
||||
errorManager.configure({
|
||||
enabled: true,
|
||||
sentry: { dsn: 'your-dsn' },
|
||||
customAPI: { endpoint: 'your-endpoint' },
|
||||
reporters: [customReporter],
|
||||
});
|
||||
```
|
||||
|
||||
## Hooks
|
||||
|
||||
### beforeReport
|
||||
|
||||
```typescript
|
||||
errorManager.configure({
|
||||
beforeReport: (report) => {
|
||||
// Filter errors
|
||||
if (report.error.message.includes('ResizeObserver')) {
|
||||
return null; // Skip reporting
|
||||
}
|
||||
|
||||
// Enrich with user data
|
||||
report.context = {
|
||||
...report.context,
|
||||
user: { id: getCurrentUserId() },
|
||||
tags: { feature: 'checkout' },
|
||||
};
|
||||
|
||||
return report;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### onReportSuccess / onReportFailure
|
||||
|
||||
```typescript
|
||||
errorManager.configure({
|
||||
onReportSuccess: (report) => {
|
||||
console.log('Error reported successfully');
|
||||
},
|
||||
onReportFailure: (error, report) => {
|
||||
console.error('Failed to report error:', error);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Manual Reporting
|
||||
|
||||
```typescript
|
||||
try {
|
||||
riskyOperation();
|
||||
} catch (error) {
|
||||
await errorManager.reportError(error as Error, null, {
|
||||
namespace: 'checkout',
|
||||
tags: { step: 'payment' },
|
||||
extra: { orderId: '123' },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Disable/Enable
|
||||
|
||||
```typescript
|
||||
// Disable reporting
|
||||
errorManager.configure({ enabled: false });
|
||||
|
||||
// Enable reporting
|
||||
errorManager.configure({ enabled: true });
|
||||
```
|
||||
|
||||
## ErrorBoundary Integration
|
||||
|
||||
Automatic - errors caught by `ReactErrorBoundary` or `ReactBasicErrorBoundary` are automatically reported.
|
||||
|
||||
Manual report button in `ReactErrorBoundary` UI also sends to ErrorManager.
|
||||
|
||||
## Install Sentry (optional)
|
||||
|
||||
```bash
|
||||
npm install @sentry/react
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
```typescript
|
||||
type ErrorSeverity = 'fatal' | 'error' | 'warning' | 'info' | 'debug';
|
||||
|
||||
interface ErrorContext {
|
||||
namespace?: string;
|
||||
componentStack?: string;
|
||||
user?: Record<string, any>;
|
||||
tags?: Record<string, string>;
|
||||
extra?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface ErrorReport {
|
||||
error: Error;
|
||||
errorInfo?: any;
|
||||
severity?: ErrorSeverity;
|
||||
context?: ErrorContext;
|
||||
timestamp?: number;
|
||||
}
|
||||
```
|
||||
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;
|
||||
50
src/ErrorBoundary/ErrorManager.types.ts
Normal file
50
src/ErrorBoundary/ErrorManager.types.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export interface CustomAPIConfig {
|
||||
endpoint: string;
|
||||
headers?: Record<string, string>;
|
||||
method?: 'POST' | 'PUT';
|
||||
transformPayload?: (report: ErrorReport) => any;
|
||||
}
|
||||
|
||||
export interface ErrorContext {
|
||||
componentStack?: string;
|
||||
extra?: Record<string, any>;
|
||||
namespace?: string;
|
||||
tags?: Record<string, string>;
|
||||
user?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ErrorManagerConfig {
|
||||
beforeReport?: (report: ErrorReport) => ErrorReport | null;
|
||||
customAPI?: CustomAPIConfig;
|
||||
enabled?: boolean;
|
||||
onReportFailure?: (error: Error, report: ErrorReport) => void;
|
||||
onReportSuccess?: (report: ErrorReport) => void;
|
||||
reporters?: ErrorReporter[];
|
||||
sentry?: SentryConfig;
|
||||
}
|
||||
|
||||
export interface ErrorReport {
|
||||
context?: ErrorContext;
|
||||
error: Error;
|
||||
errorInfo?: any;
|
||||
severity?: ErrorSeverity;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface ErrorReporter {
|
||||
captureError: (report: ErrorReport) => Promise<void> | void;
|
||||
isEnabled: () => boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type ErrorSeverity = 'debug' | 'error' | 'fatal' | 'info' | 'warning';
|
||||
|
||||
export interface SentryConfig {
|
||||
beforeSend?: (event: any) => any | null;
|
||||
dsn: string;
|
||||
environment?: string;
|
||||
ignoreErrors?: string[];
|
||||
release?: string;
|
||||
sampleRate?: number;
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
export { default as ReactBasicErrorBoundary } from './BasicErrorBoundary';
|
||||
export { default as ReactErrorBoundary } from './ErrorBoundary';
|
||||
export { default as errorManager } from './ErrorManager';
|
||||
export * from './ErrorManager.types';
|
||||
|
||||
Reference in New Issue
Block a user