| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- import { useQuery } from '@tanstack/react-query'
- import { useParams } from 'common'
- import dayjs from 'dayjs'
- import { BarChart2, ChevronRight, ExternalLink, Telescope } from 'lucide-react'
- import Link from 'next/link'
- import { useRouter } from 'next/router'
- import { AiIconAnimation, Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
- import { StatusCode } from 'ui-patterns'
- import {
- Chart,
- ChartActions,
- ChartCard,
- ChartContent,
- ChartEmptyState,
- ChartHeader,
- ChartLoadingState,
- ChartMetric,
- ChartTitle,
- } from 'ui-patterns/Chart'
- import {
- PageSection,
- PageSectionContent,
- PageSectionMeta,
- PageSectionSummary,
- PageSectionTitle,
- } from 'ui-patterns/PageSection'
- import {
- AuthErrorCodeRow,
- fetchTopAuthErrorCodes,
- fetchTopResponseErrors,
- ResponseErrorRow,
- } from './OverviewErrors.constants'
- import { OverviewTable } from './OverviewTable'
- import {
- AuthMetricsResponse,
- calculatePercentageChange,
- getApiSuccessRates,
- getAuthSuccessRates,
- getMetricValues,
- } from './OverviewUsage.constants'
- import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
- import AlertError from '@/components/ui/AlertError'
- import { ErrorCodeTooltip } from '@/components/ui/ErrorCodeTooltip/ErrorCodeTooltip'
- import { Service } from '@/data/graphql/graphql'
- import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
- import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
- const StatCard = ({
- title,
- current,
- previous,
- loading,
- suffix = '',
- href,
- tooltip,
- }: {
- title: string
- current: number
- previous: number
- loading: boolean
- suffix?: string
- invert?: boolean
- href?: string
- tooltip?: string
- }) => {
- const router = useRouter()
- const formattedCurrent =
- suffix === 'ms'
- ? current.toFixed(2)
- : suffix === '%'
- ? current.toFixed(1)
- : Math.round(current).toLocaleString()
- // const signChar = previous > 0 ? '+' : previous < 0 ? '-' : ''
- const actions = [
- {
- label: 'Go to Auth Report',
- icon: <ExternalLink size={12} />,
- onClick: href ? () => router.push(href) : undefined,
- },
- ]
- return (
- <Chart isLoading={loading}>
- <ChartCard>
- <ChartHeader align="start">
- <ChartMetric
- className="pb-4"
- label={title}
- tooltip={tooltip}
- diffValue={`${previous.toFixed(1)}%`}
- value={`${formattedCurrent}${suffix}`}
- />
- <ChartActions actions={actions} />
- </ChartHeader>
- </ChartCard>
- </Chart>
- )
- }
- const LogsLink = ({ href }: { href: string }) => (
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- type="text"
- size="tiny"
- className="p-1.5 text-foreground-lighter hover:text-foreground"
- asChild
- >
- <Link href={href} aria-label="Go to Logs">
- <ChevronRight size={12} />
- </Link>
- </Button>
- </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'
- }
- interface OverviewMetricsProps {
- metrics?: AuthMetricsResponse
- isLoading: boolean
- error: unknown
- }
- export const OverviewMetrics = ({ metrics, isLoading, error }: OverviewMetricsProps) => {
- const { ref } = useParams()
- const endDate = dayjs().toISOString()
- const startDate = dayjs().subtract(24, 'hour').toISOString()
- const aiSnap = useAiAssistantStateSnapshot()
- const { openSidebar } = useSidebarManagerSnapshot()
- const { current: activeUsersCurrent, previous: activeUsersPrevious } = getMetricValues(
- metrics,
- 'activeUsers'
- )
- const { current: signUpsCurrent, previous: signUpsPrevious } = getMetricValues(
- metrics,
- 'signUpCount'
- )
- const activeUsersChange = calculatePercentageChange(activeUsersCurrent, activeUsersPrevious)
- const signUpsChange = calculatePercentageChange(signUpsCurrent, signUpsPrevious)
- const { current: apiSuccessRateCurrent, previous: apiSuccessRatePrevious } =
- getApiSuccessRates(metrics)
- const { current: authSuccessRateCurrent, previous: authSuccessRatePrevious } =
- getAuthSuccessRates(metrics)
- const apiSuccessRateChange = calculatePercentageChange(
- apiSuccessRateCurrent,
- apiSuccessRatePrevious
- )
- const authSuccessRateChange = calculatePercentageChange(
- authSuccessRateCurrent,
- authSuccessRatePrevious
- )
- const { data: respErrData, isPending: isLoadingResp } = useQuery({
- queryKey: ['auth-overview', ref, 'top-response-errors'],
- queryFn: () => fetchTopResponseErrors(ref as string),
- enabled: !!ref,
- })
- const { data: codeErrData, isPending: 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)
- : []
- const errorCodesActions = [
- {
- label: 'Ask Assistant about Error Codes',
- icon: <AiIconAnimation size={12} />,
- onClick: () => {
- openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
- aiSnap.newChat({
- name: 'Auth Help',
- initialInput: `Can you explain to me what the authentication error codes mean?`,
- })
- },
- },
- ]
- return (
- <>
- <PageSection>
- {!!error && (
- <AlertError
- className="mb-4"
- subject="Error fetching auth metrics"
- error={{
- message: 'There was an error fetching the auth metrics.',
- }}
- />
- )}
- <PageSectionMeta>
- <PageSectionSummary>
- <div className="flex items-center justify-between">
- <PageSectionTitle>Usage</PageSectionTitle>
- <Link
- href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}&isHelper=true&helperText=Last+24+hours`}
- className="text-foreground underline underline-offset-2 decoration-foreground-muted hover:decoration-foreground transition-all text-sm inline-flex items-center gap-x-1.5"
- >
- <Telescope size={14} className="text-foreground-lighter" />
- <span>Go to observability</span>
- <ChevronRight size={14} className="text-foreground-lighter" />
- </Link>
- </div>
- </PageSectionSummary>
- </PageSectionMeta>
- <PageSectionContent>
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
- <StatCard
- title="Auth Activity"
- current={activeUsersCurrent}
- previous={activeUsersChange}
- loading={isLoading}
- href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#usage`}
- tooltip="Users who generated any Auth event in this period. This metric tracks authentication activity, not total product usage. Some active users won't appear here if their session stayed valid."
- />
- <StatCard
- title="Sign ups"
- current={signUpsCurrent}
- previous={signUpsChange}
- loading={isLoading}
- href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#usage`}
- />
- </div>
- </PageSectionContent>
- </PageSection>
- <PageSection>
- <PageSectionMeta>
- <PageSectionSummary>
- <PageSectionTitle>Monitoring</PageSectionTitle>
- </PageSectionSummary>
- </PageSectionMeta>
- <PageSectionContent>
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
- <StatCard
- title="Auth API Success Rate"
- current={apiSuccessRateCurrent}
- previous={apiSuccessRateChange}
- loading={isLoading}
- suffix="%"
- href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#monitoring`}
- />
- <StatCard
- title="Auth Server Success Rate"
- current={authSuccessRateCurrent}
- previous={authSuccessRateChange}
- loading={isLoading}
- suffix="%"
- href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#monitoring`}
- />
- </div>
- <div className="grid grid-cols-1 gap-4">
- <Chart isLoading={isLoadingResp}>
- <ChartCard>
- <ChartHeader>
- <ChartTitle>Auth API Errors</ChartTitle>
- </ChartHeader>
- <ChartContent
- className="p-0!"
- isEmpty={responseErrors.length === 0}
- emptyState={
- <div className="p-6">
- <ChartEmptyState
- icon={<BarChart2 size={16} />}
- title="No data to show"
- description="It may take up to 24 hours for data to refresh"
- />
- </div>
- }
- loadingState={
- <div className="p-6">
- <ChartLoadingState />
- </div>
- }
- >
- <OverviewTable<ResponseErrorRow>
- isLoading={isLoadingResp}
- data={responseErrors}
- columns={[
- {
- key: 'request',
- header: 'Request',
- className: 'w-auto pr-0!',
- render: (row) => {
- return <StatusCode method={row.method} statusCode={row.status_code} />
- },
- },
- {
- key: 'path',
- header: 'Path',
- className: 'w-full',
- render: (row) => (
- <span className="line-clamp-1 font-mono text-foreground-light text-xs">
- {row.path}
- </span>
- ),
- },
- {
- key: 'count',
- header: 'Count',
- className: 'text-right shrink-0 ml-auto justify-end',
- render: (row) => (
- <div className="flex justify-end items-center gap-2">
- <div className="text-right text-xs tabular-nums">{row.count}</div>
- <LogsLink href={`/project/${ref}/logs/edge-logs?s=${row.path}`} />
- </div>
- ),
- },
- ]}
- />
- </ChartContent>
- </ChartCard>
- </Chart>
- <Chart isLoading={isLoadingCodes}>
- <ChartCard>
- <ChartHeader>
- <ChartTitle>Auth Server Errors</ChartTitle>
- <ChartActions actions={errorCodesActions} />
- </ChartHeader>
- <ChartContent
- className="p-0!"
- isEmpty={errorCodes.length === 0}
- emptyState={
- <div className="p-6">
- <ChartEmptyState
- icon={<BarChart2 size={16} />}
- title="No data to show"
- description="It may take up to 24 hours for data to refresh"
- />
- </div>
- }
- loadingState={
- <div className="p-6">
- <ChartLoadingState />
- </div>
- }
- >
- <OverviewTable<AuthErrorCodeRow>
- isLoading={isLoadingCodes}
- data={errorCodes}
- columns={[
- {
- key: 'error_code',
- header: 'Error code',
- className: 'w-full',
- render: (row) => (
- <ErrorCodeTooltip errorCode={row.error_code} service={Service.Auth}>
- <span className="line-clamp-1 font-mono uppercase text-xs inline-flex text-foreground-light cursor-default">
- {row.error_code}
- </span>
- </ErrorCodeTooltip>
- ),
- },
- {
- key: 'count',
- header: 'Count',
- className: 'text-right',
- render: (row) => (
- <div className="flex justify-end items-center gap-2">
- <div className="text-right text-xs tabular-nums">{row.count}</div>
- <LogsLink href={`/project/${ref}/logs/auth-logs?s=${row.error_code}`} />
- </div>
- ),
- },
- ]}
- />
- </ChartContent>
- </ChartCard>
- </Chart>
- </div>
- </PageSectionContent>
- </PageSection>
- </>
- )
- }
|