diff --git a/docs/ERROR_HANDLING_GUIDE.md b/docs/ERROR_HANDLING_GUIDE.md
new file mode 100644
index 00000000..15b77acb
--- /dev/null
+++ b/docs/ERROR_HANDLING_GUIDE.md
@@ -0,0 +1,589 @@
+# Error Handling Guide
+
+This guide outlines the standardized error handling patterns used throughout ThrillWiki to ensure consistent, debuggable, and user-friendly error management.
+
+## Core Principles
+
+1. **All errors must be logged** - Never silently swallow errors
+2. **Provide context** - Include relevant metadata for debugging
+3. **User-friendly messages** - Show clear, actionable error messages to users
+4. **Preserve error chains** - Don't lose original error information
+5. **Use structured logging** - Avoid raw `console.*` statements
+
+## When to Use What
+
+### `handleError()` - Application Errors (User-Facing)
+
+Use `handleError()` for errors that affect user operations and should be visible in the Admin Panel.
+
+**When to use:**
+- Database operation failures
+- API call failures
+- Form submission errors
+- Authentication/authorization failures
+- Any error that impacts user workflows
+
+**Example:**
+```typescript
+import { handleError } from '@/lib/errorHandler';
+import { useAuth } from '@/hooks/useAuth';
+
+try {
+ await supabase.from('parks').insert(parkData);
+ handleSuccess('Park Created', 'Your park has been added successfully');
+} catch (error) {
+ handleError(error, {
+ action: 'Create Park',
+ userId: user?.id,
+ metadata: { parkName: parkData.name }
+ });
+ throw error; // Re-throw for parent error boundaries
+}
+```
+
+**Key features:**
+- Logs to `request_metadata` table with full context
+- Shows user-friendly toast with error reference ID
+- Captures breadcrumbs (last 10 user actions)
+- Visible in Admin Panel at `/admin/error-monitoring`
+
+### `logger.*` - Development & Debugging Logs
+
+Use `logger.*` for information that helps developers debug issues without sending data to the database.
+
+**When to use:**
+- Development debugging information
+- Performance monitoring
+- Expected failures that don't need Admin Panel visibility
+- Component lifecycle events
+- Non-critical informational messages
+
+**Available methods:**
+```typescript
+import { logger } from '@/lib/logger';
+
+// Development only - not logged in production
+logger.log('Component mounted', { props });
+logger.info('User action completed', { action: 'click' });
+logger.warn('Deprecated API used', { api: 'oldMethod' });
+logger.debug('State updated', { newState });
+
+// Always logged - even in production
+logger.error('Critical failure', { context });
+
+// Specialized logging
+logger.performance('ComponentName', durationMs);
+logger.moderationAction('approve', itemId, durationMs);
+```
+
+**Example - Expected periodic failures:**
+```typescript
+// Don't show toast or log to Admin Panel for expected periodic failures
+try {
+ await supabase.rpc('release_expired_locks');
+} catch (error) {
+ logger.debug('Periodic lock release failed', {
+ operation: 'release_expired_locks',
+ error: getErrorMessage(error)
+ });
+}
+```
+
+### `toast.*` - User Notifications
+
+Use toast notifications directly for informational messages, warnings, or confirmations.
+
+**When to use:**
+- Success confirmations (use `handleSuccess()` helper)
+- Informational messages
+- Non-error warnings
+- User confirmations
+
+**Example:**
+```typescript
+import { handleSuccess, handleInfo } from '@/lib/errorHandler';
+
+// Success messages
+handleSuccess('Changes Saved', 'Your profile has been updated');
+
+// Informational messages
+handleInfo('Processing', 'Your request is being processed');
+
+// Custom toast for special cases
+toast.info('Feature Coming Soon', {
+ description: 'This feature will be available next month',
+ duration: 4000
+});
+```
+
+### ❌ `console.*` - NEVER USE DIRECTLY
+
+**DO NOT USE** `console.*` statements in application code. They are blocked by ESLint.
+
+```typescript
+// ❌ WRONG - Will fail ESLint check
+console.log('User clicked button');
+console.error('Database error:', error);
+
+// ✅ CORRECT - Use logger or handleError
+logger.log('User clicked button');
+handleError(error, { action: 'Database Operation', userId });
+```
+
+**The only exceptions:**
+- Inside `src/lib/logger.ts` itself
+- Edge function logging (use `edgeLogger.*`)
+- Test files (*.test.ts, *.test.tsx)
+
+## Error Handling Patterns
+
+### Pattern 1: Component/Hook Errors (Most Common)
+
+For errors in components or custom hooks that affect user operations:
+
+```typescript
+import { handleError } from '@/lib/errorHandler';
+import { useAuth } from '@/hooks/useAuth';
+
+const MyComponent = () => {
+ const { user } = useAuth();
+
+ const handleSubmit = async (data: FormData) => {
+ try {
+ await saveData(data);
+ handleSuccess('Saved', 'Your changes have been saved');
+ } catch (error) {
+ handleError(error, {
+ action: 'Save Form Data',
+ userId: user?.id,
+ metadata: { formType: 'parkEdit' }
+ });
+ throw error; // Re-throw for error boundaries
+ }
+ };
+};
+```
+
+**Key points:**
+- Always include descriptive action name
+- Include userId when available
+- Add relevant metadata for debugging
+- Re-throw after handling to let error boundaries catch it
+
+### Pattern 2: TanStack Query Errors
+
+For errors within React Query hooks:
+
+```typescript
+import { useQuery } from '@tanstack/react-query';
+import { handleError } from '@/lib/errorHandler';
+
+const { data, error, isLoading } = useQuery({
+ queryKey: ['parks', parkId],
+ queryFn: async () => {
+ const { data, error } = await supabase
+ .from('parks')
+ .select('*')
+ .eq('id', parkId)
+ .single();
+
+ if (error) {
+ handleError(error, {
+ action: 'Fetch Park Details',
+ userId: user?.id,
+ metadata: { parkId }
+ });
+ throw error;
+ }
+
+ return data;
+ }
+});
+
+// Handle error state in UI
+if (error) {
+ return ;
+}
+```
+
+### Pattern 3: Expected/Recoverable Errors
+
+For operations that may fail expectedly and should be logged but not shown to users:
+
+```typescript
+import { logger } from '@/lib/logger';
+import { getErrorMessage } from '@/lib/errorHandler';
+
+// Background operation that may fail without impacting user
+const syncCache = async () => {
+ try {
+ await performCacheSync();
+ } catch (error) {
+ // Log for debugging without user notification
+ logger.warn('Cache sync failed', {
+ operation: 'syncCache',
+ error: getErrorMessage(error)
+ });
+ // Continue execution - cache sync is non-critical
+ }
+};
+```
+
+### Pattern 4: Error Boundaries (Top-Level)
+
+React Error Boundaries catch unhandled component errors:
+
+```typescript
+import { Component, ReactNode } from 'react';
+import { handleError } from '@/lib/errorHandler';
+
+class ErrorBoundary extends Component<
+ { children: ReactNode },
+ { hasError: boolean }
+> {
+ static getDerivedStateFromError() {
+ return { hasError: true };
+ }
+
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
+ handleError(error, {
+ action: 'Component Error Boundary',
+ metadata: {
+ componentStack: errorInfo.componentStack
+ }
+ });
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return ;
+ }
+ return this.props.children;
+ }
+}
+```
+
+### Pattern 5: Preserve Error Context in Chains
+
+When catching and re-throwing errors, preserve the original error information:
+
+```typescript
+// ❌ WRONG - Loses original error
+try {
+ await operation();
+} catch (error) {
+ throw new Error('Operation failed'); // Original error lost!
+}
+
+// ❌ WRONG - Silent catch loses context
+const data = await fetch(url)
+ .then(res => res.json())
+ .catch(() => ({ message: 'Failed' })); // Error details lost!
+
+// ✅ CORRECT - Preserve and log error
+try {
+ const response = await fetch(url);
+ if (!response.ok) {
+ const errorData = await response.json().catch((parseError) => {
+ logger.warn('Failed to parse error response', {
+ error: getErrorMessage(parseError),
+ status: response.status
+ });
+ return { message: 'Request failed' };
+ });
+ throw new Error(errorData.message);
+ }
+ return await response.json();
+} catch (error) {
+ handleError(error, {
+ action: 'Fetch Data',
+ userId: user?.id,
+ metadata: { url }
+ });
+ throw error;
+}
+```
+
+## Automatic Breadcrumb Tracking
+
+The application automatically tracks breadcrumbs (last 10 user actions) to provide context for errors.
+
+### Automatic Tracking (No Code Needed)
+
+1. **API Calls** - All Supabase operations are tracked automatically via the wrapped client
+2. **Navigation** - Route changes are tracked automatically
+3. **Mutation Errors** - TanStack Query mutations log failures automatically
+
+### Manual Breadcrumb Tracking
+
+Add breadcrumbs for important user actions:
+
+```typescript
+import { breadcrumb } from '@/lib/errorBreadcrumbs';
+
+// Navigation breadcrumb (usually automatic)
+breadcrumb.navigation('/parks/123', '/parks');
+
+// User action breadcrumb
+breadcrumb.userAction('clicked submit', 'ParkEditForm', {
+ parkId: '123'
+});
+
+// API call breadcrumb (usually automatic via wrapped client)
+breadcrumb.apiCall('/api/parks', 'POST', 200);
+
+// State change breadcrumb
+breadcrumb.stateChange('filter changed', {
+ filter: 'status=open'
+});
+```
+
+**When to add manual breadcrumbs:**
+- Critical user actions (form submissions, deletions)
+- Important state changes (filter updates, mode switches)
+- Non-Supabase API calls
+- Complex user workflows
+
+**When NOT to add breadcrumbs:**
+- Inside loops or frequently called functions
+- For every render or effect
+- For trivial state changes
+- Inside already tracked operations
+
+## Edge Function Error Handling
+
+Edge functions use a separate logger to prevent sensitive data exposure:
+
+```typescript
+import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
+
+Deno.serve(async (req) => {
+ const tracking = startRequest();
+
+ try {
+ // Your edge function logic
+ const result = await performOperation();
+
+ const duration = endRequest(tracking);
+ edgeLogger.info('Operation completed', {
+ requestId: tracking.requestId,
+ duration
+ });
+
+ return new Response(JSON.stringify(result), {
+ headers: { 'Content-Type': 'application/json' }
+ });
+ } catch (error) {
+ const duration = endRequest(tracking);
+
+ edgeLogger.error('Operation failed', {
+ requestId: tracking.requestId,
+ error: error.message,
+ duration
+ });
+
+ return new Response(
+ JSON.stringify({
+ error: 'Operation failed',
+ requestId: tracking.requestId
+ }),
+ { status: 500, headers: { 'Content-Type': 'application/json' } }
+ );
+ }
+});
+```
+
+**Key features:**
+- Automatic sanitization of sensitive fields
+- Request correlation IDs
+- Structured JSON logging
+- Duration tracking
+
+## Testing Error Handling
+
+### Manual Testing
+
+1. Visit `/test-error-logging` (dev only)
+2. Click "Generate Test Error"
+3. Check Admin Panel at `/admin/error-monitoring`
+4. Verify error appears with:
+ - Full stack trace
+ - Breadcrumbs (including API calls)
+ - Environment context
+ - User information
+
+### Automated Testing
+
+```typescript
+import { handleError } from '@/lib/errorHandler';
+
+describe('Error Handling', () => {
+ it('should log errors to database', async () => {
+ const mockError = new Error('Test error');
+
+ handleError(mockError, {
+ action: 'Test Action',
+ metadata: { test: true }
+ });
+
+ // Verify error logged to request_metadata table
+ const { data } = await supabase
+ .from('request_metadata')
+ .select('*')
+ .eq('error_message', 'Test error')
+ .single();
+
+ expect(data).toBeDefined();
+ expect(data.endpoint).toBe('Test Action');
+ });
+});
+```
+
+## Common Mistakes to Avoid
+
+### ❌ Mistake 1: Silent Error Catching
+```typescript
+// ❌ WRONG
+try {
+ await operation();
+} catch (error) {
+ // Nothing - error disappears!
+}
+
+// ✅ CORRECT
+try {
+ await operation();
+} catch (error) {
+ logger.debug('Expected operation failure', {
+ operation: 'name',
+ error: getErrorMessage(error)
+ });
+}
+```
+
+### ❌ Mistake 2: Using console.* Directly
+```typescript
+// ❌ WRONG - Blocked by ESLint
+console.log('Debug info', data);
+console.error('Error occurred', error);
+
+// ✅ CORRECT
+logger.log('Debug info', data);
+handleError(error, { action: 'Operation Name', userId });
+```
+
+### ❌ Mistake 3: Not Re-throwing After Handling
+```typescript
+// ❌ WRONG - Error doesn't reach error boundary
+try {
+ await operation();
+} catch (error) {
+ handleError(error, { action: 'Operation' });
+ // Error stops here - error boundary never sees it
+}
+
+// ✅ CORRECT
+try {
+ await operation();
+} catch (error) {
+ handleError(error, { action: 'Operation' });
+ throw error; // Let error boundary handle UI fallback
+}
+```
+
+### ❌ Mistake 4: Generic Error Messages
+```typescript
+// ❌ WRONG - No context
+handleError(error, { action: 'Error' });
+
+// ✅ CORRECT - Descriptive context
+handleError(error, {
+ action: 'Update Park Opening Hours',
+ userId: user?.id,
+ metadata: {
+ parkId: park.id,
+ parkName: park.name
+ }
+});
+```
+
+### ❌ Mistake 5: Losing Error Context
+```typescript
+// ❌ WRONG
+.catch(() => ({ error: 'Failed' }))
+
+// ✅ CORRECT
+.catch((error) => {
+ logger.warn('Operation failed', { error: getErrorMessage(error) });
+ return { error: 'Failed' };
+})
+```
+
+## Error Monitoring Dashboard
+
+Access the error monitoring dashboard at `/admin/error-monitoring`:
+
+**Features:**
+- Real-time error list with filtering
+- Search by error ID, message, or user
+- Full stack traces
+- Breadcrumb trails showing user actions before error
+- Environment context (browser, device, network)
+- Request metadata (endpoint, method, status)
+
+**Error ID Lookup:**
+Visit `/admin/error-lookup` to search for specific errors by their 8-character reference ID shown to users.
+
+## Related Files
+
+**Core Error Handling:**
+- `src/lib/errorHandler.ts` - Main error handling utilities
+- `src/lib/errorBreadcrumbs.ts` - Breadcrumb tracking system
+- `src/lib/environmentContext.ts` - Environment data capture
+- `src/lib/logger.ts` - Structured logging utility
+- `src/lib/supabaseClient.ts` - Wrapped client with auto-tracking
+
+**Admin Tools:**
+- `src/pages/admin/ErrorMonitoring.tsx` - Error dashboard
+- `src/pages/admin/ErrorLookup.tsx` - Error ID search
+- `src/components/admin/ErrorDetailsModal.tsx` - Error details view
+
+**Edge Functions:**
+- `supabase/functions/_shared/logger.ts` - Edge function logger
+
+**Database:**
+- `request_metadata` table - Stores all error logs
+- `request_breadcrumbs` table - Stores breadcrumb trails
+- `log_request_metadata` RPC - Logs errors from client
+
+## Summary
+
+**Golden Rules:**
+1. ✅ Use `handleError()` for user-facing application errors
+2. ✅ Use `logger.*` for development debugging and expected failures
+3. ✅ Use `toast.*` for success/info notifications
+4. ✅ Use `edgeLogger.*` in edge functions
+5. ❌ NEVER use `console.*` directly in application code
+6. ✅ Always preserve error context when catching
+7. ✅ Re-throw errors after handling for error boundaries
+8. ✅ Include descriptive action names and metadata
+9. ✅ Manual breadcrumbs for critical user actions only
+10. ✅ Test error handling in Admin Panel
+
+**Quick Reference:**
+```typescript
+// Application error (user-facing)
+handleError(error, { action: 'Action Name', userId, metadata });
+
+// Debug log (development only)
+logger.debug('Debug info', { context });
+
+// Expected failure (log but don't show toast)
+logger.warn('Expected failure', { error: getErrorMessage(error) });
+
+// Success notification
+handleSuccess('Title', 'Description');
+
+// Edge function error
+edgeLogger.error('Error message', { requestId, error: error.message });
+```
diff --git a/src/hooks/useModerationQueue.ts b/src/hooks/useModerationQueue.ts
index 6fbee7a9..bfac6a73 100644
--- a/src/hooks/useModerationQueue.ts
+++ b/src/hooks/useModerationQueue.ts
@@ -4,6 +4,7 @@ import { useAuth } from './useAuth';
import { useToast } from './use-toast';
import { getErrorMessage } from '@/lib/errorHandler';
import { getSubmissionTypeLabel } from '@/lib/moderation/entities';
+import { logger } from '@/lib/logger';
interface QueuedSubmission {
submission_id: string;
@@ -45,7 +46,11 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
try {
await supabase.rpc('release_expired_locks');
} catch (error: unknown) {
- // Silent failure - lock release happens periodically
+ // Log expected periodic failure for debugging without user toast
+ logger.debug('Periodic lock release failed', {
+ operation: 'release_expired_locks',
+ error: getErrorMessage(error)
+ });
}
}, 120000); // 2 minutes
@@ -76,16 +81,20 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
{ pendingCount: 0, avgWaitHours: 0 }
);
- setQueueStats({
- pendingCount: totals.pendingCount,
- assignedToMe: assignedCount || 0,
- avgWaitHours: slaData.length > 0 ? totals.avgWaitHours / slaData.length : 0,
- });
- }
- } catch (error: unknown) {
- // Silent failure - stats are refreshed periodically
+ setQueueStats({
+ pendingCount: totals.pendingCount,
+ assignedToMe: assignedCount || 0,
+ avgWaitHours: slaData.length > 0 ? totals.avgWaitHours / slaData.length : 0,
+ });
}
- }, [user]);
+ } catch (error: unknown) {
+ // Log stats fetch failure for debugging without user toast
+ logger.debug('Queue stats fetch failed', {
+ operation: 'fetchStats',
+ error: getErrorMessage(error)
+ });
+ }
+}, [user]);
// Start countdown timer for lock expiry with improved memory leak prevention
const startLockTimer = useCallback((expiresAt: Date) => {
@@ -348,7 +357,13 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
});
if (!response.ok) {
- const errorData = await response.json().catch(() => ({ message: 'Failed to claim submission' }));
+ const errorData = await response.json().catch((parseError) => {
+ logger.warn('Failed to parse claim error response', {
+ error: getErrorMessage(parseError),
+ status: response.status
+ });
+ return { message: 'Failed to claim submission' };
+ });
throw new Error(errorData.message || 'Failed to claim submission');
}
diff --git a/src/lib/supabaseClient.ts b/src/lib/supabaseClient.ts
index 5f41aeb9..2fe418b4 100644
--- a/src/lib/supabaseClient.ts
+++ b/src/lib/supabaseClient.ts
@@ -1,8 +1,65 @@
/**
- * Central Supabase Client Export
+ * Central Supabase Client Export with Automatic Breadcrumb Tracking
*
* All application code should import from this file instead of the base client.
- * This provides a central point for potential future enhancements without breaking imports.
+ * This wrapper automatically tracks all database operations as breadcrumbs for error debugging.
*/
-export { supabase } from '@/integrations/supabase/client';
+import { supabase as baseClient } from '@/integrations/supabase/client';
+import { breadcrumb } from './errorBreadcrumbs';
+
+type SupabaseClient = typeof baseClient;
+
+/**
+ * Wrap Supabase client to automatically track API calls as breadcrumbs
+ */
+function wrapSupabaseClient(client: SupabaseClient): SupabaseClient {
+ return new Proxy(client, {
+ get(target, prop: string | symbol) {
+ const value = target[prop as keyof typeof target];
+
+ // Only wrap 'from' and 'rpc' methods for database operations
+ if ((prop === 'from' || prop === 'rpc') && typeof value === 'function') {
+ return (...args: any[]) => {
+ const result = (value as any).apply(target, args);
+ const endpoint = prop === 'from' ? `/table/${args[0]}` : `/rpc/${args[0]}`;
+
+ // Return a proxy for chained query methods
+ return new Proxy(result, {
+ get(queryTarget: any, queryProp: string | symbol) {
+ const queryValue = queryTarget[queryProp];
+
+ // If it's a function, wrap it to track the call
+ if (typeof queryValue === 'function') {
+ return async (...queryArgs: any[]) => {
+ try {
+ const response = await queryValue.apply(queryTarget, queryArgs);
+
+ // Log breadcrumb after response
+ breadcrumb.apiCall(
+ endpoint,
+ String(queryProp).toUpperCase(),
+ response.error ? 400 : 200
+ );
+
+ return response;
+ } catch (error) {
+ // Log breadcrumb for exceptions
+ breadcrumb.apiCall(endpoint, String(queryProp).toUpperCase(), 500);
+ throw error;
+ }
+ };
+ }
+
+ return queryValue;
+ }
+ });
+ };
+ }
+
+ return value;
+ }
+ }) as SupabaseClient;
+}
+
+export const supabase = wrapSupabaseClient(baseClient);