Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import dayjs from 'dayjs'
import { fetchLogs } from 'data/reports/report.utils'

export type ResponseErrorRow = {
method: string
path: string
status_code: number
count: number
}

export type AuthErrorCodeRow = {
error_code: string
count: number
}

export const getDateRange = () => {
return {
start: dayjs().subtract(24, 'hour').toISOString(),
end: dayjs().toISOString(),
}
}

// Top API response errors for /auth/v1 endpoints (path/method/status)
export const AUTH_TOP_RESPONSE_ERRORS_SQL = `
select
request.method as method,
request.path as path,
response.status_code as status_code,
count(*) as count
from edge_logs
cross join unnest(metadata) as m
cross join unnest(m.request) as request
cross join unnest(m.response) as response
where path like '%auth/v1%'
and response.status_code between 400 and 599
group by method, path, status_code
order by count desc
limit 10
`

// Top Auth service error codes from x_sb_error_code header for /auth/v1 endpoints
export const AUTH_TOP_ERROR_CODES_SQL = `
select
h.x_sb_error_code as error_code,
count(*) as count
from edge_logs
cross join unnest(metadata) as m
cross join unnest(m.request) as request
cross join unnest(m.response) as response
cross join unnest(response.headers) as h
where path like '%auth/v1%'
and response.status_code between 400 and 599
and h.x_sb_error_code is not null
group by error_code
order by count desc
limit 10
`

export const fetchTopResponseErrors = async (projectRef: string) => {
const { start, end } = getDateRange()
return await fetchLogs(projectRef, AUTH_TOP_RESPONSE_ERRORS_SQL, start, end)
}

export const fetchTopAuthErrorCodes = async (projectRef: string) => {
const { start, end } = getDateRange()
return await fetchLogs(projectRef, AUTH_TOP_ERROR_CODES_SQL, start, end)
}
210 changes: 210 additions & 0 deletions apps/studio/components/interfaces/Auth/Overview/OverviewMonitoring.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { useQuery } from '@tanstack/react-query'
import { useParams } from 'common'
import {
Card,
CardContent,
CardHeader,
CardTitle,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import {
fetchTopAuthErrorCodes,
fetchTopResponseErrors,
AuthErrorCodeRow,
ResponseErrorRow,
} from './OverviewErrors.constants'
import { OverviewTable } from './OverviewTable'
import {
ScaffoldSection,
ScaffoldSectionContent,
ScaffoldSectionTitle,
} from 'components/layouts/Scaffold'
import {
calculatePercentageChange,
fetchAllAuthMetrics,
processAllAuthMetrics,
} from './OverviewUsage.constants'
import { StatCard } from './OverviewUsage'
import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
import { DataTableColumnStatusCode } from 'components/ui/DataTable/DataTableColumn/DataTableColumnStatusCode'
import { getStatusLevel } from 'components/interfaces/UnifiedLogs/UnifiedLogs.utils'
import dayjs from 'dayjs'

const LogsLink = ({ href }: { href: string }) => (
<Tooltip>
<TooltipTrigger asChild>
<Link className="block text-foreground-lighter hover:text-foreground p-1.5" href={href}>
<ChevronRight className="size-4" />
</Link>
</TooltipTrigger>
<TooltipContent>Go to logs</TooltipContent>
</Tooltip>
)

function isResponseErrorRow(row: unknown): row is ResponseErrorRow {
if (!row || typeof row !== 'object') return false
const r = row as Record<string, unknown>
return (
typeof r.method === 'string' &&
typeof r.path === 'string' &&
typeof r.status_code === 'number' &&
typeof r.count === 'number'
)
}

function isAuthErrorCodeRow(row: unknown): row is AuthErrorCodeRow {
if (!row || typeof row !== 'object') return false
const r = row as Record<string, unknown>
return typeof r.error_code === 'string' && typeof r.count === 'number'
}

export const OverviewMonitoring = () => {
const { ref } = useParams()
const endDate = dayjs().toISOString()
const startDate = dayjs().subtract(24, 'hour').toISOString()

// Success rate metrics (reuse OverviewUsage fetching)
const { data: currentData, isLoading: currentLoading } = useQuery({
queryKey: ['auth-metrics', ref, 'current'],
queryFn: () => fetchAllAuthMetrics(ref as string, 'current'),
enabled: !!ref,
})
const { data: previousData, isLoading: previousLoading } = useQuery({
queryKey: ['auth-metrics', ref, 'previous'],
queryFn: () => fetchAllAuthMetrics(ref as string, 'previous'),
enabled: !!ref,
})
const metrics = processAllAuthMetrics(currentData?.result || [], previousData?.result || [])

// Tables
const { data: respErrData, isLoading: isLoadingResp } = useQuery({
queryKey: ['auth-overview', ref, 'top-response-errors'],
queryFn: () => fetchTopResponseErrors(ref as string),
enabled: !!ref,
})

const { data: codeErrData, isLoading: isLoadingCodes } = useQuery({
queryKey: ['auth-overview', ref, 'top-auth-error-codes'],
queryFn: () => fetchTopAuthErrorCodes(ref as string),
enabled: !!ref,
})

const responseErrors: ResponseErrorRow[] = Array.isArray(respErrData?.result)
? (respErrData.result as unknown[]).filter(isResponseErrorRow)
: []
const errorCodes: AuthErrorCodeRow[] = Array.isArray(codeErrData?.result)
? (codeErrData.result as unknown[]).filter(isAuthErrorCodeRow)
: []

return (
<ScaffoldSection isFullWidth>
<div className="flex items-center justify-between mb-4">
<ScaffoldSectionTitle>Monitoring</ScaffoldSectionTitle>
</div>
<ScaffoldSectionContent className="gap-4">
<div className="grid grid-cols-2 gap-3">
<StatCard
title="Auth API Success Rate"
current={Math.max(0, 100 - metrics.current.apiErrorRate)}
previous={calculatePercentageChange(
Math.max(0, 100 - metrics.current.apiErrorRate),
Math.max(0, 100 - metrics.previous.apiErrorRate)
)}
loading={currentLoading || previousLoading}
suffix="%"
href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#monitoring`}
/>
<StatCard
title="Auth Server Success Rate"
current={Math.max(0, 100 - metrics.current.authErrorRate)}
previous={calculatePercentageChange(
Math.max(0, 100 - metrics.current.authErrorRate),
Math.max(0, 100 - metrics.previous.authErrorRate)
)}
loading={currentLoading || previousLoading}
suffix="%"
href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#monitoring`}
/>
</div>

<div className="grid grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle className="text-foreground-lighter">Auth API Errors</CardTitle>
</CardHeader>
<CardContent className="p-0">
<OverviewTable<ResponseErrorRow>
isLoading={isLoadingResp || currentLoading || previousLoading}
data={responseErrors}
columns={[
{
key: 'method',
header: 'Method',
className: 'w-[60px]',
render: (row) => (
<span className="text-foreground-lighter font-mono bg-background-alternative-200 px-2 py-1 rounded-md">
{row.method}
</span>
),
},
{
key: 'status_code',
header: 'Status',
className: 'w-[60px]',
render: (row) => (
<DataTableColumnStatusCode
value={row.status_code}
level={getStatusLevel(row.status_code)}
/>
),
},
{ key: 'path', header: 'Path', className: 'w-full' },
{ key: 'count', header: 'Count', className: 'text-right' },
{
key: 'actions',
header: '',
className: 'text-right',
render: (row) => (
<div>
<LogsLink href={`/project/${ref}/logs/edge-logs?s=${row.path}`} />
</div>
),
},
]}
/>
</CardContent>
</Card>

<Card>
<CardHeader>
<CardTitle className="text-foreground-lighter">Auth Server Errors</CardTitle>
</CardHeader>
<CardContent className="p-0">
<OverviewTable<AuthErrorCodeRow>
isLoading={isLoadingCodes || currentLoading || previousLoading}
data={errorCodes}
columns={[
{ key: 'error_code', header: 'Error code', className: 'w-full' },
{ key: 'count', header: 'Count', className: 'text-right' },
{
key: 'actions',
header: '',
className: 'text-right',
render: (row) => (
<div>
<LogsLink href={`/project/${ref}/logs/auth-logs?s=${row.error_code}`} />
</div>
),
},
]}
/>
</CardContent>
</Card>
</div>
</ScaffoldSectionContent>
</ScaffoldSection>
)
}
61 changes: 61 additions & 0 deletions apps/studio/components/interfaces/Auth/Overview/OverviewTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ReactNode } from 'react'
import { cn, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
import { Loader2 } from 'lucide-react'

export type OverviewTableColumn<T> = {
key: keyof T | string
header: string
render?: (row: T) => ReactNode
className?: string
}

export interface OverviewTable<T> {
columns: OverviewTableColumn<T>[]
data: T[]
isLoading?: boolean
emptyMessage?: string
}

export function OverviewTable<T>({ columns, data, isLoading, emptyMessage }: OverviewTable<T>) {
return (
<Table>
<TableHeader>
<TableRow>
{columns.map((col) => (
<TableHead key={String(col.key)} className={col.className}>
{col.header}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody className="text-foreground">
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="text-center text-foreground-light">
<div className="flex items-center justify-center gap-2 py-4">
<Loader2 className="size-4 animate-spin" />
<span>Loading…</span>
</div>
</TableCell>
</TableRow>
) : data.length === 0 ? (
<TableRow>
<TableCell colSpan={columns.length} className="text-center text-foreground-light">
{emptyMessage || 'No data available'}
</TableCell>
</TableRow>
) : (
(data as unknown as T[]).map((row, idx) => (
<TableRow key={idx}>
{columns.map((col) => (
<TableCell key={String(col.key)} className={cn('p-2 px-4', col.className)}>
{col.render ? col.render(row) : (row as any)[col.key as string]}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
)
}
Loading
Loading