Files
thrilltrack-explorer/src/pages/admin/ErrorMonitoring.tsx
gpt-engineer-app[bot] d903e96e13 Implement pipeline monitoring alerts
Approve and implement the Supabase migration for the pipeline monitoring alert system. This includes expanding alert types, adding new monitoring functions, and updating existing ones with escalating thresholds.
2025-11-07 05:05:32 +00:00

374 lines
15 KiB
TypeScript

import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/lib/supabaseClient';
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 { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { AlertCircle, XCircle } from 'lucide-react';
import { RefreshButton } from '@/components/ui/refresh-button';
import { ErrorDetailsModal } from '@/components/admin/ErrorDetailsModal';
import { ApprovalFailureModal } from '@/components/admin/ApprovalFailureModal';
import { ErrorAnalytics } from '@/components/admin/ErrorAnalytics';
import { PipelineHealthAlerts } from '@/components/admin/PipelineHealthAlerts';
import { format } from 'date-fns';
// Helper to calculate date threshold for filtering
const getDateThreshold = (range: '1h' | '24h' | '7d' | '30d'): string => {
const now = new Date();
const msMap = {
'1h': 60 * 60 * 1000, // 1 hour in milliseconds
'24h': 24 * 60 * 60 * 1000, // 1 day
'7d': 7 * 24 * 60 * 60 * 1000, // 7 days
'30d': 30 * 24 * 60 * 60 * 1000, // 30 days
};
const threshold = new Date(now.getTime() - msMap[range]);
return threshold.toISOString();
};
interface EnrichedApprovalFailure {
id: string;
submission_id: string;
moderator_id: string;
submitter_id: string;
items_count: number;
duration_ms: number | null;
error_message: string | null;
request_id: string | null;
rollback_triggered: boolean | null;
created_at: string | null;
success: boolean;
moderator?: {
user_id: string;
username: string | null;
avatar_url: string | null;
};
submission?: {
id: string;
submission_type: string;
user_id: string;
};
}
export default function ErrorMonitoring() {
const [selectedError, setSelectedError] = useState<any>(null);
const [selectedFailure, setSelectedFailure] = 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, isFetching } = useQuery({
queryKey: ['admin-errors', dateRange, errorTypeFilter, searchTerm],
queryFn: async () => {
let query = supabase
.from('request_metadata')
.select(`
*,
request_breadcrumbs(
timestamp,
category,
message,
level,
sequence_order
)
`)
.not('error_type', 'is', null)
.gte('created_at', getDateThreshold(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;
},
});
// Fetch approval metrics (last 24h)
const { data: approvalMetrics } = useQuery({
queryKey: ['approval-metrics'],
queryFn: async () => {
const { data, error } = await supabase
.from('approval_transaction_metrics')
.select('id, success, duration_ms, created_at')
.gte('created_at', getDateThreshold('24h'))
.order('created_at', { ascending: false })
.limit(1000);
if (error) throw error;
return data;
},
});
// Fetch approval failures
const { data: approvalFailures, refetch: refetchFailures, isFetching: isFetchingFailures } = useQuery<EnrichedApprovalFailure[]>({
queryKey: ['approval-failures', dateRange, searchTerm],
queryFn: async () => {
let query = supabase
.from('approval_transaction_metrics')
.select('*')
.eq('success', false)
.gte('created_at', getDateThreshold(dateRange))
.order('created_at', { ascending: false })
.limit(50);
if (searchTerm) {
query = query.or(`submission_id.ilike.%${searchTerm}%,error_message.ilike.%${searchTerm}%`);
}
const { data, error } = await query;
if (error) throw error;
// Fetch moderator and submission data separately
if (data && data.length > 0) {
const moderatorIds = [...new Set(data.map(f => f.moderator_id))];
const submissionIds = [...new Set(data.map(f => f.submission_id))];
const [moderatorsData, submissionsData] = await Promise.all([
supabase.from('profiles').select('user_id, username, avatar_url').in('user_id', moderatorIds),
supabase.from('content_submissions').select('id, submission_type, user_id').in('id', submissionIds)
]);
// Enrich data with moderator and submission info
return data.map(failure => ({
...failure,
moderator: moderatorsData.data?.find(m => m.user_id === failure.moderator_id),
submission: submissionsData.data?.find(s => s.id === failure.submission_id)
})) as EnrichedApprovalFailure[];
}
return (data || []) as EnrichedApprovalFailure[];
},
refetchInterval: 30000,
});
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>
<RefreshButton
onRefresh={async () => { await refetch(); }}
isLoading={isFetching}
variant="outline"
size="sm"
/>
</div>
{/* Pipeline Health Alerts */}
<PipelineHealthAlerts />
{/* Analytics Section */}
<ErrorAnalytics errorSummary={errorSummary} approvalMetrics={approvalMetrics} />
{/* Tabs for Errors and Approval Failures */}
<Tabs defaultValue="errors" className="w-full">
<TabsList>
<TabsTrigger value="errors">Application Errors</TabsTrigger>
<TabsTrigger value="approvals">Approval Failures</TabsTrigger>
</TabsList>
<TabsContent value="errors" className="space-y-4">
<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>
{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>
{error.duration_ms != null && <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>
</TabsContent>
<TabsContent value="approvals" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Approval Failures</CardTitle>
<CardDescription>Failed approval transactions requiring investigation</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-4 mb-6">
<div className="flex-1">
<Input
placeholder="Search by submission ID 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>
</div>
{isFetchingFailures ? (
<div className="text-center py-8 text-muted-foreground">Loading approval failures...</div>
) : approvalFailures && approvalFailures.length > 0 ? (
<div className="space-y-2">
{approvalFailures.map((failure) => (
<div
key={failure.id}
onClick={() => setSelectedFailure(failure)}
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">
<XCircle className="w-4 h-4 text-destructive" />
<span className="font-medium">Approval Failed</span>
<Badge variant="outline" className="text-xs">
{failure.submission?.submission_type || 'Unknown'}
</Badge>
{failure.rollback_triggered && (
<Badge variant="destructive" className="text-xs">
Rollback
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground mb-2">
{failure.error_message || 'No error message available'}
</p>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span>Moderator: {failure.moderator?.username || 'Unknown'}</span>
<span>{failure.created_at && format(new Date(failure.created_at), 'PPp')}</span>
{failure.duration_ms != null && <span>{failure.duration_ms}ms</span>}
<span>{failure.items_count} items</span>
</div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
No approval failures found for the selected filters
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
{/* Error Details Modal */}
{selectedError && (
<ErrorDetailsModal
error={selectedError}
onClose={() => setSelectedError(null)}
/>
)}
{/* Approval Failure Modal */}
{selectedFailure && (
<ApprovalFailureModal
failure={selectedFailure}
onClose={() => setSelectedFailure(null)}
/>
)}
</AdminLayout>
);
}