diff --git a/app.py b/app.py index e5f10fd..ed3412c 100644 --- a/app.py +++ b/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) diff --git a/static/js/src/app.ts b/static/js/src/app.ts index dd2d2a4..8185cb6 100644 --- a/static/js/src/app.ts +++ b/static/js/src/app.ts @@ -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 = '

Translation failed. Please try again.

'; + } + } + }); + + // 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 { + const transcribeAudioBase = async function(audioBlob: Blob): Promise { 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 = '

Transcription failed. Please try again.

'; + 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 = '

Translation failed. Please try again.

'; + 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 { + const playAudioBase = async function(text: string, language: string): Promise { 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() { diff --git a/static/js/src/errorBoundary.ts b/static/js/src/errorBoundary.ts new file mode 100644 index 0000000..dd25fb4 --- /dev/null +++ b/static/js/src/errorBoundary.ts @@ -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 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 any>( + fn: T, + component: string, + fallback?: (...args: Parameters) => ReturnType + ): T { + return ((...args: Parameters): ReturnType => { + 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; + } + throw error; + }) as ReturnType; + } + + 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 Promise>( + fn: T, + component: string, + fallback?: (...args: Parameters) => ReturnType + ): T { + return (async (...args: Parameters) => { + 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 = ` + Error${errorInfo.component ? ` in ${errorInfo.component}` : ''} +

${message}

+ ${!isUserFacing ? 'The error has been logged for investigation.' : ''} + + `; + + 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 { + // 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 + ); + } +} \ No newline at end of file