import React, { useState, useEffect } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, } from "recharts"; import { Loader2, AlertTriangle } from "lucide-react"; import { Tooltip as UITooltip, TooltipContent, TooltipTrigger, TooltipProvider, } from "@/components/ui/tooltip"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Table, TableHeader, TableHead, TableBody, TableRow, TableCell, } from "@/components/ui/table"; import { googleAnalyticsService } from "../../services/googleAnalyticsService"; import { format } from "date-fns"; const formatNumber = (value, decimalPlaces = 0) => { return new Intl.NumberFormat("en-US", { minimumFractionDigits: decimalPlaces, maximumFractionDigits: decimalPlaces, }).format(value || 0); }; const formatPercent = (value, decimalPlaces = 1) => `${(value || 0).toFixed(decimalPlaces)}%`; const summaryCard = (label, sublabel, value, options = {}) => { const { isMonetary = false, isPercentage = false, decimalPlaces = 0, } = options; let displayValue; if (isMonetary) { displayValue = formatCurrency(value, decimalPlaces); } else if (isPercentage) { displayValue = formatPercent(value, decimalPlaces); } else { displayValue = formatNumber(value, decimalPlaces); } return (
{label}
{displayValue}
{sublabel}
); }; const QuotaInfo = ({ tokenQuota }) => { // Add early return if tokenQuota is null or undefined if (!tokenQuota || typeof tokenQuota !== "object") return null; const { projectHourly = {}, daily = {}, serverErrors = {}, thresholdedRequests = {}, } = tokenQuota; // Add null checks and default values for all properties const { remaining: projectHourlyRemaining = 0, consumed: projectHourlyConsumed = 0, } = projectHourly; const { remaining: dailyRemaining = 0, consumed: dailyConsumed = 0 } = daily; const { remaining: errorsRemaining = 10, consumed: errorsConsumed = 0 } = serverErrors; const { remaining: thresholdRemaining = 120, consumed: thresholdConsumed = 0, } = thresholdedRequests; // Calculate percentages with safe math const hourlyPercentage = ((projectHourlyRemaining / 14000) * 100).toFixed(1); const dailyPercentage = ((dailyRemaining / 200000) * 100).toFixed(1); const errorPercentage = ((errorsRemaining / 10) * 100).toFixed(1); const thresholdPercentage = ((thresholdRemaining / 120) * 100).toFixed(1); // Determine color based on remaining percentage const getStatusColor = (percentage) => { const numericPercentage = parseFloat(percentage); if (isNaN(numericPercentage) || numericPercentage < 20) return "text-red-500 dark:text-red-400"; if (numericPercentage < 40) return "text-yellow-500 dark:text-yellow-400"; return "text-green-500 dark:text-green-400"; }; return (
Quota: {hourlyPercentage}%
Project Hourly
{projectHourlyRemaining.toLocaleString()} / 14,000 remaining
Daily
{dailyRemaining.toLocaleString()} / 200,000 remaining
Server Errors
{errorsConsumed} / 10 used this hour
Thresholded Requests
{thresholdConsumed} / 120 used this hour
); }; const RealtimeAnalytics = () => { const [basicData, setBasicData] = useState({ last30MinUsers: 0, last5MinUsers: 0, byMinute: [], tokenQuota: null, lastUpdated: null, }); const [detailedData, setDetailedData] = useState({ currentPages: [], sources: [], recentEvents: [], lastUpdated: null, }); const [loading, setLoading] = useState(true); const [isPaused, setIsPaused] = useState(false); const [error, setError] = useState(null); useEffect(() => { let basicInterval; let detailedInterval; const fetchBasicData = async () => { if (isPaused) return; try { const response = await fetch("/api/analytics/realtime/basic", { credentials: "include", }); const result = await response.json(); const processed = await googleAnalyticsService.getRealTimeBasicData(); setBasicData(processed); setError(null); } catch (error) { console.error("Error details:", { message: error.message, stack: error.stack, response: error.response, }); if (error.message === "QUOTA_EXCEEDED") { setError("Quota exceeded. Analytics paused until manually resumed."); setIsPaused(true); } else { setError("Failed to fetch analytics data"); } } }; const fetchDetailedData = async () => { if (isPaused) return; try { const result = await googleAnalyticsService.getRealTimeDetailedData(); setDetailedData(result); } catch (error) { console.error("Failed to fetch detailed realtime data:", error); if (error.message === "QUOTA_EXCEEDED") { setError("Quota exceeded. Analytics paused until manually resumed."); setIsPaused(true); } else { setError("Failed to fetch analytics data"); } } finally { setLoading(false); } }; // Initial fetches fetchBasicData(); fetchDetailedData(); // Set up intervals basicInterval = setInterval(fetchBasicData, 30000); // 30 seconds detailedInterval = setInterval(fetchDetailedData, 300000); // 5 minutes return () => { clearInterval(basicInterval); clearInterval(detailedInterval); }; }, []); const togglePause = () => { setIsPaused(!isPaused); }; if (loading && !basicData && !detailedData) { return ( ); } // Pie chart colors const COLORS = [ "#8b5cf6", "#10b981", "#f59e0b", "#3b82f6", "#0088FE", "#00C49F", "#FFBB28", ]; // Handle 'other' in data const totalUsers = detailedData.sources.reduce( (sum, source) => sum + source.users, 0 ); const sourcesData = detailedData.sources.map((source) => { const percent = (source.users / totalUsers) * 100; return { ...source, percent }; }); return (
Realtime Analytics
{basicData?.data?.quotaInfo && ( )}
Last updated:{" "} {basicData.lastUpdated ? format(new Date(basicData.lastUpdated), "p") : "N/A"}
{" "}
{error && ( {error} )} {/* Summary Cards */}
{[ { label: "Last 30 Minutes", sublabel: "Active Users", value: basicData.last30MinUsers, }, { label: "Last 5 Minutes", sublabel: "Active Users", value: basicData.last5MinUsers, }, ].map((card) => (
{summaryCard(card.label, card.sublabel, card.value)}
))}
{/* User Activity Chart */}
Active Users Per Minute
{ if (active && payload && payload.length) { return (

{`${payload[0].value} active users`}

{payload[0].payload.timestamp}

); } return null; }} />{" "}
{/* Tabs for Detailed Data */} Current Pages Recent Events Active Devices {/* Current Pages Tab */}
Last updated:{" "} {detailedData.lastUpdated ? format(new Date(detailedData.lastUpdated), "p") : "N/A"}
Page Views {detailedData.currentPages.map(({ page, views }, index) => ( {page} {formatNumber(views)} ))}
{/* Recent Events Tab */}
Last updated:{" "} {detailedData.lastUpdated ? format(new Date(detailedData.lastUpdated), "p") : "N/A"}
Event Count {detailedData.recentEvents.map(({ event, count }, index) => ( {event} {formatNumber(count)} ))}
{/* Active Devices Tab */} {" "}
Last updated:{" "} {detailedData.lastUpdated ? format(new Date(detailedData.lastUpdated), "p") : "N/A"}
`${device}: ${percent.toFixed(0)}%` } > {sourcesData.map((entry, index) => ( ))} { if (active && payload && payload.length) { return (

{payload[0].payload.device}

{`${formatNumber( payload[0].value )} users`}

); } return null; }} />
{" "}
{sourcesData.map((source, index) => (
{source.device}
{formatNumber(source.users)}
{source.percent.toFixed(0)}% of users
))}
); }; export default RealtimeAnalytics;