193 lines
4.1 KiB
TypeScript
193 lines
4.1 KiB
TypeScript
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<string, unknown>
|
|
}
|
|
|
|
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<string, string>
|
|
extra?: Record<string, unknown>
|
|
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<string, unknown>
|
|
) {
|
|
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',
|
|
},
|
|
}
|
|
)
|
|
})
|