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.backends import default_backend
|
||||
import gc # For garbage collection
|
||||
from functools import wraps
|
||||
import traceback
|
||||
|
||||
# Initialize logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
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.config['UPLOAD_FOLDER'] = tempfile.mkdtemp()
|
||||
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)}")
|
||||
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
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health_check():
|
||||
@ -741,5 +786,46 @@ app.request_count = 0
|
||||
def before_request():
|
||||
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__':
|
||||
app.run(host='0.0.0.0', port=5005, debug=True)
|
||||
|
@ -16,18 +16,60 @@ import {
|
||||
} from './types';
|
||||
import { TranslationCache } from './translationCache';
|
||||
import { RequestQueueManager } from './requestQueue';
|
||||
import { ErrorBoundary } from './errorBoundary';
|
||||
|
||||
// Initialize error boundary
|
||||
const errorBoundary = ErrorBoundary.getInstance();
|
||||
|
||||
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
|
||||
if ('serviceWorker' in navigator) {
|
||||
registerServiceWorker();
|
||||
safeRegisterServiceWorker();
|
||||
}
|
||||
|
||||
// Initialize app
|
||||
initApp();
|
||||
safeInitApp();
|
||||
|
||||
// Check for PWA installation prompts
|
||||
initInstallPrompt();
|
||||
safeInitInstallPrompt();
|
||||
});
|
||||
|
||||
// Service Worker Registration
|
||||
@ -356,7 +398,7 @@ function initApp(): void {
|
||||
}
|
||||
|
||||
// Function to transcribe audio
|
||||
async function transcribeAudio(audioBlob: Blob): Promise<void> {
|
||||
const transcribeAudioBase = async function(audioBlob: Blob): Promise<void> {
|
||||
const formData = new FormData();
|
||||
formData.append('audio', audioBlob, 'audio.webm'); // Add filename for better server handling
|
||||
formData.append('source_lang', sourceLanguage.value);
|
||||
@ -438,10 +480,24 @@ function initApp(): void {
|
||||
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
|
||||
translateBtn.addEventListener('click', async function() {
|
||||
translateBtn.addEventListener('click', errorBoundary.wrapAsync(async function() {
|
||||
if (!currentSourceText) {
|
||||
return;
|
||||
}
|
||||
@ -556,7 +612,12 @@ function initApp(): void {
|
||||
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
|
||||
playSource.addEventListener('click', function() {
|
||||
@ -575,7 +636,7 @@ function initApp(): void {
|
||||
});
|
||||
|
||||
// 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();
|
||||
showLoadingOverlay('Generating audio...');
|
||||
|
||||
@ -654,7 +715,19 @@ function initApp(): void {
|
||||
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
|
||||
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