mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 12:31:26 -05:00
Approve database migration
This commit is contained in:
246
docs/ERROR_TRACKING.md
Normal file
246
docs/ERROR_TRACKING.md
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
# Error Tracking System Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The error tracking system provides comprehensive monitoring and debugging capabilities for ThrillWiki. It captures detailed error context including stack traces, user action breadcrumbs, and environment information.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 1. Enhanced Error Context
|
||||||
|
|
||||||
|
Every error captured includes:
|
||||||
|
- **Stack Trace**: First 5000 characters of the error stack
|
||||||
|
- **Breadcrumbs**: Last 10 user actions before the error
|
||||||
|
- **Environment Context**: Browser/device information at error time
|
||||||
|
- **Request Metadata**: Endpoint, method, duration, status code
|
||||||
|
- **User Context**: User ID, session information
|
||||||
|
|
||||||
|
### 2. Error Monitoring Dashboard
|
||||||
|
|
||||||
|
**Location**: `/admin/error-monitoring`
|
||||||
|
|
||||||
|
**Access**: Admin/Moderator with MFA only
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Real-time error list with auto-refresh (30 seconds)
|
||||||
|
- Filter by date range (1h, 24h, 7d, 30d)
|
||||||
|
- Filter by error type
|
||||||
|
- Search by request ID, endpoint, or error message
|
||||||
|
- Error analytics (total errors, error types, affected users, avg duration)
|
||||||
|
- Top 5 errors chart
|
||||||
|
|
||||||
|
### 3. Error Details Modal
|
||||||
|
|
||||||
|
Click any error to view:
|
||||||
|
- Full request ID (copyable)
|
||||||
|
- Timestamp
|
||||||
|
- Endpoint and HTTP method
|
||||||
|
- Status code and duration
|
||||||
|
- Full error message
|
||||||
|
- Stack trace (collapsible)
|
||||||
|
- Breadcrumb trail with timestamps
|
||||||
|
- Environment context (formatted JSON)
|
||||||
|
- Link to user profile (if available)
|
||||||
|
- Copy error report button
|
||||||
|
|
||||||
|
### 4. User-Facing Error IDs
|
||||||
|
|
||||||
|
All errors shown to users include a short reference ID (first 8 characters of request UUID):
|
||||||
|
|
||||||
|
```
|
||||||
|
Error occurred
|
||||||
|
Reference ID: a3f7b2c1
|
||||||
|
```
|
||||||
|
|
||||||
|
Users can provide this ID to support for quick error lookup.
|
||||||
|
|
||||||
|
### 5. Error ID Lookup
|
||||||
|
|
||||||
|
**Location**: `/admin/error-lookup`
|
||||||
|
|
||||||
|
Quick search interface for finding errors by their reference ID. Enter the 8-character ID and get redirected to the full error details.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Breadcrumb Tracking
|
||||||
|
|
||||||
|
Breadcrumbs are automatically captured for:
|
||||||
|
- **Navigation**: Route changes
|
||||||
|
- **User Actions**: Button clicks, form submissions
|
||||||
|
- **API Calls**: Edge function and Supabase calls
|
||||||
|
- **State Changes**: Important state updates
|
||||||
|
|
||||||
|
### Environment Context
|
||||||
|
|
||||||
|
Captured automatically on error:
|
||||||
|
- Viewport dimensions
|
||||||
|
- Screen resolution
|
||||||
|
- Browser memory usage (Chrome only)
|
||||||
|
- Network connection type
|
||||||
|
- Timezone and language
|
||||||
|
- Platform information
|
||||||
|
- Storage availability
|
||||||
|
|
||||||
|
### Error Flow
|
||||||
|
|
||||||
|
1. **Error Occurs** → Error boundary or catch block
|
||||||
|
2. **Context Captured** → Breadcrumbs + environment + stack trace
|
||||||
|
3. **Logged to Database** → `request_metadata` table via RPC function
|
||||||
|
4. **User Notification** → Toast with error ID
|
||||||
|
5. **Admin Dashboard** → Real-time visibility
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### request_metadata Table
|
||||||
|
|
||||||
|
New columns added:
|
||||||
|
- `error_stack` (text): Stack trace (max 5000 chars)
|
||||||
|
- `breadcrumbs` (jsonb): Array of breadcrumb objects
|
||||||
|
- `environment_context` (jsonb): Browser/device information
|
||||||
|
|
||||||
|
### error_summary View
|
||||||
|
|
||||||
|
Aggregated error statistics:
|
||||||
|
- Error type and endpoint
|
||||||
|
- Occurrence count
|
||||||
|
- Affected users count
|
||||||
|
- First and last occurrence timestamps
|
||||||
|
- Average duration
|
||||||
|
- Recent request IDs (last 24h)
|
||||||
|
|
||||||
|
## Using the System
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
|
||||||
|
#### Adding Breadcrumbs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { breadcrumb } from '@/lib/errorBreadcrumbs';
|
||||||
|
|
||||||
|
// Navigation (automatic via App.tsx)
|
||||||
|
breadcrumb.navigation('/parks/123', '/parks');
|
||||||
|
|
||||||
|
// User action
|
||||||
|
breadcrumb.userAction('clicked submit', 'ParkForm', { parkId: '123' });
|
||||||
|
|
||||||
|
// API call
|
||||||
|
breadcrumb.apiCall('/functions/v1/detect-location', 'POST', 200);
|
||||||
|
|
||||||
|
// State change
|
||||||
|
breadcrumb.stateChange('Park data loaded', { parkId: '123' });
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Error Handling with Tracking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { handleError } from '@/lib/errorHandler';
|
||||||
|
import { trackRequest } from '@/lib/requestTracking';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await trackRequest(
|
||||||
|
{ endpoint: '/api/parks', method: 'GET' },
|
||||||
|
async (context) => {
|
||||||
|
// Your code here
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, {
|
||||||
|
action: 'Load park data',
|
||||||
|
metadata: { parkId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Support Staff
|
||||||
|
|
||||||
|
#### Finding an Error
|
||||||
|
|
||||||
|
1. User reports error with ID: `a3f7b2c1`
|
||||||
|
2. Go to `/admin/error-lookup`
|
||||||
|
3. Enter the ID
|
||||||
|
4. View full error details
|
||||||
|
|
||||||
|
#### Analyzing Error Patterns
|
||||||
|
|
||||||
|
1. Go to `/admin/error-monitoring`
|
||||||
|
2. Review analytics cards for trends
|
||||||
|
3. Check Top 5 Errors chart
|
||||||
|
4. Filter by time range to see patterns
|
||||||
|
5. Click any error for full details
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### DO:
|
||||||
|
- ✅ Always use error boundaries around risky components
|
||||||
|
- ✅ Add breadcrumbs for important user actions
|
||||||
|
- ✅ Use `trackRequest` for critical API calls
|
||||||
|
- ✅ Include context in `handleError` calls
|
||||||
|
- ✅ Check error monitoring dashboard regularly
|
||||||
|
|
||||||
|
### DON'T:
|
||||||
|
- ❌ Log sensitive data in breadcrumbs
|
||||||
|
- ❌ Add breadcrumbs in tight loops
|
||||||
|
- ❌ Ignore error IDs in user reports
|
||||||
|
- ❌ Skip error context when handling errors
|
||||||
|
- ❌ Let errors go untracked
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
- **Error tracking overhead**: < 10ms per request
|
||||||
|
- **Breadcrumb memory**: Max 10 breadcrumbs retained
|
||||||
|
- **Stack trace size**: Limited to 5000 characters
|
||||||
|
- **Database cleanup**: 30-day retention (automatic)
|
||||||
|
- **Dashboard refresh**: Every 30 seconds
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Error not appearing in dashboard
|
||||||
|
- Check if error occurred within selected time range
|
||||||
|
- Verify error type filter settings
|
||||||
|
- Try clearing search term
|
||||||
|
- Refresh the dashboard manually
|
||||||
|
|
||||||
|
### Missing breadcrumbs
|
||||||
|
- Breadcrumbs only captured for last 10 actions
|
||||||
|
- Check if breadcrumb tracking is enabled for that action type
|
||||||
|
- Verify error occurred after breadcrumbs were added
|
||||||
|
|
||||||
|
### Incomplete stack traces
|
||||||
|
- Stack traces limited to 5000 characters
|
||||||
|
- Some browsers don't provide full stacks
|
||||||
|
- Source maps not currently supported
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
**Not Included**:
|
||||||
|
- Third-party error tracking (Sentry, Rollbar)
|
||||||
|
- Session replay functionality
|
||||||
|
- Source map support for minified code
|
||||||
|
- Real-time alerting (future enhancement)
|
||||||
|
- Cross-origin error tracking
|
||||||
|
- Error rate limiting
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- AI-powered error categorization
|
||||||
|
- Automatic error assignment to team members
|
||||||
|
- GitHub Issues integration
|
||||||
|
- Slack/Discord notifications for critical errors
|
||||||
|
- Real-time WebSocket updates
|
||||||
|
- Error severity auto-detection
|
||||||
|
- Error resolution workflow
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues with the error tracking system itself:
|
||||||
|
1. Check console for tracking errors
|
||||||
|
2. Verify database connectivity
|
||||||
|
3. Check RLS policies on `request_metadata`
|
||||||
|
4. Review edge function logs
|
||||||
|
5. Contact dev team with details
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Last updated: 2025-11-03
|
||||||
|
Version: 1.0.0
|
||||||
53
src/App.tsx
53
src/App.tsx
@@ -1,11 +1,10 @@
|
|||||||
import * as React from "react";
|
import { lazy, Suspense, useEffect, useRef } from "react";
|
||||||
import { lazy, Suspense } from "react";
|
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { Toaster as Sonner } from "@/components/ui/sonner";
|
import { Toaster as Sonner } from "@/components/ui/sonner";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom";
|
||||||
import { AuthProvider } from "@/hooks/useAuth";
|
import { AuthProvider } from "@/hooks/useAuth";
|
||||||
import { AuthModalProvider } from "@/contexts/AuthModalContext";
|
import { AuthModalProvider } from "@/contexts/AuthModalContext";
|
||||||
import { LocationAutoDetectProvider } from "@/components/providers/LocationAutoDetectProvider";
|
import { LocationAutoDetectProvider } from "@/components/providers/LocationAutoDetectProvider";
|
||||||
@@ -15,6 +14,8 @@ import { PageLoader } from "@/components/loading/PageSkeletons";
|
|||||||
import { RouteErrorBoundary } from "@/components/error/RouteErrorBoundary";
|
import { RouteErrorBoundary } from "@/components/error/RouteErrorBoundary";
|
||||||
import { AdminErrorBoundary } from "@/components/error/AdminErrorBoundary";
|
import { AdminErrorBoundary } from "@/components/error/AdminErrorBoundary";
|
||||||
import { EntityErrorBoundary } from "@/components/error/EntityErrorBoundary";
|
import { EntityErrorBoundary } from "@/components/error/EntityErrorBoundary";
|
||||||
|
import { breadcrumb } from "@/lib/errorBreadcrumbs";
|
||||||
|
import { handleError } from "@/lib/errorHandler";
|
||||||
|
|
||||||
// Core routes (eager-loaded for best UX)
|
// Core routes (eager-loaded for best UX)
|
||||||
import Index from "./pages/Index";
|
import Index from "./pages/Index";
|
||||||
@@ -59,6 +60,8 @@ const AdminBlog = lazy(() => import("./pages/AdminBlog"));
|
|||||||
const AdminSettings = lazy(() => import("./pages/AdminSettings"));
|
const AdminSettings = lazy(() => import("./pages/AdminSettings"));
|
||||||
const AdminContact = lazy(() => import("./pages/admin/AdminContact"));
|
const AdminContact = lazy(() => import("./pages/admin/AdminContact"));
|
||||||
const AdminEmailSettings = lazy(() => import("./pages/admin/AdminEmailSettings"));
|
const AdminEmailSettings = lazy(() => import("./pages/admin/AdminEmailSettings"));
|
||||||
|
const ErrorMonitoring = lazy(() => import("./pages/admin/ErrorMonitoring"));
|
||||||
|
const ErrorLookup = lazy(() => import("./pages/admin/ErrorLookup"));
|
||||||
|
|
||||||
// User routes (lazy-loaded)
|
// User routes (lazy-loaded)
|
||||||
const Profile = lazy(() => import("./pages/Profile"));
|
const Profile = lazy(() => import("./pages/Profile"));
|
||||||
@@ -79,10 +82,38 @@ const queryClient = new QueryClient({
|
|||||||
staleTime: 30000, // 30 seconds - queries stay fresh for 30s
|
staleTime: 30000, // 30 seconds - queries stay fresh for 30s
|
||||||
gcTime: 5 * 60 * 1000, // 5 minutes - keep in cache for 5 mins
|
gcTime: 5 * 60 * 1000, // 5 minutes - keep in cache for 5 mins
|
||||||
},
|
},
|
||||||
|
mutations: {
|
||||||
|
onError: (error: any, variables: any, context: any) => {
|
||||||
|
// Track mutation errors with breadcrumbs
|
||||||
|
breadcrumb.apiCall(
|
||||||
|
context?.endpoint || 'mutation',
|
||||||
|
'MUTATION',
|
||||||
|
error?.status || 500
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle error with tracking
|
||||||
|
handleError(error, {
|
||||||
|
action: 'Mutation failed',
|
||||||
|
metadata: {
|
||||||
|
variables,
|
||||||
|
context,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function AppContent(): React.JSX.Element {
|
function AppContent(): React.JSX.Element {
|
||||||
|
const location = useLocation();
|
||||||
|
const prevLocation = useRef<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const from = prevLocation.current || undefined;
|
||||||
|
breadcrumb.navigation(location.pathname, from);
|
||||||
|
prevLocation.current = location.pathname;
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
@@ -304,6 +335,22 @@ function AppContent(): React.JSX.Element {
|
|||||||
</AdminErrorBoundary>
|
</AdminErrorBoundary>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/admin/error-monitoring"
|
||||||
|
element={
|
||||||
|
<AdminErrorBoundary section="Error Monitoring">
|
||||||
|
<ErrorMonitoring />
|
||||||
|
</AdminErrorBoundary>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/admin/error-lookup"
|
||||||
|
element={
|
||||||
|
<AdminErrorBoundary section="Error Lookup">
|
||||||
|
<ErrorLookup />
|
||||||
|
</AdminErrorBoundary>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Utility routes - lazy loaded */}
|
{/* Utility routes - lazy loaded */}
|
||||||
<Route path="/force-logout" element={<ForceLogout />} />
|
<Route path="/force-logout" element={<ForceLogout />} />
|
||||||
|
|||||||
83
src/components/admin/ErrorAnalytics.tsx
Normal file
83
src/components/admin/ErrorAnalytics.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
|
||||||
|
import { AlertCircle, TrendingUp, Users, Zap } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ErrorAnalyticsProps {
|
||||||
|
errorSummary: any[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorAnalytics({ errorSummary }: ErrorAnalyticsProps) {
|
||||||
|
if (!errorSummary || errorSummary.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalErrors = errorSummary.reduce((sum, item) => sum + item.occurrence_count, 0);
|
||||||
|
const totalAffectedUsers = errorSummary.reduce((sum, item) => sum + item.affected_users, 0);
|
||||||
|
const avgDuration = errorSummary.reduce((sum, item) => sum + (item.avg_duration_ms || 0), 0) / errorSummary.length;
|
||||||
|
|
||||||
|
const topErrors = errorSummary.slice(0, 5);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 md:grid-cols-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Total Errors</CardTitle>
|
||||||
|
<AlertCircle className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{totalErrors}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Last 30 days</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Error Types</CardTitle>
|
||||||
|
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{errorSummary.length}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Unique error types</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Affected Users</CardTitle>
|
||||||
|
<Users className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{totalAffectedUsers}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Users impacted</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Avg Duration</CardTitle>
|
||||||
|
<Zap className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{Math.round(avgDuration)}ms</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Before error occurs</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Top 5 Errors</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<BarChart data={topErrors}>
|
||||||
|
<XAxis dataKey="error_type" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Bar dataKey="occurrence_count" fill="hsl(var(--destructive))" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
173
src/components/admin/ErrorDetailsModal.tsx
Normal file
173
src/components/admin/ErrorDetailsModal.tsx
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Copy, ExternalLink } from 'lucide-react';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface ErrorDetailsModalProps {
|
||||||
|
error: any;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorDetailsModal({ error, onClose }: ErrorDetailsModalProps) {
|
||||||
|
const copyErrorId = () => {
|
||||||
|
navigator.clipboard.writeText(error.request_id);
|
||||||
|
toast.success('Error ID copied to clipboard');
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyErrorReport = () => {
|
||||||
|
const report = `
|
||||||
|
Error Report
|
||||||
|
============
|
||||||
|
Request ID: ${error.request_id}
|
||||||
|
Timestamp: ${format(new Date(error.created_at), 'PPpp')}
|
||||||
|
Type: ${error.error_type}
|
||||||
|
Endpoint: ${error.endpoint}
|
||||||
|
Method: ${error.method}
|
||||||
|
Status: ${error.status_code}
|
||||||
|
Duration: ${error.duration_ms}ms
|
||||||
|
|
||||||
|
Error Message:
|
||||||
|
${error.error_message}
|
||||||
|
|
||||||
|
${error.error_stack ? `Stack Trace:\n${error.error_stack}` : ''}
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(report);
|
||||||
|
toast.success('Error report copied to clipboard');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
Error Details
|
||||||
|
<Badge variant="destructive">{error.error_type}</Badge>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Tabs defaultValue="overview" className="w-full">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||||
|
<TabsTrigger value="stack">Stack Trace</TabsTrigger>
|
||||||
|
<TabsTrigger value="breadcrumbs">Breadcrumbs</TabsTrigger>
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="overview" className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Request ID</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<code className="text-sm bg-muted px-2 py-1 rounded">
|
||||||
|
{error.request_id}
|
||||||
|
</code>
|
||||||
|
<Button size="sm" variant="ghost" onClick={copyErrorId}>
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Timestamp</label>
|
||||||
|
<p className="text-sm">{format(new Date(error.created_at), 'PPpp')}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Endpoint</label>
|
||||||
|
<p className="text-sm font-mono">{error.endpoint}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Method</label>
|
||||||
|
<Badge variant="outline">{error.method}</Badge>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Status Code</label>
|
||||||
|
<p className="text-sm">{error.status_code}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Duration</label>
|
||||||
|
<p className="text-sm">{error.duration_ms}ms</p>
|
||||||
|
</div>
|
||||||
|
{error.user_id && (
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">User ID</label>
|
||||||
|
<a
|
||||||
|
href={`/admin/users?search=${error.user_id}`}
|
||||||
|
className="text-sm text-primary hover:underline flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{error.user_id.slice(0, 8)}...
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Error Message</label>
|
||||||
|
<div className="bg-muted p-4 rounded-lg mt-2">
|
||||||
|
<p className="text-sm font-mono">{error.error_message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="stack">
|
||||||
|
{error.error_stack ? (
|
||||||
|
<pre className="bg-muted p-4 rounded-lg overflow-x-auto text-xs">
|
||||||
|
{error.error_stack}
|
||||||
|
</pre>
|
||||||
|
) : (
|
||||||
|
<p className="text-muted-foreground">No stack trace available</p>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="breadcrumbs">
|
||||||
|
{error.breadcrumbs && error.breadcrumbs.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{error.breadcrumbs.map((crumb: any, index: number) => (
|
||||||
|
<div key={index} className="border-l-2 border-primary pl-4 py-2">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{crumb.category}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{format(new Date(crumb.timestamp), 'HH:mm:ss.SSS')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm">{crumb.message}</p>
|
||||||
|
{crumb.data && (
|
||||||
|
<pre className="text-xs text-muted-foreground mt-1">
|
||||||
|
{JSON.stringify(crumb.data, null, 2)}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-muted-foreground">No breadcrumbs recorded</p>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="environment">
|
||||||
|
{error.environment_context ? (
|
||||||
|
<pre className="bg-muted p-4 rounded-lg overflow-x-auto text-xs">
|
||||||
|
{JSON.stringify(error.environment_context, null, 2)}
|
||||||
|
</pre>
|
||||||
|
) : (
|
||||||
|
<p className="text-muted-foreground">No environment context available</p>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="outline" onClick={copyErrorReport}>
|
||||||
|
<Copy className="w-4 h-4 mr-2" />
|
||||||
|
Copy Report
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onClose}>Close</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -48,15 +48,19 @@ export class AdminErrorBoundary extends Component<AdminErrorBoundaryProps, Admin
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
// Generate error ID for user reference
|
||||||
|
const errorId = crypto.randomUUID();
|
||||||
|
|
||||||
logger.error('Admin panel error caught by boundary', {
|
logger.error('Admin panel error caught by boundary', {
|
||||||
section: this.props.section || 'unknown',
|
section: this.props.section || 'unknown',
|
||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
componentStack: errorInfo.componentStack,
|
componentStack: errorInfo.componentStack,
|
||||||
severity: 'high', // Admin errors are high priority
|
severity: 'high', // Admin errors are high priority
|
||||||
|
errorId,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({ errorInfo });
|
this.setState({ errorInfo, error: { ...error, errorId } as any });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRetry = () => {
|
handleRetry = () => {
|
||||||
@@ -107,6 +111,11 @@ export class AdminErrorBoundary extends Component<AdminErrorBoundaryProps, Admin
|
|||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{this.state.error?.message || 'An unexpected error occurred in the admin panel'}
|
{this.state.error?.message || 'An unexpected error occurred in the admin panel'}
|
||||||
</p>
|
</p>
|
||||||
|
{(this.state.error as any)?.errorId && (
|
||||||
|
<p className="text-xs font-mono bg-destructive/10 px-2 py-1 rounded">
|
||||||
|
Reference ID: {((this.state.error as any).errorId as string).slice(0, 8)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
This error has been logged. If the problem persists, please contact support.
|
This error has been logged. If the problem persists, please contact support.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -49,15 +49,19 @@ export class EntityErrorBoundary extends Component<EntityErrorBoundaryProps, Ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
// Generate error ID for user reference
|
||||||
|
const errorId = crypto.randomUUID();
|
||||||
|
|
||||||
logger.error('Entity page error caught by boundary', {
|
logger.error('Entity page error caught by boundary', {
|
||||||
entityType: this.props.entityType,
|
entityType: this.props.entityType,
|
||||||
entitySlug: this.props.entitySlug,
|
entitySlug: this.props.entitySlug,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
componentStack: errorInfo.componentStack,
|
componentStack: errorInfo.componentStack,
|
||||||
|
errorId,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({ errorInfo });
|
this.setState({ errorInfo, error: { ...error, errorId } as any });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRetry = () => {
|
handleRetry = () => {
|
||||||
@@ -127,6 +131,11 @@ export class EntityErrorBoundary extends Component<EntityErrorBoundaryProps, Ent
|
|||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{this.state.error?.message || `An unexpected error occurred while loading this ${entityLabel.toLowerCase()}`}
|
{this.state.error?.message || `An unexpected error occurred while loading this ${entityLabel.toLowerCase()}`}
|
||||||
</p>
|
</p>
|
||||||
|
{(this.state.error as any)?.errorId && (
|
||||||
|
<p className="text-xs font-mono bg-destructive/10 px-2 py-1 rounded">
|
||||||
|
Reference ID: {((this.state.error as any).errorId as string).slice(0, 8)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
This might be due to:
|
This might be due to:
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -49,15 +49,19 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
// Generate error ID for user reference
|
||||||
|
const errorId = crypto.randomUUID();
|
||||||
|
|
||||||
// Log error with context
|
// Log error with context
|
||||||
logger.error('Component error caught by boundary', {
|
logger.error('Component error caught by boundary', {
|
||||||
context: this.props.context || 'unknown',
|
context: this.props.context || 'unknown',
|
||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
componentStack: errorInfo.componentStack,
|
componentStack: errorInfo.componentStack,
|
||||||
|
errorId,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({ errorInfo });
|
this.setState({ errorInfo, error: { ...error, errorId } as any });
|
||||||
this.props.onError?.(error, errorInfo);
|
this.props.onError?.(error, errorInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +105,11 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|||||||
<p className="text-sm mt-2">
|
<p className="text-sm mt-2">
|
||||||
{this.state.error?.message || 'An unexpected error occurred'}
|
{this.state.error?.message || 'An unexpected error occurred'}
|
||||||
</p>
|
</p>
|
||||||
|
{(this.state.error as any)?.errorId && (
|
||||||
|
<p className="text-xs mt-2 font-mono bg-destructive/10 px-2 py-1 rounded">
|
||||||
|
Reference ID: {((this.state.error as any).errorId as string).slice(0, 8)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, Route
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
// Generate error ID for user reference
|
||||||
|
const errorId = crypto.randomUUID();
|
||||||
|
|
||||||
// Critical: Route-level error - highest priority logging
|
// Critical: Route-level error - highest priority logging
|
||||||
logger.error('Route-level error caught by boundary', {
|
logger.error('Route-level error caught by boundary', {
|
||||||
error: error.message,
|
error: error.message,
|
||||||
@@ -50,7 +53,10 @@ export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, Route
|
|||||||
componentStack: errorInfo.componentStack,
|
componentStack: errorInfo.componentStack,
|
||||||
url: window.location.href,
|
url: window.location.href,
|
||||||
severity: 'critical',
|
severity: 'critical',
|
||||||
|
errorId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.setState({ error: { ...error, errorId } as any });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReload = () => {
|
handleReload = () => {
|
||||||
@@ -78,11 +84,18 @@ export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, Route
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{import.meta.env.DEV && this.state.error && (
|
{this.state.error && (
|
||||||
<div className="p-3 bg-muted rounded-lg">
|
<div className="p-3 bg-muted rounded-lg space-y-2">
|
||||||
|
{import.meta.env.DEV && (
|
||||||
<p className="text-xs font-mono text-muted-foreground">
|
<p className="text-xs font-mono text-muted-foreground">
|
||||||
{this.state.error.message}
|
{this.state.error.message}
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
|
{(this.state.error as any)?.errorId && (
|
||||||
|
<p className="text-xs font-mono bg-destructive/10 px-2 py-1 rounded">
|
||||||
|
Reference ID: {((this.state.error as any).errorId as string).slice(0, 8)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LayoutDashboard, FileText, Flag, Users, Settings, ArrowLeft, ScrollText, BookOpen, Inbox, Mail } from 'lucide-react';
|
import { LayoutDashboard, FileText, Flag, Users, Settings, ArrowLeft, ScrollText, BookOpen, Inbox, Mail, AlertTriangle } from 'lucide-react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
import { useSidebar } from '@/hooks/useSidebar';
|
import { useSidebar } from '@/hooks/useSidebar';
|
||||||
@@ -48,6 +48,11 @@ export function AdminSidebar() {
|
|||||||
url: '/admin/system-log',
|
url: '/admin/system-log',
|
||||||
icon: ScrollText,
|
icon: ScrollText,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Error Monitoring',
|
||||||
|
url: '/admin/error-monitoring',
|
||||||
|
icon: AlertTriangle,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Users',
|
title: 'Users',
|
||||||
url: '/admin/users',
|
url: '/admin/users',
|
||||||
|
|||||||
@@ -2316,12 +2316,15 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
request_metadata: {
|
request_metadata: {
|
||||||
Row: {
|
Row: {
|
||||||
|
breadcrumbs: Json | null
|
||||||
client_version: string | null
|
client_version: string | null
|
||||||
completed_at: string | null
|
completed_at: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
duration_ms: number | null
|
duration_ms: number | null
|
||||||
endpoint: string
|
endpoint: string
|
||||||
|
environment_context: Json | null
|
||||||
error_message: string | null
|
error_message: string | null
|
||||||
|
error_stack: string | null
|
||||||
error_type: string | null
|
error_type: string | null
|
||||||
id: string
|
id: string
|
||||||
ip_address_hash: string | null
|
ip_address_hash: string | null
|
||||||
@@ -2336,12 +2339,15 @@ export type Database = {
|
|||||||
user_id: string | null
|
user_id: string | null
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
|
breadcrumbs?: Json | null
|
||||||
client_version?: string | null
|
client_version?: string | null
|
||||||
completed_at?: string | null
|
completed_at?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
duration_ms?: number | null
|
duration_ms?: number | null
|
||||||
endpoint: string
|
endpoint: string
|
||||||
|
environment_context?: Json | null
|
||||||
error_message?: string | null
|
error_message?: string | null
|
||||||
|
error_stack?: string | null
|
||||||
error_type?: string | null
|
error_type?: string | null
|
||||||
id?: string
|
id?: string
|
||||||
ip_address_hash?: string | null
|
ip_address_hash?: string | null
|
||||||
@@ -2356,12 +2362,15 @@ export type Database = {
|
|||||||
user_id?: string | null
|
user_id?: string | null
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
|
breadcrumbs?: Json | null
|
||||||
client_version?: string | null
|
client_version?: string | null
|
||||||
completed_at?: string | null
|
completed_at?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
duration_ms?: number | null
|
duration_ms?: number | null
|
||||||
endpoint?: string
|
endpoint?: string
|
||||||
|
environment_context?: Json | null
|
||||||
error_message?: string | null
|
error_message?: string | null
|
||||||
|
error_stack?: string | null
|
||||||
error_type?: string | null
|
error_type?: string | null
|
||||||
id?: string
|
id?: string
|
||||||
ip_address_hash?: string | null
|
ip_address_hash?: string | null
|
||||||
@@ -4585,6 +4594,19 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Views: {
|
Views: {
|
||||||
|
error_summary: {
|
||||||
|
Row: {
|
||||||
|
affected_users: number | null
|
||||||
|
avg_duration_ms: number | null
|
||||||
|
endpoint: string | null
|
||||||
|
error_type: string | null
|
||||||
|
first_occurred: string | null
|
||||||
|
last_occurred: string | null
|
||||||
|
occurrence_count: number | null
|
||||||
|
recent_request_ids: string[] | null
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
filtered_profiles: {
|
filtered_profiles: {
|
||||||
Row: {
|
Row: {
|
||||||
avatar_image_id: string | null
|
avatar_image_id: string | null
|
||||||
@@ -4898,7 +4920,28 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Returns: string
|
Returns: string
|
||||||
}
|
}
|
||||||
log_request_metadata: {
|
log_request_metadata:
|
||||||
|
| {
|
||||||
|
Args: {
|
||||||
|
p_breadcrumbs?: Json
|
||||||
|
p_client_version?: string
|
||||||
|
p_duration_ms?: number
|
||||||
|
p_endpoint?: string
|
||||||
|
p_environment_context?: Json
|
||||||
|
p_error_message?: string
|
||||||
|
p_error_stack?: string
|
||||||
|
p_error_type?: string
|
||||||
|
p_method?: string
|
||||||
|
p_parent_request_id?: string
|
||||||
|
p_request_id: string
|
||||||
|
p_status_code?: number
|
||||||
|
p_trace_id?: string
|
||||||
|
p_user_agent?: string
|
||||||
|
p_user_id?: string
|
||||||
|
}
|
||||||
|
Returns: undefined
|
||||||
|
}
|
||||||
|
| {
|
||||||
Args: {
|
Args: {
|
||||||
p_client_version?: string
|
p_client_version?: string
|
||||||
p_duration_ms?: number
|
p_duration_ms?: number
|
||||||
|
|||||||
64
src/lib/environmentContext.ts
Normal file
64
src/lib/environmentContext.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Environment Context Capture
|
||||||
|
* Captures browser/device information for error reports
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface EnvironmentContext {
|
||||||
|
viewport: { width: number; height: number };
|
||||||
|
screen: { width: number; height: number };
|
||||||
|
memory?: { usedJSHeapSize?: number; totalJSHeapSize?: number };
|
||||||
|
connection?: string;
|
||||||
|
timezone: string;
|
||||||
|
language: string;
|
||||||
|
platform: string;
|
||||||
|
cookiesEnabled: boolean;
|
||||||
|
localStorage: boolean;
|
||||||
|
sessionStorage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function captureEnvironmentContext(): EnvironmentContext {
|
||||||
|
const context: EnvironmentContext = {
|
||||||
|
viewport: {
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
},
|
||||||
|
screen: {
|
||||||
|
width: window.screen.width,
|
||||||
|
height: window.screen.height,
|
||||||
|
},
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
language: navigator.language,
|
||||||
|
platform: navigator.platform,
|
||||||
|
cookiesEnabled: navigator.cookieEnabled,
|
||||||
|
localStorage: isStorageAvailable('localStorage'),
|
||||||
|
sessionStorage: isStorageAvailable('sessionStorage'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Memory info (Chrome only)
|
||||||
|
if ('memory' in performance && (performance as any).memory) {
|
||||||
|
const memory = (performance as any).memory;
|
||||||
|
context.memory = {
|
||||||
|
usedJSHeapSize: memory.usedJSHeapSize,
|
||||||
|
totalJSHeapSize: memory.totalJSHeapSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection info
|
||||||
|
if ('connection' in navigator) {
|
||||||
|
context.connection = (navigator as any).connection?.effectiveType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStorageAvailable(type: 'localStorage' | 'sessionStorage'): boolean {
|
||||||
|
try {
|
||||||
|
const storage = window[type];
|
||||||
|
const test = '__storage_test__';
|
||||||
|
storage.setItem(test, test);
|
||||||
|
storage.removeItem(test);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/lib/errorBreadcrumbs.ts
Normal file
80
src/lib/errorBreadcrumbs.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Error Breadcrumb Tracking
|
||||||
|
* Captures user actions before errors occur for better debugging
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Breadcrumb {
|
||||||
|
timestamp: string;
|
||||||
|
category: 'navigation' | 'user_action' | 'api_call' | 'state_change';
|
||||||
|
message: string;
|
||||||
|
level: 'info' | 'warning' | 'error';
|
||||||
|
data?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BreadcrumbManager {
|
||||||
|
private breadcrumbs: Breadcrumb[] = [];
|
||||||
|
private readonly MAX_BREADCRUMBS = 10;
|
||||||
|
|
||||||
|
add(breadcrumb: Omit<Breadcrumb, 'timestamp'>): void {
|
||||||
|
const newBreadcrumb: Breadcrumb = {
|
||||||
|
...breadcrumb,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.breadcrumbs.push(newBreadcrumb);
|
||||||
|
|
||||||
|
// Keep only last 10 breadcrumbs
|
||||||
|
if (this.breadcrumbs.length > this.MAX_BREADCRUMBS) {
|
||||||
|
this.breadcrumbs.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(): Breadcrumb[] {
|
||||||
|
return [...this.breadcrumbs];
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.breadcrumbs = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const breadcrumbManager = new BreadcrumbManager();
|
||||||
|
|
||||||
|
// Helper functions for common breadcrumb types
|
||||||
|
export const breadcrumb = {
|
||||||
|
navigation: (to: string, from?: string) => {
|
||||||
|
breadcrumbManager.add({
|
||||||
|
category: 'navigation',
|
||||||
|
message: `Navigated to ${to}`,
|
||||||
|
level: 'info',
|
||||||
|
data: { to, from },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
userAction: (action: string, component: string, data?: Record<string, any>) => {
|
||||||
|
breadcrumbManager.add({
|
||||||
|
category: 'user_action',
|
||||||
|
message: `User ${action} in ${component}`,
|
||||||
|
level: 'info',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
apiCall: (endpoint: string, method: string, status?: number) => {
|
||||||
|
breadcrumbManager.add({
|
||||||
|
category: 'api_call',
|
||||||
|
message: `API ${method} ${endpoint}`,
|
||||||
|
level: status && status >= 400 ? 'error' : 'info',
|
||||||
|
data: { endpoint, method, status },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
stateChange: (description: string, data?: Record<string, any>) => {
|
||||||
|
breadcrumbManager.add({
|
||||||
|
category: 'state_change',
|
||||||
|
message: description,
|
||||||
|
level: 'info',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -21,7 +21,10 @@ export class AppError extends Error {
|
|||||||
export const handleError = (
|
export const handleError = (
|
||||||
error: unknown,
|
error: unknown,
|
||||||
context: ErrorContext
|
context: ErrorContext
|
||||||
): void => {
|
): string => { // Now returns error ID
|
||||||
|
const errorId = context.metadata?.requestId as string | undefined;
|
||||||
|
const shortErrorId = errorId ? errorId.slice(0, 8) : undefined;
|
||||||
|
|
||||||
const errorMessage = error instanceof AppError
|
const errorMessage = error instanceof AppError
|
||||||
? error.userMessage || error.message
|
? error.userMessage || error.message
|
||||||
: error instanceof Error
|
: error instanceof Error
|
||||||
@@ -32,14 +35,19 @@ export const handleError = (
|
|||||||
logger.error('Error occurred', {
|
logger.error('Error occurred', {
|
||||||
...context,
|
...context,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
stack: error instanceof Error ? error.stack : undefined
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
|
errorId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show user-friendly toast
|
// Show user-friendly toast with error ID
|
||||||
toast.error(context.action, {
|
toast.error(context.action, {
|
||||||
description: errorMessage,
|
description: shortErrorId
|
||||||
duration: 5000
|
? `${errorMessage}\n\nReference ID: ${shortErrorId}`
|
||||||
|
: errorMessage,
|
||||||
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return errorId || 'unknown';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleSuccess = (
|
export const handleSuccess = (
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { requestContext, type RequestContext } from './requestContext';
|
import { requestContext, type RequestContext } from './requestContext';
|
||||||
|
import { breadcrumbManager } from './errorBreadcrumbs';
|
||||||
|
import { captureEnvironmentContext } from './environmentContext';
|
||||||
|
|
||||||
export interface RequestTrackingOptions {
|
export interface RequestTrackingOptions {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
@@ -63,8 +65,16 @@ export async function trackRequest<T>(
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const duration = Date.now() - start;
|
const duration = Date.now() - start;
|
||||||
const errorInfo = error instanceof Error
|
const errorInfo = error instanceof Error
|
||||||
? { type: error.name, message: error.message }
|
? {
|
||||||
: { type: 'UnknownError', message: String(error) };
|
type: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack ? error.stack.slice(0, 5000) : undefined // Limit to 5000 chars
|
||||||
|
}
|
||||||
|
: { type: 'UnknownError', message: String(error), stack: undefined };
|
||||||
|
|
||||||
|
// Capture breadcrumbs and environment
|
||||||
|
const breadcrumbs = breadcrumbManager.getAll();
|
||||||
|
const environment = captureEnvironmentContext();
|
||||||
|
|
||||||
// Log error to database (fire and forget)
|
// Log error to database (fire and forget)
|
||||||
logRequestMetadata({
|
logRequestMetadata({
|
||||||
@@ -76,6 +86,9 @@ export async function trackRequest<T>(
|
|||||||
duration,
|
duration,
|
||||||
errorType: errorInfo.type,
|
errorType: errorInfo.type,
|
||||||
errorMessage: errorInfo.message,
|
errorMessage: errorInfo.message,
|
||||||
|
errorStack: errorInfo.stack,
|
||||||
|
breadcrumbs,
|
||||||
|
environmentContext: environment,
|
||||||
userAgent: context.userAgent,
|
userAgent: context.userAgent,
|
||||||
clientVersion: context.clientVersion,
|
clientVersion: context.clientVersion,
|
||||||
parentRequestId: options.parentRequestId,
|
parentRequestId: options.parentRequestId,
|
||||||
@@ -100,6 +113,9 @@ interface RequestMetadata {
|
|||||||
duration: number;
|
duration: number;
|
||||||
errorType?: string;
|
errorType?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
|
errorStack?: string;
|
||||||
|
breadcrumbs?: any[];
|
||||||
|
environmentContext?: any;
|
||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
clientVersion?: string;
|
clientVersion?: string;
|
||||||
parentRequestId?: string;
|
parentRequestId?: string;
|
||||||
@@ -117,6 +133,9 @@ async function logRequestMetadata(metadata: RequestMetadata): Promise<void> {
|
|||||||
p_duration_ms: metadata.duration,
|
p_duration_ms: metadata.duration,
|
||||||
p_error_type: metadata.errorType ?? undefined,
|
p_error_type: metadata.errorType ?? undefined,
|
||||||
p_error_message: metadata.errorMessage ?? undefined,
|
p_error_message: metadata.errorMessage ?? undefined,
|
||||||
|
p_error_stack: metadata.errorStack ?? undefined,
|
||||||
|
p_breadcrumbs: metadata.breadcrumbs ? JSON.stringify(metadata.breadcrumbs) : '[]',
|
||||||
|
p_environment_context: metadata.environmentContext ? JSON.stringify(metadata.environmentContext) : '{}',
|
||||||
p_user_agent: metadata.userAgent ?? undefined,
|
p_user_agent: metadata.userAgent ?? undefined,
|
||||||
p_client_version: metadata.clientVersion ?? undefined,
|
p_client_version: metadata.clientVersion ?? undefined,
|
||||||
p_parent_request_id: metadata.parentRequestId ?? undefined,
|
p_parent_request_id: metadata.parentRequestId ?? undefined,
|
||||||
|
|||||||
87
src/pages/admin/ErrorLookup.tsx
Normal file
87
src/pages/admin/ErrorLookup.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { AdminLayout } from '@/components/layout/AdminLayout';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Search } from 'lucide-react';
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export default function ErrorLookup() {
|
||||||
|
const [errorId, setErrorId] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
if (!errorId.trim()) {
|
||||||
|
toast.error('Please enter an error ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// Search by partial or full request ID
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('request_metadata')
|
||||||
|
.select('*')
|
||||||
|
.ilike('request_id', `${errorId}%`)
|
||||||
|
.not('error_type', 'is', null)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error || !data) {
|
||||||
|
toast.error('Error not found', {
|
||||||
|
description: 'No error found with this ID. Please check the ID and try again.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to error monitoring with this error pre-selected
|
||||||
|
navigate('/admin/error-monitoring', { state: { selectedErrorId: data.request_id } });
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('Search failed', {
|
||||||
|
description: 'An error occurred while searching. Please try again.',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminLayout>
|
||||||
|
<div className="max-w-2xl mx-auto space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">Error Lookup</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Search for specific errors by their reference ID
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Search by Error ID</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter the error reference ID provided to users (first 8 characters are sufficient)
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="e.g., a3f7b2c1"
|
||||||
|
value={errorId}
|
||||||
|
onChange={(e) => setErrorId(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
|
className="font-mono"
|
||||||
|
/>
|
||||||
|
<Button onClick={handleSearch} disabled={loading}>
|
||||||
|
<Search className="w-4 h-4 mr-2" />
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</AdminLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
175
src/pages/admin/ErrorMonitoring.tsx
Normal file
175
src/pages/admin/ErrorMonitoring.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { AdminLayout } from '@/components/layout/AdminLayout';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { AlertCircle, RefreshCw } from 'lucide-react';
|
||||||
|
import { ErrorDetailsModal } from '@/components/admin/ErrorDetailsModal';
|
||||||
|
import { ErrorAnalytics } from '@/components/admin/ErrorAnalytics';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
export default function ErrorMonitoring() {
|
||||||
|
const [selectedError, setSelectedError] = useState<any>(null);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [errorTypeFilter, setErrorTypeFilter] = useState<string>('all');
|
||||||
|
const [dateRange, setDateRange] = useState<'1h' | '24h' | '7d' | '30d'>('24h');
|
||||||
|
|
||||||
|
// Fetch recent errors
|
||||||
|
const { data: errors, isLoading, refetch } = useQuery({
|
||||||
|
queryKey: ['admin-errors', dateRange, errorTypeFilter, searchTerm],
|
||||||
|
queryFn: async () => {
|
||||||
|
const dateMap = {
|
||||||
|
'1h': '1 hour',
|
||||||
|
'24h': '1 day',
|
||||||
|
'7d': '7 days',
|
||||||
|
'30d': '30 days',
|
||||||
|
};
|
||||||
|
|
||||||
|
let query = supabase
|
||||||
|
.from('request_metadata')
|
||||||
|
.select('*')
|
||||||
|
.not('error_type', 'is', null)
|
||||||
|
.gte('created_at', `now() - interval '${dateMap[dateRange]}'`)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(100);
|
||||||
|
|
||||||
|
if (errorTypeFilter !== 'all') {
|
||||||
|
query = query.eq('error_type', errorTypeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
query = query.or(`request_id.ilike.%${searchTerm}%,error_message.ilike.%${searchTerm}%,endpoint.ilike.%${searchTerm}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await query;
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
refetchInterval: 30000, // Auto-refresh every 30 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch error summary
|
||||||
|
const { data: errorSummary } = useQuery({
|
||||||
|
queryKey: ['error-summary'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('error_summary')
|
||||||
|
.select('*');
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminLayout>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">Error Monitoring</h1>
|
||||||
|
<p className="text-muted-foreground">Track and analyze application errors</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => refetch()} variant="outline" size="sm">
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Analytics Section */}
|
||||||
|
<ErrorAnalytics errorSummary={errorSummary} />
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Error Log</CardTitle>
|
||||||
|
<CardDescription>Recent errors across the application</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex gap-4 mb-6">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
placeholder="Search by request ID, endpoint, or error message..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Select value={dateRange} onValueChange={(v: any) => setDateRange(v)}>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1h">Last Hour</SelectItem>
|
||||||
|
<SelectItem value="24h">Last 24 Hours</SelectItem>
|
||||||
|
<SelectItem value="7d">Last 7 Days</SelectItem>
|
||||||
|
<SelectItem value="30d">Last 30 Days</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select value={errorTypeFilter} onValueChange={setErrorTypeFilter}>
|
||||||
|
<SelectTrigger className="w-[200px]">
|
||||||
|
<SelectValue placeholder="Error type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Types</SelectItem>
|
||||||
|
<SelectItem value="FunctionsFetchError">Functions Fetch</SelectItem>
|
||||||
|
<SelectItem value="FunctionsHttpError">Functions HTTP</SelectItem>
|
||||||
|
<SelectItem value="Error">Generic Error</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error List */}
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">Loading errors...</div>
|
||||||
|
) : errors && errors.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{errors.map((error) => (
|
||||||
|
<div
|
||||||
|
key={error.id}
|
||||||
|
onClick={() => setSelectedError(error)}
|
||||||
|
className="p-4 border rounded-lg hover:bg-accent cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<AlertCircle className="w-4 h-4 text-destructive" />
|
||||||
|
<span className="font-medium">{error.error_type}</span>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{error.endpoint}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">
|
||||||
|
{error.error_message}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||||
|
<span>ID: {error.request_id.slice(0, 8)}</span>
|
||||||
|
<span>{format(new Date(error.created_at), 'PPp')}</span>
|
||||||
|
<span>{error.duration_ms}ms</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
No errors found for the selected filters
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Details Modal */}
|
||||||
|
{selectedError && (
|
||||||
|
<ErrorDetailsModal
|
||||||
|
error={selectedError}
|
||||||
|
onClose={() => setSelectedError(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AdminLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
-- Phase 1: Enhanced Error Tracking Schema
|
||||||
|
-- Add columns for enhanced error context to request_metadata table
|
||||||
|
|
||||||
|
-- Add new columns for error tracking
|
||||||
|
ALTER TABLE request_metadata
|
||||||
|
ADD COLUMN IF NOT EXISTS error_stack text,
|
||||||
|
ADD COLUMN IF NOT EXISTS breadcrumbs jsonb DEFAULT '[]'::jsonb,
|
||||||
|
ADD COLUMN IF NOT EXISTS environment_context jsonb DEFAULT '{}'::jsonb;
|
||||||
|
|
||||||
|
-- Add comments to document new columns
|
||||||
|
COMMENT ON COLUMN request_metadata.error_stack IS 'Stack trace of the error (first 5000 chars)';
|
||||||
|
COMMENT ON COLUMN request_metadata.breadcrumbs IS 'User action trail before error occurred (last 10 actions)';
|
||||||
|
COMMENT ON COLUMN request_metadata.environment_context IS 'Browser/device context when error occurred';
|
||||||
|
|
||||||
|
-- Index for error grouping and analysis
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_request_metadata_error_grouping
|
||||||
|
ON request_metadata(error_type, endpoint, created_at DESC)
|
||||||
|
WHERE error_type IS NOT NULL;
|
||||||
|
|
||||||
|
-- Index for recent errors query (dashboard)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_request_metadata_recent_errors
|
||||||
|
ON request_metadata(created_at DESC)
|
||||||
|
WHERE error_type IS NOT NULL;
|
||||||
|
|
||||||
|
-- Update log_request_metadata RPC function to accept new fields
|
||||||
|
CREATE OR REPLACE FUNCTION log_request_metadata(
|
||||||
|
p_request_id uuid,
|
||||||
|
p_user_id uuid DEFAULT NULL,
|
||||||
|
p_endpoint text DEFAULT NULL,
|
||||||
|
p_method text DEFAULT NULL,
|
||||||
|
p_status_code integer DEFAULT NULL,
|
||||||
|
p_duration_ms integer DEFAULT NULL,
|
||||||
|
p_error_type text DEFAULT NULL,
|
||||||
|
p_error_message text DEFAULT NULL,
|
||||||
|
p_user_agent text DEFAULT NULL,
|
||||||
|
p_client_version text DEFAULT NULL,
|
||||||
|
p_parent_request_id uuid DEFAULT NULL,
|
||||||
|
p_trace_id uuid DEFAULT NULL,
|
||||||
|
p_error_stack text DEFAULT NULL,
|
||||||
|
p_breadcrumbs jsonb DEFAULT '[]'::jsonb,
|
||||||
|
p_environment_context jsonb DEFAULT '{}'::jsonb
|
||||||
|
)
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO request_metadata (
|
||||||
|
request_id, user_id, endpoint, method, status_code, duration_ms,
|
||||||
|
error_type, error_message, user_agent, client_version,
|
||||||
|
parent_request_id, trace_id, error_stack, breadcrumbs, environment_context
|
||||||
|
) VALUES (
|
||||||
|
p_request_id, p_user_id, p_endpoint, p_method, p_status_code, p_duration_ms,
|
||||||
|
p_error_type, p_error_message, p_user_agent, p_client_version,
|
||||||
|
p_parent_request_id, p_trace_id, p_error_stack, p_breadcrumbs, p_environment_context
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Create error_summary view for aggregation
|
||||||
|
CREATE OR REPLACE VIEW error_summary AS
|
||||||
|
SELECT
|
||||||
|
error_type,
|
||||||
|
endpoint,
|
||||||
|
COUNT(*) as occurrence_count,
|
||||||
|
COUNT(DISTINCT user_id) as affected_users,
|
||||||
|
MAX(created_at) as last_occurred,
|
||||||
|
MIN(created_at) as first_occurred,
|
||||||
|
ROUND(AVG(duration_ms)::numeric, 2) as avg_duration_ms,
|
||||||
|
(ARRAY_AGG(request_id ORDER BY created_at DESC)
|
||||||
|
FILTER (WHERE created_at > now() - interval '1 day')
|
||||||
|
)[1:5] as recent_request_ids
|
||||||
|
FROM request_metadata
|
||||||
|
WHERE error_type IS NOT NULL
|
||||||
|
AND created_at > now() - interval '30 days'
|
||||||
|
GROUP BY error_type, endpoint
|
||||||
|
ORDER BY occurrence_count DESC;
|
||||||
|
|
||||||
|
COMMENT ON VIEW error_summary IS 'Aggregated error statistics for monitoring dashboard';
|
||||||
|
|
||||||
|
-- Grant access to authenticated users
|
||||||
|
GRANT SELECT ON error_summary TO authenticated;
|
||||||
Reference in New Issue
Block a user