feat: Implement centralized error capture and handling with new middleware, services, and API endpoints, and add new admin and statistics API views.

This commit is contained in:
pacnpal
2026-01-02 15:55:42 -05:00
parent 1adba1b804
commit 95700c7d7b
43 changed files with 2477 additions and 158 deletions

308
docs/ERROR_HANDLING.md Normal file
View File

@@ -0,0 +1,308 @@
# Error Handling Guide
This guide covers ThrillWiki's comprehensive error handling system for both the Django backend and Nuxt frontend.
## Overview
The error system captures errors from all sources (frontend, backend, API) and displays them in the admin dashboard at `/admin/errors`.
---
## Quick Start
### Backend (Python)
```python
from apps.core.utils import capture_errors, error_context, capture_and_log
# Option 1: Decorator on functions/views
@capture_errors(severity='high')
def create_park(request, data):
return ParkService.create(data) # Errors auto-captured
# Option 2: Context manager for code blocks
with error_context('Processing payment', severity='critical'):
process_payment()
# Option 3: Manual capture
try:
risky_operation()
except Exception as e:
error_id = capture_and_log(e, 'Risky operation failed')
return Response({'error': f'Failed (ref: {error_id})'})
```
### Frontend (TypeScript)
```typescript
// Option 1: useErrorBoundary composable
const { wrap, error, isError, retry } = useErrorBoundary({
componentName: 'RideDetail'
})
const ride = await wrap(() => api.get('/rides/1'), 'Loading ride')
// Option 2: tryCatch utility (Go-style)
const [data, err] = await tryCatch(fetchRide(id), 'Fetching ride')
if (err) return // Error already reported
// Option 3: useReportError (manual)
const { reportError } = useReportError()
try {
await riskyOperation()
} catch (e) {
reportError(e, { action: 'Risky operation' })
}
```
---
## Backend Utilities
### `@capture_errors` Decorator
Automatically captures exceptions from decorated functions.
```python
from apps.core.utils import capture_errors
@capture_errors(
source='api', # 'frontend', 'backend', or 'api'
severity='high', # 'critical', 'high', 'medium', 'low'
reraise=True, # Re-raise after capturing (default True)
log_errors=True # Also log to Python logger (default True)
)
def my_view(request):
# If this raises, error is automatically captured
return do_something()
```
**Use for**: API views, service methods, any function where you want automatic tracking.
### `error_context` Context Manager
Captures errors from a code block with rich context.
```python
from apps.core.utils import error_context
with error_context(
'Creating ride submission',
source='backend',
severity='high',
request=request, # Optional: for user/IP context
entity_type='Ride', # Optional: what entity
entity_id=123, # Optional: which entity
reraise=True # Default True
):
submission = SubmissionService.create(data)
```
**Use for**: Specific operations where you want detailed context about what was happening.
### `capture_and_log` Function
One-liner for manual error capture that returns the error ID.
```python
from apps.core.utils import capture_and_log
try:
result = risky_operation()
except Exception as e:
error_id = capture_and_log(
e,
'Risky operation',
severity='high',
request=request,
entity_type='Park',
entity_id=42
)
return Response({'error': f'Failed (ref: {error_id})'}, status=500)
```
**Use for**: When you need the error ID to show to users for support reference.
---
## Frontend Utilities
### `useErrorBoundary` Composable
Component-level error boundary with retry support.
```typescript
const {
error, // Ref<Error | null> - current error
isError, // ComputedRef<boolean> - whether error exists
lastErrorId, // Ref<string | null> - for support reference
clear, // () => void - clear error state
retry, // () => Promise<void> - retry last operation
wrap, // Wrap async function
wrapSync // Wrap sync function
} = useErrorBoundary({
componentName: 'MyComponent',
entityType: 'Ride',
defaultSeverity: 'medium',
showToast: true,
onError: (err, id) => console.log('Error:', err)
})
// In template
// <div v-if="isError" class="error">
// {{ error?.message }}
// <button @click="retry">Try Again</button>
// </div>
```
### `tryCatch` Utility
Go-style error handling for one-liners.
```typescript
import { tryCatch, tryCatchSync } from '~/utils/tryCatch'
// Async
const [ride, error] = await tryCatch(api.get('/rides/1'), 'Loading ride')
if (error) {
console.log('Failed:', error.message)
return
}
console.log('Got ride:', ride)
// Sync
const [data, err] = tryCatchSync(() => JSON.parse(input), 'Parsing JSON')
// With options
const [data, err] = await tryCatch(fetchData(), 'Fetching', {
severity: 'critical',
silent: true, // No toast notification
entityType: 'Ride',
entityId: 123
})
```
### `useReportError` Composable
Full-featured error reporting with maximum context.
```typescript
const { reportError, reportErrorSilent, withErrorReporting } = useReportError()
// Report with toast
await reportError(error, {
action: 'Saving ride',
componentName: 'EditRideModal',
entityType: 'Ride',
entityId: 42,
severity: 'high',
metadata: { custom: 'data' }
})
// Report without toast
await reportErrorSilent(error, { action: 'Background task' })
// Wrap a function
const safeFetch = withErrorReporting(fetchRide, {
componentName: 'RideDetail',
entityType: 'Ride'
})
```
### Global Error Plugin
The `errorPlugin.client.ts` automatically catches:
- Vue component errors
- Unhandled promise rejections
- Global JavaScript errors
These are reported silently to the dashboard without user intervention.
---
## Viewing Errors
1. Navigate to `/admin/errors` (requires admin role)
2. Filter by severity, source, date range, or resolution status
3. Click on an error to see full details including:
- Stack trace
- Browser environment
- User context
- Request details
4. Mark errors as resolved with notes
5. Export errors to CSV
---
## Best Practices
1. **Use decorators for views**: All API viewsets should have `@capture_errors`
2. **Use context managers for critical operations**: Payments, data migrations, bulk operations
3. **Use `tryCatch` for async code**: Clean, Go-style error handling
4. **Set appropriate severity**:
- `critical`: Database errors, payment failures, data loss
- `high`: Unexpected runtime errors, 5xx responses
- `medium`: Validation errors, user input issues
- `low`: Warnings, graceful degradation
5. **Include entity context**: Always provide `entity_type` and `entity_id` when operating on specific records
6. **Don't swallow errors**: Use `reraise=True` (default) unless you have a recovery strategy
---
## Integration Patterns
### API ViewSet with Decorator
```python
from rest_framework import viewsets
from apps.core.utils import capture_errors
class RideViewSet(viewsets.ModelViewSet):
@capture_errors(source='api')
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
```
### Vue Component with Error Boundary
```vue
<script setup lang="ts">
const { wrap, isError, error, retry } = useErrorBoundary({
componentName: 'RideCard'
})
const ride = ref(null)
onMounted(async () => {
ride.value = await wrap(() => api.get(`/rides/${props.id}`), 'Loading ride')
})
</script>
<template>
<div v-if="isError" class="error-state">
<p>{{ error?.message }}</p>
<UButton @click="retry">Retry</UButton>
</div>
<div v-else-if="ride">
<!-- Normal content -->
</div>
</template>
```
---
## Troubleshooting
**Errors not appearing in dashboard?**
- Check the backend server logs for capture failures
- Verify `MIDDLEWARE` includes `ErrorCaptureMiddleware`
- Ensure the `ApplicationError` model is migrated
**Frontend errors not reporting?**
- Check browser console for API call failures
- Verify `.env` has correct `NUXT_PUBLIC_API_BASE` (port 8000)
- Check network tab for `/api/v1/errors/report/` requests