import { z } from "zod"; import prisma from "~/server/utils/prisma"; // Query parameter validation schema const querySchema = z.object({ period: z.enum(["hour", "day", "week", "month"]).default("day"), metric: z.enum(["throughput", "error_rate", "response_time"]).default("throughput"), }); export default defineEventHandler(async (event) => { try { // Parse and validate query parameters const query = getQuery(event); const { period, metric } = querySchema.parse(query); // Set time range based on period const now = new Date(); let startDate; let intervalMinutes; let numPoints; switch (period) { case "hour": startDate = new Date(now.getTime() - 60 * 60 * 1000); // 1 hour ago intervalMinutes = 1; // 1-minute intervals numPoints = 60; break; case "day": startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 24 hours ago intervalMinutes = 60; // 1-hour intervals numPoints = 24; break; case "week": startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7 days ago intervalMinutes = 24 * 60; // 1-day intervals numPoints = 7; break; case "month": startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); // 30 days ago intervalMinutes = 24 * 60; // 1-day intervals numPoints = 30; break; default: startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); intervalMinutes = 60; numPoints = 24; } const dataPoints = []; // Generate time buckets for (let i = 0; i < numPoints; i++) { const bucketStart = new Date(startDate.getTime() + i * intervalMinutes * 60 * 1000); const bucketEnd = new Date(bucketStart.getTime() + intervalMinutes * 60 * 1000); let value = 0; switch (metric) { case "throughput": { // Count completed jobs in this time bucket const count = await prisma.notification_queue.count({ where: { status: "completed", updated_at: { gte: bucketStart, lt: bucketEnd, }, }, }); value = count; break; } case "error_rate": { // Calculate error rate in this time bucket const [totalJobs, failedJobs] = await Promise.all([ prisma.notification_queue.count({ where: { OR: [{ status: "completed" }, { status: "failed" }], updated_at: { gte: bucketStart, lt: bucketEnd, }, }, }), prisma.notification_queue.count({ where: { status: "failed", updated_at: { gte: bucketStart, lt: bucketEnd, }, }, }), ]); value = totalJobs > 0 ? Math.round((failedJobs / totalJobs) * 100) : 0; break; } case "response_time": { // Calculate average response time in this time bucket const jobs = await prisma.notification_queue.findMany({ where: { status: "completed", updated_at: { gte: bucketStart, lt: bucketEnd, }, last_attempt_at: { not: null }, }, select: { created_at: true, last_attempt_at: true, }, take: 50, // Sample size }); if (jobs.length > 0) { const responseTimes = jobs.map(job => { const diff = new Date(job.last_attempt_at).getTime() - new Date(job.created_at).getTime(); return Math.abs(diff); }); value = Math.round(responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length); } else { value = 0; } break; } } dataPoints.push({ timestamp: bucketStart.toISOString(), value, }); } // Calculate summary statistics const values = dataPoints.map(p => p.value); const min = Math.min(...values); const max = Math.max(...values); const avg = Math.round(values.reduce((sum, val) => sum + val, 0) / values.length); // Calculate trend (compare first half vs second half) const midpoint = Math.floor(values.length / 2); const firstHalfAvg = values.slice(0, midpoint).reduce((sum, val) => sum + val, 0) / midpoint; const secondHalfAvg = values.slice(midpoint).reduce((sum, val) => sum + val, 0) / (values.length - midpoint); const trend = secondHalfAvg > firstHalfAvg * 1.1 ? "increasing" : secondHalfAvg < firstHalfAvg * 0.9 ? "decreasing" : "stable"; return { success: true, data: { metric, period, dataPoints, summary: { min, max, avg, trend, }, }, }; } catch (error) { console.error("Error fetching queue history:", error); if (error.name === "ZodError") { throw createError({ statusCode: 400, statusMessage: "Invalid query parameters", data: { errors: error.errors, }, }); } throw createError({ statusCode: 500, statusMessage: "Failed to fetch queue history", data: { error: error.message, }, }); } finally { } });