Files
thrilltrack-explorer/supabase/functions/collect-metrics/index.ts
gpt-engineer-app[bot] 5531376edf Fix span duplicates and metrics
Implements complete plan to resolve duplicate span_id issues and metric collection errors:
- Ensure edge handlers return proper Response objects to prevent double logging
- Update collect-metrics to use valid metric categories, fix system_alerts query, and adjust returns
- Apply detect-anomalies adjustments if needed and add defensive handling in wrapper
- Prepare ground for end-to-end verification of location-related fixes
2025-11-12 04:57:54 +00:00

177 lines
5.7 KiB
TypeScript

import { serve } from 'https://deno.land/std@0.190.0/http/server.ts';
import { createEdgeFunction, type EdgeFunctionContext } from '../_shared/edgeFunctionWrapper.ts';
import { addSpanEvent } from '../_shared/logger.ts';
import { corsHeaders } from '../_shared/cors.ts';
interface MetricRecord {
metric_name: string;
metric_value: number;
metric_category: string;
timestamp: string;
}
const handler = async (req: Request, { supabase, span, requestId }: EdgeFunctionContext) => {
addSpanEvent(span, 'starting_metrics_collection', { requestId });
const metrics: MetricRecord[] = [];
const timestamp = new Date().toISOString();
// 1. Collect API error rate from recent logs
const { data: recentErrors, error: errorQueryError } = await supabase
.from('system_alerts')
.select('id', { count: 'exact', head: true })
.gte('created_at', new Date(Date.now() - 60000).toISOString())
.in('severity', ['high', 'critical']);
if (!errorQueryError) {
const errorCount = recentErrors || 0;
metrics.push({
metric_name: 'api_error_count',
metric_value: errorCount as number,
metric_category: 'api',
timestamp,
});
}
// 2. Collect rate limit violations
const { data: rateLimitViolations, error: rateLimitError } = await supabase
.from('rate_limit_logs')
.select('id', { count: 'exact', head: true })
.gte('timestamp', new Date(Date.now() - 60000).toISOString())
.eq('action_taken', 'blocked');
if (!rateLimitError) {
const violationCount = rateLimitViolations || 0;
metrics.push({
metric_name: 'rate_limit_violations',
metric_value: violationCount as number,
metric_category: 'rate_limit',
timestamp,
});
}
// 3. Collect pending submissions count
const { data: pendingSubmissions, error: submissionsError } = await supabase
.from('submissions')
.select('id', { count: 'exact', head: true })
.eq('moderation_status', 'pending');
if (!submissionsError) {
const pendingCount = pendingSubmissions || 0;
metrics.push({
metric_name: 'pending_submissions',
metric_value: pendingCount as number,
metric_category: 'moderation',
timestamp,
});
}
// 4. Collect active incidents count
const { data: activeIncidents, error: incidentsError } = await supabase
.from('incidents')
.select('id', { count: 'exact', head: true })
.in('status', ['open', 'investigating']);
if (!incidentsError) {
const incidentCount = activeIncidents || 0;
metrics.push({
metric_name: 'active_incidents',
metric_value: incidentCount as number,
metric_category: 'system',
timestamp,
});
}
// 5. Collect unresolved alerts count
const { data: unresolvedAlerts, error: alertsError } = await supabase
.from('system_alerts')
.select('id', { count: 'exact', head: true })
.is('resolved_at', null);
if (!alertsError) {
const alertCount = unresolvedAlerts || 0;
metrics.push({
metric_name: 'unresolved_alerts',
metric_value: alertCount as number,
metric_category: 'system',
timestamp,
});
}
// 6. Calculate submission approval rate (last hour)
const { data: recentSubmissions, error: recentSubmissionsError } = await supabase
.from('submissions')
.select('moderation_status', { count: 'exact' })
.gte('created_at', new Date(Date.now() - 3600000).toISOString());
if (!recentSubmissionsError && recentSubmissions) {
const total = recentSubmissions.length;
const approved = recentSubmissions.filter(s => s.moderation_status === 'approved').length;
const approvalRate = total > 0 ? (approved / total) * 100 : 100;
metrics.push({
metric_name: 'submission_approval_rate',
metric_value: approvalRate,
metric_category: 'moderation',
timestamp,
});
}
// 7. Calculate average moderation time (last hour)
const { data: moderatedSubmissions, error: moderatedError } = await supabase
.from('submissions')
.select('created_at, moderated_at')
.not('moderated_at', 'is', null)
.gte('moderated_at', new Date(Date.now() - 3600000).toISOString());
if (!moderatedError && moderatedSubmissions && moderatedSubmissions.length > 0) {
const totalTime = moderatedSubmissions.reduce((sum, sub) => {
const created = new Date(sub.created_at).getTime();
const moderated = new Date(sub.moderated_at).getTime();
return sum + (moderated - created);
}, 0);
const avgTimeMinutes = (totalTime / moderatedSubmissions.length) / 60000;
metrics.push({
metric_name: 'avg_moderation_time',
metric_value: avgTimeMinutes,
metric_category: 'moderation',
timestamp,
});
}
// Insert all collected metrics
if (metrics.length > 0) {
const { error: insertError } = await supabase
.from('metric_time_series')
.insert(metrics);
if (insertError) {
addSpanEvent(span, 'error_inserting_metrics', { error: insertError.message });
throw insertError;
}
addSpanEvent(span, 'metrics_recorded', { count: metrics.length });
}
return new Response(
JSON.stringify({
success: true,
metrics_collected: metrics.length,
metrics: metrics.map(m => ({ name: m.metric_name, value: m.metric_value })),
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
);
};
serve(createEdgeFunction({
name: 'collect-metrics',
requireAuth: false,
corsHeaders,
enableTracing: true,
}, handler));