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