feat(error-tracking): Add dual error tracking system with Sentry and backend fallback
This commit is contained in:
parent
375d75fe44
commit
6021d83385
392
dev-docs/ERROR_TRACKING_FALLBACK.md
Normal file
392
dev-docs/ERROR_TRACKING_FALLBACK.md
Normal 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
192
src/lib/error-tracker.ts
Normal 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',
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user