Implement proper error boundaries to prevent app crashes
Frontend Error Boundaries: - Created ErrorBoundary class for centralized error handling - Wraps critical functions (transcribe, translate, TTS) with error boundaries - Global error handlers for unhandled errors and promise rejections - Component-specific error recovery with fallback functions - User-friendly error notifications with auto-dismiss - Error logging to backend for monitoring - Prevents cascading failures from component errors Backend Error Handling: - Added error boundary decorator for Flask routes - Global Flask error handlers (404, 500, generic exceptions) - Frontend error logging endpoint (/api/log-error) - Structured error responses with component information - Full traceback logging for debugging - Production vs development error message handling Features: - Graceful degradation when components fail - Automatic error recovery attempts - Error history tracking (last 50 errors) - Component-specific error handling - Production error monitoring ready - Prevents full app crashes from isolated errors 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0c9186e57e
commit
3804897e2b
86
app.py
86
app.py
@ -15,11 +15,33 @@ from cryptography.hazmat.primitives.asymmetric import ec
|
|||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
import gc # For garbage collection
|
import gc # For garbage collection
|
||||||
|
from functools import wraps
|
||||||
|
import traceback
|
||||||
|
|
||||||
# Initialize logging
|
# Initialize logging
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Error boundary decorator for Flask routes
|
||||||
|
def with_error_boundary(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
# Log the full exception with traceback
|
||||||
|
logger.error(f"Error in {func.__name__}: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
# Return appropriate error response
|
||||||
|
error_message = str(e) if app.debug else "An internal error occurred"
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': error_message,
|
||||||
|
'component': func.__name__
|
||||||
|
}), 500
|
||||||
|
return wrapper
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['UPLOAD_FOLDER'] = tempfile.mkdtemp()
|
app.config['UPLOAD_FOLDER'] = tempfile.mkdtemp()
|
||||||
app.config['TTS_SERVER'] = os.environ.get('TTS_SERVER_URL', 'http://localhost:5050/v1/audio/speech')
|
app.config['TTS_SERVER'] = os.environ.get('TTS_SERVER_URL', 'http://localhost:5050/v1/audio/speech')
|
||||||
@ -623,6 +645,29 @@ def get_audio(filename):
|
|||||||
logger.error(f"Audio retrieval error: {str(e)}")
|
logger.error(f"Audio retrieval error: {str(e)}")
|
||||||
return jsonify({'error': f'Audio retrieval failed: {str(e)}'}), 500
|
return jsonify({'error': f'Audio retrieval failed: {str(e)}'}), 500
|
||||||
|
|
||||||
|
# Error logging endpoint for frontend error reporting
|
||||||
|
@app.route('/api/log-error', methods=['POST'])
|
||||||
|
def log_error():
|
||||||
|
"""Log frontend errors for monitoring"""
|
||||||
|
try:
|
||||||
|
error_data = request.json
|
||||||
|
error_info = error_data.get('errorInfo', {})
|
||||||
|
error_details = error_data.get('error', {})
|
||||||
|
|
||||||
|
# Log the error
|
||||||
|
logger.error(f"Frontend error in {error_info.get('component', 'unknown')}: {error_details.get('message', 'No message')}")
|
||||||
|
logger.error(f"Stack trace: {error_details.get('stack', 'No stack trace')}")
|
||||||
|
logger.error(f"User agent: {error_info.get('userAgent', 'Unknown')}")
|
||||||
|
logger.error(f"URL: {error_info.get('url', 'Unknown')}")
|
||||||
|
|
||||||
|
# In production, you might want to send this to a monitoring service
|
||||||
|
# like Sentry, LogRocket, or your own analytics
|
||||||
|
|
||||||
|
return jsonify({'success': True})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to log frontend error: {str(e)}")
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
# Health check endpoints for monitoring
|
# Health check endpoints for monitoring
|
||||||
@app.route('/health', methods=['GET'])
|
@app.route('/health', methods=['GET'])
|
||||||
def health_check():
|
def health_check():
|
||||||
@ -741,5 +786,46 @@ app.request_count = 0
|
|||||||
def before_request():
|
def before_request():
|
||||||
app.request_count = getattr(app, 'request_count', 0) + 1
|
app.request_count = getattr(app, 'request_count', 0) + 1
|
||||||
|
|
||||||
|
# Global error handlers
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def not_found_error(error):
|
||||||
|
logger.warning(f"404 error: {request.url}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Resource not found',
|
||||||
|
'status': 404
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def internal_error(error):
|
||||||
|
logger.error(f"500 error: {str(error)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Internal server error',
|
||||||
|
'status': 500
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.errorhandler(Exception)
|
||||||
|
def handle_exception(error):
|
||||||
|
# Log the error
|
||||||
|
logger.error(f"Unhandled exception: {str(error)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
# Return JSON instead of HTML for HTTP errors
|
||||||
|
if hasattr(error, 'code'):
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': str(error),
|
||||||
|
'status': error.code
|
||||||
|
}), error.code
|
||||||
|
|
||||||
|
# Non-HTTP exceptions
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': 'An unexpected error occurred',
|
||||||
|
'status': 500
|
||||||
|
}), 500
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=5005, debug=True)
|
app.run(host='0.0.0.0', port=5005, debug=True)
|
||||||
|
@ -16,18 +16,60 @@ import {
|
|||||||
} from './types';
|
} from './types';
|
||||||
import { TranslationCache } from './translationCache';
|
import { TranslationCache } from './translationCache';
|
||||||
import { RequestQueueManager } from './requestQueue';
|
import { RequestQueueManager } from './requestQueue';
|
||||||
|
import { ErrorBoundary } from './errorBoundary';
|
||||||
|
|
||||||
|
// Initialize error boundary
|
||||||
|
const errorBoundary = ErrorBoundary.getInstance();
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Set up global error handler
|
||||||
|
errorBoundary.setGlobalErrorHandler((error, errorInfo) => {
|
||||||
|
console.error('Global error caught:', error);
|
||||||
|
|
||||||
|
// Show user-friendly message based on component
|
||||||
|
if (errorInfo.component === 'transcription') {
|
||||||
|
const statusIndicator = document.getElementById('statusIndicator');
|
||||||
|
if (statusIndicator) {
|
||||||
|
statusIndicator.textContent = 'Transcription failed. Please try again.';
|
||||||
|
statusIndicator.classList.add('text-danger');
|
||||||
|
}
|
||||||
|
} else if (errorInfo.component === 'translation') {
|
||||||
|
const translatedText = document.getElementById('translatedText');
|
||||||
|
if (translatedText) {
|
||||||
|
translatedText.innerHTML = '<p class="text-danger">Translation failed. Please try again.</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wrap initialization functions with error boundaries
|
||||||
|
const safeRegisterServiceWorker = errorBoundary.wrapAsync(
|
||||||
|
registerServiceWorker,
|
||||||
|
'service-worker',
|
||||||
|
async () => console.warn('Service worker registration failed, continuing without PWA features')
|
||||||
|
);
|
||||||
|
|
||||||
|
const safeInitApp = errorBoundary.wrap(
|
||||||
|
initApp,
|
||||||
|
'app-init',
|
||||||
|
() => console.error('App initialization failed')
|
||||||
|
);
|
||||||
|
|
||||||
|
const safeInitInstallPrompt = errorBoundary.wrap(
|
||||||
|
initInstallPrompt,
|
||||||
|
'install-prompt',
|
||||||
|
() => console.warn('Install prompt initialization failed')
|
||||||
|
);
|
||||||
|
|
||||||
// Register service worker
|
// Register service worker
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
registerServiceWorker();
|
safeRegisterServiceWorker();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize app
|
// Initialize app
|
||||||
initApp();
|
safeInitApp();
|
||||||
|
|
||||||
// Check for PWA installation prompts
|
// Check for PWA installation prompts
|
||||||
initInstallPrompt();
|
safeInitInstallPrompt();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Service Worker Registration
|
// Service Worker Registration
|
||||||
@ -356,7 +398,7 @@ function initApp(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Function to transcribe audio
|
// Function to transcribe audio
|
||||||
async function transcribeAudio(audioBlob: Blob): Promise<void> {
|
const transcribeAudioBase = async function(audioBlob: Blob): Promise<void> {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('audio', audioBlob, 'audio.webm'); // Add filename for better server handling
|
formData.append('audio', audioBlob, 'audio.webm'); // Add filename for better server handling
|
||||||
formData.append('source_lang', sourceLanguage.value);
|
formData.append('source_lang', sourceLanguage.value);
|
||||||
@ -438,10 +480,24 @@ function initApp(): void {
|
|||||||
statusIndicator.textContent = 'Transcription failed';
|
statusIndicator.textContent = 'Transcription failed';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// Wrap transcribe function with error boundary
|
||||||
|
const transcribeAudio = errorBoundary.wrapAsync(
|
||||||
|
transcribeAudioBase,
|
||||||
|
'transcription',
|
||||||
|
async () => {
|
||||||
|
hideProgress();
|
||||||
|
hideLoadingOverlay();
|
||||||
|
sourceText.innerHTML = '<p class="text-danger">Transcription failed. Please try again.</p>';
|
||||||
|
statusIndicator.textContent = 'Transcription error - please retry';
|
||||||
|
statusIndicator.classList.remove('processing');
|
||||||
|
statusIndicator.classList.add('error');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Translate button click event
|
// Translate button click event
|
||||||
translateBtn.addEventListener('click', async function() {
|
translateBtn.addEventListener('click', errorBoundary.wrapAsync(async function() {
|
||||||
if (!currentSourceText) {
|
if (!currentSourceText) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -556,7 +612,12 @@ function initApp(): void {
|
|||||||
statusIndicator.textContent = 'Translation failed';
|
statusIndicator.textContent = 'Translation failed';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}, 'translation', async () => {
|
||||||
|
hideProgress();
|
||||||
|
hideLoadingOverlay();
|
||||||
|
translatedText.innerHTML = '<p class="text-danger">Translation failed. Please try again.</p>';
|
||||||
|
statusIndicator.textContent = 'Translation error - please retry';
|
||||||
|
}));
|
||||||
|
|
||||||
// Play source text
|
// Play source text
|
||||||
playSource.addEventListener('click', function() {
|
playSource.addEventListener('click', function() {
|
||||||
@ -575,7 +636,7 @@ function initApp(): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Function to play audio via TTS
|
// Function to play audio via TTS
|
||||||
async function playAudio(text: string, language: string): Promise<void> {
|
const playAudioBase = async function(text: string, language: string): Promise<void> {
|
||||||
showProgress();
|
showProgress();
|
||||||
showLoadingOverlay('Generating audio...');
|
showLoadingOverlay('Generating audio...');
|
||||||
|
|
||||||
@ -654,7 +715,19 @@ function initApp(): void {
|
|||||||
ttsServerMessage.textContent = 'Failed to connect to TTS server';
|
ttsServerMessage.textContent = 'Failed to connect to TTS server';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// Wrap playAudio function with error boundary
|
||||||
|
const playAudio = errorBoundary.wrapAsync(
|
||||||
|
playAudioBase,
|
||||||
|
'tts',
|
||||||
|
async () => {
|
||||||
|
hideProgress();
|
||||||
|
hideLoadingOverlay();
|
||||||
|
statusIndicator.textContent = 'Audio playback failed';
|
||||||
|
alert('Failed to generate audio. Please check your TTS server connection.');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Clear buttons
|
// Clear buttons
|
||||||
clearSource.addEventListener('click', function() {
|
clearSource.addEventListener('click', function() {
|
||||||
|
286
static/js/src/errorBoundary.ts
Normal file
286
static/js/src/errorBoundary.ts
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
// Error boundary implementation for better error handling
|
||||||
|
export interface ErrorInfo {
|
||||||
|
message: string;
|
||||||
|
stack?: string;
|
||||||
|
component?: string;
|
||||||
|
timestamp: number;
|
||||||
|
userAgent: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary {
|
||||||
|
private static instance: ErrorBoundary;
|
||||||
|
private errorLog: ErrorInfo[] = [];
|
||||||
|
private maxErrorLog = 50;
|
||||||
|
private errorHandlers: Map<string, (error: Error, errorInfo: ErrorInfo) => void> = new Map();
|
||||||
|
private globalErrorHandler: ((error: Error, errorInfo: ErrorInfo) => void) | null = null;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.setupGlobalErrorHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): ErrorBoundary {
|
||||||
|
if (!ErrorBoundary.instance) {
|
||||||
|
ErrorBoundary.instance = new ErrorBoundary();
|
||||||
|
}
|
||||||
|
return ErrorBoundary.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupGlobalErrorHandlers(): void {
|
||||||
|
// Handle unhandled errors
|
||||||
|
window.addEventListener('error', (event: ErrorEvent) => {
|
||||||
|
const errorInfo: ErrorInfo = {
|
||||||
|
message: event.message,
|
||||||
|
stack: event.error?.stack,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
url: window.location.href,
|
||||||
|
component: 'global'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logError(event.error || new Error(event.message), errorInfo);
|
||||||
|
this.handleError(event.error || new Error(event.message), errorInfo);
|
||||||
|
|
||||||
|
// Prevent default error handling
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle unhandled promise rejections
|
||||||
|
window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {
|
||||||
|
const error = new Error(event.reason?.message || 'Unhandled Promise Rejection');
|
||||||
|
const errorInfo: ErrorInfo = {
|
||||||
|
message: error.message,
|
||||||
|
stack: event.reason?.stack,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
url: window.location.href,
|
||||||
|
component: 'promise'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logError(error, errorInfo);
|
||||||
|
this.handleError(error, errorInfo);
|
||||||
|
|
||||||
|
// Prevent default error handling
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap a function with error boundary
|
||||||
|
wrap<T extends (...args: any[]) => any>(
|
||||||
|
fn: T,
|
||||||
|
component: string,
|
||||||
|
fallback?: (...args: Parameters<T>) => ReturnType<T>
|
||||||
|
): T {
|
||||||
|
return ((...args: Parameters<T>): ReturnType<T> => {
|
||||||
|
try {
|
||||||
|
const result = fn(...args);
|
||||||
|
|
||||||
|
// Handle async functions
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
return result.catch((error: Error) => {
|
||||||
|
const errorInfo: ErrorInfo = {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
component,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
url: window.location.href
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logError(error, errorInfo);
|
||||||
|
this.handleError(error, errorInfo);
|
||||||
|
|
||||||
|
if (fallback) {
|
||||||
|
return fallback(...args) as ReturnType<T>;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}) as ReturnType<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorInfo: ErrorInfo = {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
component,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
url: window.location.href
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logError(error, errorInfo);
|
||||||
|
this.handleError(error, errorInfo);
|
||||||
|
|
||||||
|
if (fallback) {
|
||||||
|
return fallback(...args);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap async functions specifically
|
||||||
|
wrapAsync<T extends (...args: any[]) => Promise<any>>(
|
||||||
|
fn: T,
|
||||||
|
component: string,
|
||||||
|
fallback?: (...args: Parameters<T>) => ReturnType<T>
|
||||||
|
): T {
|
||||||
|
return (async (...args: Parameters<T>) => {
|
||||||
|
try {
|
||||||
|
return await fn(...args);
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorInfo: ErrorInfo = {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
component,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
url: window.location.href
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logError(error, errorInfo);
|
||||||
|
this.handleError(error, errorInfo);
|
||||||
|
|
||||||
|
if (fallback) {
|
||||||
|
return fallback(...args);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register component-specific error handler
|
||||||
|
registerErrorHandler(component: string, handler: (error: Error, errorInfo: ErrorInfo) => void): void {
|
||||||
|
this.errorHandlers.set(component, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set global error handler
|
||||||
|
setGlobalErrorHandler(handler: (error: Error, errorInfo: ErrorInfo) => void): void {
|
||||||
|
this.globalErrorHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private logError(error: Error, errorInfo: ErrorInfo): void {
|
||||||
|
// Add to error log
|
||||||
|
this.errorLog.push(errorInfo);
|
||||||
|
|
||||||
|
// Keep only recent errors
|
||||||
|
if (this.errorLog.length > this.maxErrorLog) {
|
||||||
|
this.errorLog.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log to console in development
|
||||||
|
console.error(`[${errorInfo.component}] Error:`, error);
|
||||||
|
console.error('Error Info:', errorInfo);
|
||||||
|
|
||||||
|
// Send to monitoring service if available
|
||||||
|
this.sendToMonitoring(error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: Error, errorInfo: ErrorInfo): void {
|
||||||
|
// Check for component-specific handler
|
||||||
|
const componentHandler = this.errorHandlers.get(errorInfo.component || '');
|
||||||
|
if (componentHandler) {
|
||||||
|
componentHandler(error, errorInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use global handler if set
|
||||||
|
if (this.globalErrorHandler) {
|
||||||
|
this.globalErrorHandler(error, errorInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default error handling
|
||||||
|
this.showErrorNotification(error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private showErrorNotification(error: Error, errorInfo: ErrorInfo): void {
|
||||||
|
// Create error notification
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'alert alert-danger alert-dismissible fade show position-fixed bottom-0 end-0 m-3';
|
||||||
|
notification.style.zIndex = '9999';
|
||||||
|
notification.style.maxWidth = '400px';
|
||||||
|
|
||||||
|
const isUserFacing = this.isUserFacingError(error);
|
||||||
|
const message = isUserFacing ? error.message : 'An unexpected error occurred. Please try again.';
|
||||||
|
|
||||||
|
notification.innerHTML = `
|
||||||
|
<strong><i class="fas fa-exclamation-circle"></i> Error${errorInfo.component ? ` in ${errorInfo.component}` : ''}</strong>
|
||||||
|
<p class="mb-0">${message}</p>
|
||||||
|
${!isUserFacing ? '<small class="text-muted">The error has been logged for investigation.</small>' : ''}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Auto-dismiss after 10 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.remove();
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isUserFacingError(error: Error): boolean {
|
||||||
|
// Determine if error should be shown to user as-is
|
||||||
|
const userFacingMessages = [
|
||||||
|
'rate limit',
|
||||||
|
'network',
|
||||||
|
'offline',
|
||||||
|
'not found',
|
||||||
|
'unauthorized',
|
||||||
|
'forbidden',
|
||||||
|
'timeout',
|
||||||
|
'invalid'
|
||||||
|
];
|
||||||
|
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
return userFacingMessages.some(msg => message.includes(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendToMonitoring(error: Error, errorInfo: ErrorInfo): Promise<void> {
|
||||||
|
// Only send errors in production
|
||||||
|
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send error to backend monitoring endpoint
|
||||||
|
await fetch('/api/log-error', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
error: {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
name: error.name
|
||||||
|
},
|
||||||
|
errorInfo
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch (monitoringError) {
|
||||||
|
// Fail silently - don't create error loop
|
||||||
|
console.error('Failed to send error to monitoring:', monitoringError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get error log for debugging
|
||||||
|
getErrorLog(): ErrorInfo[] {
|
||||||
|
return [...this.errorLog];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear error log
|
||||||
|
clearErrorLog(): void {
|
||||||
|
this.errorLog = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if component has recent errors
|
||||||
|
hasRecentErrors(component: string, timeWindow: number = 60000): boolean {
|
||||||
|
const cutoff = Date.now() - timeWindow;
|
||||||
|
return this.errorLog.some(
|
||||||
|
error => error.component === component && error.timestamp > cutoff
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user