feat(error-tracking): Add dual error tracking system with Sentry and backend fallback

This commit is contained in:
debudebuye 2026-02-24 13:20:28 +03:00
parent 375d75fe44
commit 6021d83385
4 changed files with 586 additions and 0 deletions

View File

@ -0,0 +1,392 @@
# Error Tracking with Fallback System
## Overview
This project uses a **dual error tracking system**:
1. **Primary**: Sentry (cloud-based)
2. **Fallback**: Custom backend logging (if Sentry fails)
## Architecture
```
Error Occurs
Try Sentry First
If Sentry Fails → Queue for Backend
Send to Backend API: POST /api/v1/errors/log
```
## Usage
### Basic Error Tracking
```typescript
import { errorTracker } from '@/lib/error-tracker'
try {
await riskyOperation()
} catch (error) {
errorTracker.trackError(error, {
tags: { section: 'payment' },
extra: { orderId: '123' },
userId: user.id
})
}
```
### Track Messages
```typescript
errorTracker.trackMessage('Payment processed successfully', 'info', {
amount: 100,
currency: 'USD'
})
```
### Set User Context
```typescript
// After login
errorTracker.setUser({
id: user.id,
email: user.email,
name: user.name
})
// On logout
errorTracker.clearUser()
```
### Add Breadcrumbs
```typescript
errorTracker.addBreadcrumb('navigation', 'User clicked checkout button', 'info')
```
## Backend API Required
Your backend needs to implement this endpoint:
### POST /api/v1/errors/log
**Request Body:**
```json
{
"message": "Error message",
"stack": "Error stack trace",
"url": "https://app.example.com/dashboard",
"userAgent": "Mozilla/5.0...",
"timestamp": "2024-02-24T10:30:00.000Z",
"userId": "user-123",
"extra": {
"section": "payment",
"orderId": "123"
}
}
```
**Response:**
```json
{
"success": true,
"logId": "log-456"
}
```
### Backend Implementation Example (Node.js/Express)
```javascript
// routes/errors.js
router.post('/errors/log', async (req, res) => {
try {
const { message, stack, url, userAgent, timestamp, userId, extra } = req.body
// Save to database
await ErrorLog.create({
message,
stack,
url,
userAgent,
timestamp: new Date(timestamp),
userId,
extra: JSON.stringify(extra)
})
// Optional: Send alert for critical errors
if (message.includes('payment') || message.includes('auth')) {
await sendSlackAlert(message, stack)
}
res.json({ success: true })
} catch (error) {
console.error('Failed to log error:', error)
res.status(500).json({ success: false })
}
})
```
### Database Schema
```sql
CREATE TABLE error_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message TEXT NOT NULL,
stack TEXT,
url TEXT NOT NULL,
user_agent TEXT,
timestamp TIMESTAMP NOT NULL,
user_id UUID,
extra JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_error_logs_timestamp ON error_logs(timestamp DESC);
CREATE INDEX idx_error_logs_user_id ON error_logs(user_id);
```
## How It Works
### 1. Automatic Global Error Handling
All uncaught errors are automatically tracked:
```typescript
// Automatically catches all errors
window.addEventListener('error', (event) => {
errorTracker.trackError(new Error(event.message))
})
// Catches unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
errorTracker.trackError(event.reason)
})
```
### 2. Queue System
Errors are queued and sent to backend:
- Max queue size: 50 errors
- Automatic retry on failure
- Prevents memory leaks
### 3. Dual Tracking
Every error is sent to:
1. **Sentry** (if available) - Rich debugging features
2. **Backend** (always) - Your own database for compliance/analysis
## Sentry Alternatives
If you want to replace Sentry entirely:
### 1. LogRocket
```bash
npm install logrocket
```
```typescript
import LogRocket from 'logrocket'
LogRocket.init('your-app-id')
// Track errors
LogRocket.captureException(error)
```
### 2. Rollbar
```bash
npm install rollbar
```
```typescript
import Rollbar from 'rollbar'
const rollbar = new Rollbar({
accessToken: 'your-token',
environment: 'production'
})
rollbar.error(error)
```
### 3. Bugsnag
```bash
npm install @bugsnag/js @bugsnag/plugin-react
```
```typescript
import Bugsnag from '@bugsnag/js'
import BugsnagPluginReact from '@bugsnag/plugin-react'
Bugsnag.start({
apiKey: 'your-api-key',
plugins: [new BugsnagPluginReact()]
})
```
### 4. Self-Hosted GlitchTip
```bash
# Docker Compose
docker-compose up -d
```
Free, open-source, Sentry-compatible API.
## Benefits of Fallback System
### 1. Reliability
- Never lose error data if Sentry is down
- Backend always receives errors
### 2. Compliance
- Keep error logs in your own database
- Meet data residency requirements
- Full control over sensitive data
### 3. Cost Control
- Reduce Sentry event count
- Use backend for high-volume errors
- Keep Sentry for detailed debugging
### 4. Custom Analysis
- Query errors with SQL
- Build custom dashboards
- Integrate with your alerting system
## Configuration
### Enable/Disable Fallback
```typescript
// src/lib/error-tracker.ts
// Disable backend logging (Sentry only)
const ENABLE_BACKEND_LOGGING = false
private queueError(errorLog: ErrorLog) {
if (!ENABLE_BACKEND_LOGGING) return
// ... rest of code
}
```
### Adjust Queue Size
```typescript
private maxQueueSize = 100 // Increase for high-traffic apps
```
### Change Backend Endpoint
```typescript
await apiClient.post('/custom/error-endpoint', errorLog)
```
## Monitoring Dashboard
Build a simple error dashboard:
```typescript
// Backend endpoint
router.get('/errors/stats', async (req, res) => {
const stats = await db.query(`
SELECT
DATE(timestamp) as date,
COUNT(*) as count,
COUNT(DISTINCT user_id) as affected_users
FROM error_logs
WHERE timestamp > NOW() - INTERVAL '7 days'
GROUP BY DATE(timestamp)
ORDER BY date DESC
`)
res.json(stats)
})
```
## Best Practices
### 1. Don't Log Everything
```typescript
// Bad: Logging expected errors
if (!user) {
errorTracker.trackError(new Error('User not found'))
}
// Good: Only log unexpected errors
try {
await criticalOperation()
} catch (error) {
errorTracker.trackError(error)
}
```
### 2. Add Context
```typescript
errorTracker.trackError(error, {
extra: {
action: 'checkout',
step: 'payment',
amount: 100
}
})
```
### 3. Set User Context Early
```typescript
// In your auth flow
useEffect(() => {
if (user) {
errorTracker.setUser({
id: user.id,
email: user.email,
name: user.name
})
}
}, [user])
```
### 4. Clean Up on Logout
```typescript
const handleLogout = () => {
errorTracker.clearUser()
// ... rest of logout
}
```
## Troubleshooting
### Errors Not Reaching Backend
1. Check network tab for failed requests
2. Verify backend endpoint exists
3. Check CORS configuration
4. Review backend logs
### Queue Growing Too Large
1. Increase `maxQueueSize`
2. Check backend availability
3. Add retry logic with exponential backoff
### Duplicate Errors
This is intentional - errors go to both Sentry and backend. To disable:
```typescript
// Only send to backend if Sentry fails
try {
Sentry.captureException(error)
} catch (sentryError) {
this.queueError(errorLog) // Only fallback
}
```
## Resources
- [Sentry Documentation](https://docs.sentry.io/)
- [LogRocket Documentation](https://docs.logrocket.com/)
- [Rollbar Documentation](https://docs.rollbar.com/)
- [GlitchTip (Self-hosted)](https://glitchtip.com/)

192
src/lib/error-tracker.ts Normal file
View File

@ -0,0 +1,192 @@
import { Sentry } from './sentry'
import adminApi from './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 adminApi.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',
},
}
)
})

View File

@ -8,6 +8,7 @@ import { queryClient } from "@/app/query-client"
import { Toaster } from "@/components/ui/toast"
import { ErrorBoundary } from "@/components/ErrorBoundary"
import { initSentry } from "@/lib/sentry"
import "@/lib/error-tracker" // Initialize global error handlers
// Initialize Sentry
initSentry()

View File

@ -7,6 +7,7 @@ import { Label } from "@/components/ui/label"
import { Eye, EyeOff } from "lucide-react"
import { toast } from "sonner"
import { adminApiHelpers } from "@/lib/api-client"
import { errorTracker } from "@/lib/error-tracker"
export default function LoginPage() {
const navigate = useNavigate()