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:
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user