186 lines
5.1 KiB
TypeScript
186 lines
5.1 KiB
TypeScript
/**
|
|
* Error Reporting Utilities
|
|
*
|
|
* Centralized error logging and reporting for the application.
|
|
* Integrates with external services like Sentry, LogRocket, etc.
|
|
*/
|
|
|
|
import { apiClient } from '../api/client';
|
|
|
|
export interface ErrorReport {
|
|
error: Error | string;
|
|
context?: string;
|
|
userId?: string;
|
|
metadata?: Record<string, any>;
|
|
severity?: 'low' | 'medium' | 'high' | 'critical';
|
|
timestamp: string;
|
|
}
|
|
|
|
/**
|
|
* Report an error to monitoring services
|
|
*/
|
|
export const reportError = (report: ErrorReport): void => {
|
|
try {
|
|
// Log to console in development
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.group(`🚨 Error Report [${report.severity || 'medium'}]`);
|
|
console.error('Error:', report.error);
|
|
console.log('Context:', report.context);
|
|
console.log('User:', report.userId);
|
|
console.log('Metadata:', report.metadata);
|
|
console.log('Timestamp:', report.timestamp);
|
|
console.groupEnd();
|
|
}
|
|
|
|
// Send to Sentry (if configured)
|
|
if (typeof window !== 'undefined' && (window as any).Sentry) {
|
|
const Sentry = (window as any).Sentry;
|
|
|
|
if (report.error instanceof Error) {
|
|
Sentry.captureException(report.error, {
|
|
level: report.severity || 'error',
|
|
tags: {
|
|
context: report.context,
|
|
},
|
|
user: report.userId ? { id: report.userId } : undefined,
|
|
extra: report.metadata,
|
|
});
|
|
} else {
|
|
Sentry.captureMessage(report.error, {
|
|
level: report.severity || 'error',
|
|
tags: {
|
|
context: report.context,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
// Send to backend logging endpoint
|
|
sendToBackend(report);
|
|
} catch (e) {
|
|
console.error('Failed to report error:', e);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Send error to backend logging endpoint
|
|
*/
|
|
const sendToBackend = async (report: ErrorReport): Promise<void> => {
|
|
try {
|
|
// Only send in production or if explicitly enabled
|
|
if (process.env.NODE_ENV === 'production' || process.env.REACT_APP_ENABLE_ERROR_REPORTING === 'true') {
|
|
await apiClient.post('/api/log-error', {
|
|
error_message: report.error instanceof Error ? report.error.message : report.error,
|
|
error_stack: report.error instanceof Error ? report.error.stack : undefined,
|
|
context: report.context,
|
|
user_id: report.userId,
|
|
metadata: report.metadata,
|
|
severity: report.severity,
|
|
timestamp: report.timestamp,
|
|
user_agent: navigator.userAgent,
|
|
url: window.location.href,
|
|
});
|
|
}
|
|
} catch (e) {
|
|
// Fail silently - don't want error reporting to cause more errors
|
|
console.warn('Failed to send error to backend:', e);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Track error for analytics
|
|
*/
|
|
export const trackError = (
|
|
errorType: string,
|
|
message: string,
|
|
metadata?: Record<string, any>
|
|
): void => {
|
|
try {
|
|
// Track in analytics (Google Analytics, Mixpanel, etc.)
|
|
if (typeof window !== 'undefined' && (window as any).gtag) {
|
|
(window as any).gtag('event', 'exception', {
|
|
description: `${errorType}: ${message}`,
|
|
fatal: false,
|
|
...metadata,
|
|
});
|
|
}
|
|
|
|
// Log to console
|
|
console.warn(`📊 Error Tracked: ${errorType}`, message, metadata);
|
|
} catch (e) {
|
|
console.error('Failed to track error:', e);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Helper to determine if error is retryable
|
|
*/
|
|
export const isRetryableError = (error: unknown): boolean => {
|
|
if (error instanceof Error) {
|
|
const message = error.message.toLowerCase();
|
|
|
|
// Network errors are typically retryable
|
|
if (
|
|
message.includes('network') ||
|
|
message.includes('timeout') ||
|
|
message.includes('fetch') ||
|
|
message.includes('connection')
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
// 5xx errors are retryable
|
|
if (message.includes('500') || message.includes('502') || message.includes('503')) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Helper to sanitize error messages for user display
|
|
*/
|
|
export const sanitizeErrorMessage = (error: unknown): string => {
|
|
if (error instanceof Error) {
|
|
const message = error.message;
|
|
|
|
// Remove technical details from user-facing messages
|
|
if (message.includes('ECONNREFUSED')) {
|
|
return 'Unable to connect to server. Please check your connection.';
|
|
}
|
|
|
|
if (message.includes('401') || message.includes('unauthorized')) {
|
|
return 'Authentication failed. Please sign in again.';
|
|
}
|
|
|
|
if (message.includes('403') || message.includes('forbidden')) {
|
|
return 'You do not have permission to access this resource.';
|
|
}
|
|
|
|
if (message.includes('404')) {
|
|
return 'The requested resource was not found.';
|
|
}
|
|
|
|
if (message.includes('429')) {
|
|
return 'Too many requests. Please wait a moment and try again.';
|
|
}
|
|
|
|
if (message.includes('500') || message.includes('502') || message.includes('503')) {
|
|
return 'Server error occurred. Please try again later.';
|
|
}
|
|
|
|
// Return original message if no sanitization needed
|
|
return message;
|
|
}
|
|
|
|
if (typeof error === 'string') {
|
|
return error;
|
|
}
|
|
|
|
return 'An unexpected error occurred';
|
|
};
|
|
|
|
export default reportError;
|
|
|