mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 06:05:18 -05:00
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:
308
docs/ERROR_HANDLING.md
Normal file
308
docs/ERROR_HANDLING.md
Normal 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
|
||||
Reference in New Issue
Block a user