import { Sentry } from './sentry' import apiClient from '@/services/api/client' interface ErrorLog { message: string stack?: string url: string userAgent: string timestamp: string userId?: string extra?: Record } class ErrorTracker { private queue: ErrorLog[] = [] private isProcessing = false private maxQueueSize = 50 /** * Track an error with fallback to backend if Sentry fails */ async trackError( error: Error, context?: { tags?: Record extra?: Record userId?: string } ) { const errorLog: ErrorLog = { message: error.message, stack: error.stack, url: window.location.href, userAgent: navigator.userAgent, timestamp: new Date().toISOString(), userId: context?.userId, extra: context?.extra, } // Try Sentry first try { Sentry.captureException(error, { tags: context?.tags, extra: context?.extra, }) } catch (sentryError) { console.warn('Sentry failed, using fallback:', sentryError) // If Sentry fails, queue for backend logging this.queueError(errorLog) } // Always log to backend as backup this.queueError(errorLog) } /** * Track a message with fallback */ async trackMessage( message: string, level: 'info' | 'warning' | 'error' = 'info', extra?: Record ) { try { Sentry.captureMessage(message, level) } catch (sentryError) { console.warn('Sentry failed, using fallback:', sentryError) this.queueError({ message, url: window.location.href, userAgent: navigator.userAgent, timestamp: new Date().toISOString(), extra: { level, ...extra }, }) } } /** * Queue error for backend logging */ private queueError(errorLog: ErrorLog) { this.queue.push(errorLog) // Prevent queue from growing too large if (this.queue.length > this.maxQueueSize) { this.queue.shift() } // Process queue this.processQueue() } /** * Send queued errors to backend */ private async processQueue() { if (this.isProcessing || this.queue.length === 0) { return } this.isProcessing = true while (this.queue.length > 0) { const errorLog = this.queue[0] try { // Send to your backend error logging endpoint await apiClient.post('/errors/log', errorLog) this.queue.shift() // Remove from queue on success } catch (error) { console.error('Failed to log error to backend:', error) // Keep in queue and try again later break } } this.isProcessing = false } /** * Set user context for tracking */ setUser(user: { id: string; email?: string; name?: string }) { try { Sentry.setUser({ id: user.id, email: user.email, username: user.name, }) } catch (error) { console.warn('Failed to set Sentry user:', error) } } /** * Clear user context (on logout) */ clearUser() { try { Sentry.setUser(null) } catch (error) { console.warn('Failed to clear Sentry user:', error) } } /** * Add breadcrumb for debugging */ addBreadcrumb( category: string, message: string, level: 'info' | 'warning' | 'error' = 'info' ) { try { Sentry.addBreadcrumb({ category, message, level, }) } catch (error) { console.warn('Failed to add Sentry breadcrumb:', error) } } } // Export singleton instance export const errorTracker = new ErrorTracker() // Global error handler window.addEventListener('error', (event) => { errorTracker.trackError(new Error(event.message), { extra: { filename: event.filename, lineno: event.lineno, colno: event.colno, }, }) }) // Unhandled promise rejection handler window.addEventListener('unhandledrejection', (event) => { errorTracker.trackError( event.reason instanceof Error ? event.reason : new Error(String(event.reason)), { extra: { type: 'unhandledRejection', }, } ) })