Skip to content

Commit be5e8b2

Browse files
authored
Auth Overview: Refactor to Logflare Endpoint for all metrics (supabase#40042)
* refactor * fix schema, add validation, add tests, remove old code * remove debugging tag * fix imports * prettier * fix prettier * move query to its own file
1 parent 4c4083b commit be5e8b2

File tree

9 files changed

+702
-576
lines changed

9 files changed

+702
-576
lines changed
Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
import { useQuery } from '@tanstack/react-query'
2+
import { useParams } from 'common'
3+
import {
4+
Card,
5+
CardContent,
6+
CardHeader,
7+
CardTitle,
8+
cn,
9+
Skeleton,
10+
Tooltip,
11+
TooltipContent,
12+
TooltipTrigger,
13+
} from 'ui'
14+
import Link from 'next/link'
15+
import { ChevronRight, ExternalLink, HelpCircle } from 'lucide-react'
16+
import { Reports } from 'icons'
17+
import {
18+
ScaffoldSection,
19+
ScaffoldSectionTitle,
20+
ScaffoldSectionContent,
21+
} from 'components/layouts/Scaffold'
22+
import {
23+
calculatePercentageChange,
24+
getChangeColor,
25+
getMetricValues,
26+
AuthMetricsResponse,
27+
getApiSuccessRates,
28+
getAuthSuccessRates,
29+
} from './OverviewUsage.constants'
30+
import {
31+
fetchTopAuthErrorCodes,
32+
fetchTopResponseErrors,
33+
AuthErrorCodeRow,
34+
ResponseErrorRow,
35+
} from './OverviewErrors.constants'
36+
import { OverviewTable } from './OverviewTable'
37+
import dayjs from 'dayjs'
38+
import AlertError from 'components/ui/AlertError'
39+
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
40+
import { useRouter } from 'next/router'
41+
import { DataTableColumnStatusCode } from 'components/ui/DataTable/DataTableColumn/DataTableColumnStatusCode'
42+
import { getStatusLevel } from 'components/interfaces/UnifiedLogs/UnifiedLogs.utils'
43+
44+
const StatCard = ({
45+
title,
46+
current,
47+
previous,
48+
loading,
49+
suffix = '',
50+
invert = false,
51+
href,
52+
tooltip,
53+
}: {
54+
title: string
55+
current: number
56+
previous: number
57+
loading: boolean
58+
suffix?: string
59+
invert?: boolean
60+
href?: string
61+
tooltip?: string
62+
}) => {
63+
const router = useRouter()
64+
const isZeroChange = previous === 0
65+
const changeColor = isZeroChange
66+
? 'text-foreground-lighter'
67+
: invert
68+
? previous >= 0
69+
? 'text-destructive'
70+
: 'text-brand'
71+
: getChangeColor(previous)
72+
const formattedCurrent =
73+
suffix === 'ms'
74+
? current.toFixed(2)
75+
: suffix === '%'
76+
? current.toFixed(1)
77+
: Math.round(current).toLocaleString()
78+
const signChar = previous > 0 ? '+' : previous < 0 ? '-' : ''
79+
80+
return (
81+
<Card className={cn(href, 'mb-0 flex flex-col')}>
82+
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0 pb-0 border-b-0 relative">
83+
<CardTitle className="text-foreground-light flex items-center gap-2">
84+
{title}
85+
{tooltip && (
86+
<Tooltip>
87+
<TooltipTrigger>
88+
<HelpCircle className="text-foreground-light" size={14} strokeWidth={1.5} />
89+
</TooltipTrigger>
90+
<TooltipContent className="w-[300px]">
91+
<p>{tooltip}</p>
92+
</TooltipContent>
93+
</Tooltip>
94+
)}
95+
</CardTitle>
96+
<ButtonTooltip
97+
type="text"
98+
size="tiny"
99+
icon={<ExternalLink />}
100+
className="w-6 h-6 absolute right-4 top-3"
101+
onClick={() => router.push(href || '')}
102+
tooltip={{
103+
content: {
104+
side: 'top',
105+
text: 'Go to Auth Report',
106+
},
107+
}}
108+
/>
109+
</CardHeader>
110+
<CardContent
111+
className={cn(
112+
'pb-4 px-6 pt-0 flex-1 h-full overflow-hidden',
113+
loading && 'pt-2 opacity-50 items-center justify-center'
114+
)}
115+
>
116+
{loading ? (
117+
<div className="flex flex-col gap-2">
118+
<Skeleton className="h-6 w-20" />
119+
<Skeleton className="h-3 w-8" />
120+
</div>
121+
) : (
122+
<div className="flex flex-col gap-0.5">
123+
<p className="text-xl">{`${formattedCurrent}${suffix}`}</p>
124+
<span className={cn('flex items-center gap-1 text-sm', changeColor)}>
125+
<span>{`${signChar}${Math.abs(previous).toFixed(1)}%`}</span>
126+
</span>
127+
</div>
128+
)}
129+
</CardContent>
130+
</Card>
131+
)
132+
}
133+
134+
const LogsLink = ({ href }: { href: string }) => (
135+
<Tooltip>
136+
<TooltipTrigger asChild>
137+
<Link className="block text-foreground-lighter hover:text-foreground p-1.5" href={href}>
138+
<ChevronRight className="size-4" />
139+
</Link>
140+
</TooltipTrigger>
141+
<TooltipContent>Go to logs</TooltipContent>
142+
</Tooltip>
143+
)
144+
145+
function isResponseErrorRow(row: unknown): row is ResponseErrorRow {
146+
if (!row || typeof row !== 'object') return false
147+
const r = row as Record<string, unknown>
148+
return (
149+
typeof r.method === 'string' &&
150+
typeof r.path === 'string' &&
151+
typeof r.status_code === 'number' &&
152+
typeof r.count === 'number'
153+
)
154+
}
155+
156+
function isAuthErrorCodeRow(row: unknown): row is AuthErrorCodeRow {
157+
if (!row || typeof row !== 'object') return false
158+
const r = row as Record<string, unknown>
159+
return typeof r.error_code === 'string' && typeof r.count === 'number'
160+
}
161+
162+
interface OverviewMetricsProps {
163+
metrics?: AuthMetricsResponse
164+
isLoading: boolean
165+
error: unknown
166+
}
167+
168+
export const OverviewMetrics = ({ metrics, isLoading, error }: OverviewMetricsProps) => {
169+
const { ref } = useParams()
170+
const endDate = dayjs().toISOString()
171+
const startDate = dayjs().subtract(24, 'hour').toISOString()
172+
173+
const { current: activeUsersCurrent, previous: activeUsersPrevious } = getMetricValues(
174+
metrics,
175+
'activeUsers'
176+
)
177+
178+
const { current: signUpsCurrent, previous: signUpsPrevious } = getMetricValues(
179+
metrics,
180+
'signUpCount'
181+
)
182+
183+
const activeUsersChange = calculatePercentageChange(activeUsersCurrent, activeUsersPrevious)
184+
const signUpsChange = calculatePercentageChange(signUpsCurrent, signUpsPrevious)
185+
186+
const { current: apiSuccessRateCurrent, previous: apiSuccessRatePrevious } =
187+
getApiSuccessRates(metrics)
188+
const { current: authSuccessRateCurrent, previous: authSuccessRatePrevious } =
189+
getAuthSuccessRates(metrics)
190+
191+
const apiSuccessRateChange = calculatePercentageChange(
192+
apiSuccessRateCurrent,
193+
apiSuccessRatePrevious
194+
)
195+
const authSuccessRateChange = calculatePercentageChange(
196+
authSuccessRateCurrent,
197+
authSuccessRatePrevious
198+
)
199+
200+
const { data: respErrData, isLoading: isLoadingResp } = useQuery({
201+
queryKey: ['auth-overview', ref, 'top-response-errors'],
202+
queryFn: () => fetchTopResponseErrors(ref as string),
203+
enabled: !!ref,
204+
})
205+
206+
const { data: codeErrData, isLoading: isLoadingCodes } = useQuery({
207+
queryKey: ['auth-overview', ref, 'top-auth-error-codes'],
208+
queryFn: () => fetchTopAuthErrorCodes(ref as string),
209+
enabled: !!ref,
210+
})
211+
212+
const responseErrors: ResponseErrorRow[] = Array.isArray(respErrData?.result)
213+
? (respErrData?.result as unknown[]).filter(isResponseErrorRow)
214+
: []
215+
const errorCodes: AuthErrorCodeRow[] = Array.isArray(codeErrData?.result)
216+
? (codeErrData?.result as unknown[]).filter(isAuthErrorCodeRow)
217+
: []
218+
219+
return (
220+
<>
221+
<ScaffoldSection isFullWidth>
222+
{!!error && (
223+
<AlertError
224+
className="mb-4"
225+
subject="Error fetching auth metrics"
226+
error={{
227+
message: 'There was an error fetching the auth metrics.',
228+
}}
229+
/>
230+
)}
231+
<div className="flex items-center justify-between mb-4">
232+
<ScaffoldSectionTitle>Usage</ScaffoldSectionTitle>
233+
<Link
234+
href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}&isHelper=true&helperText=Last+24+hours`}
235+
className="text-sm text-link inline-flex items-center gap-x-1.5"
236+
>
237+
<Reports size={14} />
238+
<span>View all reports</span>
239+
<ChevronRight size={14} />
240+
</Link>
241+
</div>
242+
<ScaffoldSectionContent className="gap-4">
243+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
244+
<StatCard
245+
title="Auth Activity"
246+
current={activeUsersCurrent}
247+
previous={activeUsersChange}
248+
loading={isLoading}
249+
href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#usage`}
250+
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."
251+
/>
252+
<StatCard
253+
title="Sign ups"
254+
current={signUpsCurrent}
255+
previous={signUpsChange}
256+
loading={isLoading}
257+
href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#usage`}
258+
/>
259+
</div>
260+
</ScaffoldSectionContent>
261+
</ScaffoldSection>
262+
263+
<ScaffoldSection isFullWidth>
264+
<div className="flex items-center justify-between mb-4">
265+
<ScaffoldSectionTitle>Monitoring</ScaffoldSectionTitle>
266+
</div>
267+
<ScaffoldSectionContent className="gap-4">
268+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
269+
<StatCard
270+
title="Auth API Success Rate"
271+
current={apiSuccessRateCurrent}
272+
previous={apiSuccessRateChange}
273+
loading={isLoading}
274+
suffix="%"
275+
href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#monitoring`}
276+
/>
277+
<StatCard
278+
title="Auth Server Success Rate"
279+
current={authSuccessRateCurrent}
280+
previous={authSuccessRateChange}
281+
loading={isLoading}
282+
suffix="%"
283+
href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#monitoring`}
284+
/>
285+
</div>
286+
287+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
288+
<Card>
289+
<CardHeader className={cn('border-b-0', responseErrors.length > 0 ? 'pb-4' : 'pb-0')}>
290+
<CardTitle className="text-foreground-light">Auth API Errors</CardTitle>
291+
</CardHeader>
292+
<CardContent className="p-0">
293+
<OverviewTable<ResponseErrorRow>
294+
isLoading={isLoadingResp}
295+
data={responseErrors}
296+
columns={[
297+
{
298+
key: 'request',
299+
header: 'Request',
300+
className: 'w-[60px]',
301+
render: (row) => (
302+
<span className="font-mono text-xs truncate select-text cursor-text py-1 px-1.5 text-center rounded-md bg-alternative-200">
303+
{row.method}
304+
</span>
305+
),
306+
},
307+
{
308+
key: 'status_code',
309+
header: 'Status',
310+
className: 'w-[60px]',
311+
render: (row) => (
312+
<DataTableColumnStatusCode
313+
value={row.status_code}
314+
level={getStatusLevel(row.status_code)}
315+
className="text-sm"
316+
/>
317+
),
318+
},
319+
{
320+
key: 'path',
321+
header: 'Path',
322+
className: 'flex-shrink-0 w-52',
323+
render: (row) => (
324+
<div className="line-clamp-1 font-mono text-foreground-light text-xs">
325+
{row.path}
326+
</div>
327+
),
328+
},
329+
{
330+
key: 'count',
331+
header: 'Count',
332+
className: 'text-right flex-shrink-0 ml-auto justify-end',
333+
render: (row) => (
334+
<div className="text-right text-xs tabular-nums">{row.count}</div>
335+
),
336+
},
337+
{
338+
key: 'actions',
339+
header: '',
340+
className: 'w-6',
341+
render: (row) => (
342+
<div className="flex justify-end">
343+
<LogsLink href={`/project/${ref}/logs/edge-logs?s=${row.path}`} />
344+
</div>
345+
),
346+
},
347+
]}
348+
/>
349+
</CardContent>
350+
</Card>
351+
352+
<Card>
353+
<CardHeader className={cn('border-b-0', errorCodes.length > 0 ? 'pb-4' : 'pb-0')}>
354+
<CardTitle className="text-foreground-light">Auth Server Errors</CardTitle>
355+
</CardHeader>
356+
<CardContent className="p-0">
357+
<OverviewTable<AuthErrorCodeRow>
358+
isLoading={isLoadingCodes}
359+
data={errorCodes}
360+
columns={[
361+
{
362+
key: 'error_code',
363+
header: 'Error code',
364+
className: 'w-full',
365+
render: (row) => (
366+
<div className="line-clamp-1 font-mono text-foreground uppercase text-xs">
367+
{row.error_code}
368+
</div>
369+
),
370+
},
371+
{
372+
key: 'count',
373+
header: 'Count',
374+
className: 'text-right',
375+
render: (row) => (
376+
<div className="text-right text-xs tabular-nums">{row.count}</div>
377+
),
378+
},
379+
{
380+
key: 'actions',
381+
header: '',
382+
className: 'text-right',
383+
render: (row) => (
384+
<div>
385+
<LogsLink href={`/project/${ref}/logs/auth-logs?s=${row.error_code}`} />
386+
</div>
387+
),
388+
},
389+
]}
390+
/>
391+
</CardContent>
392+
</Card>
393+
</div>
394+
</ScaffoldSectionContent>
395+
</ScaffoldSection>
396+
</>
397+
)
398+
}

0 commit comments

Comments
 (0)