mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 16:51:13 -05:00
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.
374 lines
15 KiB
TypeScript
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>
|
|
);
|
|
}
|