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:
Adolfo Delorenzo 2025-06-02 22:47:43 -06:00
parent 0c9186e57e
commit 3804897e2b
3 changed files with 454 additions and 9 deletions

86
app.py
View File

@ -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)

View File

@ -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() {

View 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
);
}
}