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 { Toaster } from "@/components/ui/toast"
|
||||||
import { ErrorBoundary } from "@/components/ErrorBoundary"
|
import { ErrorBoundary } from "@/components/ErrorBoundary"
|
||||||
import { initSentry } from "@/lib/sentry"
|
import { initSentry } from "@/lib/sentry"
|
||||||
|
import "@/lib/error-tracker" // Initialize global error handlers
|
||||||
|
|
||||||
// Initialize Sentry
|
// Initialize Sentry
|
||||||
initSentry()
|
initSentry()
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { Label } from "@/components/ui/label"
|
||||||
import { Eye, EyeOff } from "lucide-react"
|
import { Eye, EyeOff } from "lucide-react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { adminApiHelpers } from "@/lib/api-client"
|
||||||
|
import { errorTracker } from "@/lib/error-tracker"
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user