diff --git a/dev-docs/ERROR_TRACKING_FALLBACK.md b/dev-docs/ERROR_TRACKING_FALLBACK.md new file mode 100644 index 0000000..cffb0e0 --- /dev/null +++ b/dev-docs/ERROR_TRACKING_FALLBACK.md @@ -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/) + diff --git a/src/lib/error-tracker.ts b/src/lib/error-tracker.ts new file mode 100644 index 0000000..a4db758 --- /dev/null +++ b/src/lib/error-tracker.ts @@ -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 +} + +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 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', + }, + } + ) +}) diff --git a/src/main.tsx b/src/main.tsx index 87ef17c..f31ddc9 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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() diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index f38fee4..3d3f5bb 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -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()