import dayjs from 'dayjs' import { Archive, ChevronDown, Database, Key, Zap, Code } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useState, useMemo } from 'react' import { useParams } from 'common' import { LogsBarChart } from 'ui-patterns/LogsBarChart' import { InlineLink } from 'components/ui/InlineLink' import useProjectUsageStats from 'hooks/analytics/useProjectUsageStats' import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import type { ChartIntervals } from 'types' import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, Loading, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import { Card, CardContent, CardHeader, CardTitle } from 'ui' import { Row } from 'ui-patterns' import NoDataPlaceholder from 'components/ui/Charts/NoDataPlaceholder' type ChartIntervalKey = '1hr' | '1day' | '7day' const LOG_RETENTION = { free: 1, pro: 7, team: 28, enterprise: 90 } const CHART_INTERVALS: ChartIntervals[] = [ { key: '1hr', label: 'Last 60 minutes', startValue: 1, startUnit: 'hour', format: 'MMM D, h:mma', availableIn: ['free', 'pro', 'team', 'enterprise'], }, { key: '1day', label: 'Last 24 hours', startValue: 24, startUnit: 'hour', format: 'MMM D, ha', availableIn: ['free', 'pro', 'team', 'enterprise'], }, { key: '7day', label: 'Last 7 days', startValue: 7, startUnit: 'day', format: 'MMM D', availableIn: ['pro', 'team', 'enterprise'], }, ] type LogsBarChartDatum = { timestamp: string error_count: number ok_count: number warning_count: number } type ServiceKey = 'db' | 'functions' | 'auth' | 'storage' | 'realtime' type ServiceEntry = { key: ServiceKey title: string icon: React.ReactNode href?: string route: string enabled: boolean } type ServiceComputed = ServiceEntry & { data: LogsBarChartDatum[] total: number warn: number err: number stats: ReturnType } type ServiceStatsMap = Record< ServiceKey, { current: ReturnType previous: ReturnType } > const useServiceStats = ( projectRef: string, timestampStart: string, timestampEnd: string, previousStart: string, previousEnd: string ): ServiceStatsMap => { const dbCurrent = useProjectUsageStats({ projectRef, table: LogsTableName.EDGE, timestampStart, timestampEnd, }) const dbPrevious = useProjectUsageStats({ projectRef, table: LogsTableName.EDGE, timestampStart: previousStart, timestampEnd: previousEnd, }) const fnCurrent = useProjectUsageStats({ projectRef, table: LogsTableName.FN_EDGE, timestampStart, timestampEnd, }) const fnPrevious = useProjectUsageStats({ projectRef, table: LogsTableName.FN_EDGE, timestampStart: previousStart, timestampEnd: previousEnd, }) const authCurrent = useProjectUsageStats({ projectRef, table: LogsTableName.AUTH, timestampStart, timestampEnd, }) const authPrevious = useProjectUsageStats({ projectRef, table: LogsTableName.AUTH, timestampStart: previousStart, timestampEnd: previousEnd, }) const storageCurrent = useProjectUsageStats({ projectRef, table: LogsTableName.STORAGE, timestampStart, timestampEnd, }) const storagePrevious = useProjectUsageStats({ projectRef, table: LogsTableName.STORAGE, timestampStart: previousStart, timestampEnd: previousEnd, }) const realtimeCurrent = useProjectUsageStats({ projectRef, table: LogsTableName.REALTIME, timestampStart, timestampEnd, }) const realtimePrevious = useProjectUsageStats({ projectRef, table: LogsTableName.REALTIME, timestampStart: previousStart, timestampEnd: previousEnd, }) return { db: { current: dbCurrent, previous: dbPrevious }, functions: { current: fnCurrent, previous: fnPrevious }, auth: { current: authCurrent, previous: authPrevious }, storage: { current: storageCurrent, previous: storagePrevious }, realtime: { current: realtimeCurrent, previous: realtimePrevious }, } } const ProjectUsage = () => { const router = useRouter() const { ref: projectRef } = useParams() const { data: organization } = useSelectedOrganizationQuery() const { projectAuthAll: authEnabled, projectStorageAll: storageEnabled } = useIsFeatureEnabled([ 'project_auth:all', 'project_storage:all', ]) const { plan } = useCurrentOrgPlan() const DEFAULT_INTERVAL: ChartIntervalKey = plan?.id === 'free' ? '1hr' : '1day' const [interval, setInterval] = useState(DEFAULT_INTERVAL) const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1] const { timestampStart, timestampEnd, datetimeFormat } = useMemo(() => { const startDateLocal = dayjs().subtract( selectedInterval.startValue, selectedInterval.startUnit as dayjs.ManipulateType ) const endDateLocal = dayjs() const format = selectedInterval.format || 'MMM D, ha' return { timestampStart: startDateLocal.toISOString(), timestampEnd: endDateLocal.toISOString(), datetimeFormat: format, } }, [selectedInterval]) // Only recalculate when interval changes const { previousStart, previousEnd } = useMemo(() => { const currentStart = dayjs(timestampStart) const currentEnd = dayjs(timestampEnd) const durationMs = currentEnd.diff(currentStart) const prevEnd = currentStart const prevStart = currentStart.subtract(durationMs, 'millisecond') return { previousStart: prevStart.toISOString(), previousEnd: prevEnd.toISOString() } }, [timestampStart, timestampEnd]) const statsByService = useServiceStats( projectRef as string, timestampStart, timestampEnd, previousStart, previousEnd ) const toLogsBarChartData = (rows: any[] = []): LogsBarChartDatum[] => { return rows.map((r) => ({ timestamp: String(r.timestamp), ok_count: Number(r.ok_count || 0), warning_count: Number(r.warning_count || 0), error_count: Number(r.error_count || 0), })) } const sumTotal = (data: LogsBarChartDatum[]) => data.reduce((acc, r) => acc + r.ok_count + r.warning_count + r.error_count, 0) const sumWarnings = (data: LogsBarChartDatum[]) => data.reduce((acc, r) => acc + r.warning_count, 0) const sumErrors = (data: LogsBarChartDatum[]) => data.reduce((acc, r) => acc + r.error_count, 0) const serviceBase: ServiceEntry[] = useMemo( () => [ { key: 'db', title: 'Database requests', icon: , href: `/project/${projectRef}/editor`, route: '/logs/postgres-logs', enabled: true, }, { key: 'functions', title: 'Functions requests', icon: , route: '/logs/edge-functions-logs', enabled: true, }, { key: 'auth', title: 'Auth requests', icon: , href: `/project/${projectRef}/auth/users`, route: '/logs/auth-logs', enabled: authEnabled, }, { key: 'storage', title: 'Storage requests', icon: , href: `/project/${projectRef}/storage/buckets`, route: '/logs/storage-logs', enabled: storageEnabled, }, { key: 'realtime', title: 'Realtime requests', icon: , route: '/logs/realtime-logs', enabled: true, }, ], [projectRef, authEnabled, storageEnabled] ) const services: ServiceComputed[] = useMemo( () => serviceBase.map((s) => { const currentStats = statsByService[s.key].current const data = toLogsBarChartData(currentStats.eventChartData) const total = sumTotal(data) const warn = sumWarnings(data) const err = sumErrors(data) return { ...s, stats: currentStats, data, total, warn, err } }), [serviceBase, statsByService] ) const isLoading = services.some((s) => s.stats.isLoading) const handleBarClick = (logRoute: string) => (datum: any) => { if (!datum?.timestamp) return const datumTimestamp = dayjs(datum.timestamp).toISOString() const start = dayjs(datumTimestamp).subtract(1, 'minute').toISOString() const end = dayjs(datumTimestamp).add(1, 'minute').toISOString() const queryParams = new URLSearchParams({ iso_timestamp_start: start, iso_timestamp_end: end, }) router.push(`/project/${projectRef}${logRoute}?${queryParams.toString()}`) } const enabledServices = services.filter((s) => s.enabled) const totalRequests = enabledServices.reduce((sum, s) => sum + (s.total || 0), 0) const totalErrors = enabledServices.reduce((sum, s) => sum + (s.err || 0), 0) const errorRate = totalRequests > 0 ? (totalErrors / totalRequests) * 100 : 0 const prevServiceTotals = useMemo( () => serviceBase.map((s) => { const previousStats = statsByService[s.key].previous const data = toLogsBarChartData(previousStats.eventChartData) return { enabled: s.enabled, total: sumTotal(data), err: sumErrors(data), } }), [serviceBase, statsByService] ) const enabledPrev = prevServiceTotals.filter((s) => s.enabled) const prevTotalRequests = enabledPrev.reduce((sum, s) => sum + (s.total || 0), 0) const prevTotalErrors = enabledPrev.reduce((sum, s) => sum + (s.err || 0), 0) const prevErrorRate = prevTotalRequests > 0 ? (prevTotalErrors / prevTotalRequests) * 100 : 0 const totalRequestsChangePct = prevTotalRequests === 0 ? totalRequests > 0 ? 100 : 0 : ((totalRequests - prevTotalRequests) / prevTotalRequests) * 100 const errorRateChangePct = prevErrorRate === 0 ? errorRate > 0 ? 100 : 0 : ((errorRate - prevErrorRate) / prevErrorRate) * 100 const formatDelta = (v: number) => `${v >= 0 ? '+' : ''}${v.toFixed(1)}%` const totalDeltaClass = totalRequestsChangePct >= 0 ? 'text-brand' : 'text-destructive' const errorDeltaClass = errorRateChangePct <= 0 ? 'text-brand' : 'text-destructive' return (
{totalRequests.toLocaleString()} Total Requests {formatDelta(totalRequestsChangePct)}
/
{errorRate.toFixed(1)}% Error Rate {formatDelta(errorRateChangePct)}
setInterval(interval as ChartIntervalKey)} > {CHART_INTERVALS.map((i) => { const disabled = !i.availableIn?.includes(plan?.id || 'free') if (disabled) { const retentionDuration = LOG_RETENTION[plan?.id ?? 'free'] return ( {i.label}

{plan?.name} plan only includes up to {retentionDuration} day {retentionDuration > 1 ? 's' : ''} of log retention

Upgrade your plan {' '} to increase log retention and view statistics for the{' '} {i.label.toLowerCase()}

) } else { return ( {i.label} ) } })}
{enabledServices.map((s) => (
{s.href ? {s.title} : s.title}
{(s.total || 0).toLocaleString()}
Warn
{(s.warn || 0).toLocaleString()}
Err
{(s.err || 0).toLocaleString()}
} /> ))}
) } export default ProjectUsage