OverviewMetrics.tsx408 lines · main
1import { useQuery } from '@tanstack/react-query'
2import { useParams } from 'common'
3import dayjs from 'dayjs'
4import { BarChart2, ChevronRight, ExternalLink, Telescope } from 'lucide-react'
5import Link from 'next/link'
6import { useRouter } from 'next/router'
7import { AiIconAnimation, Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
8import { StatusCode } from 'ui-patterns'
9import {
10 Chart,
11 ChartActions,
12 ChartCard,
13 ChartContent,
14 ChartEmptyState,
15 ChartHeader,
16 ChartLoadingState,
17 ChartMetric,
18 ChartTitle,
19} from 'ui-patterns/Chart'
20import {
21 PageSection,
22 PageSectionContent,
23 PageSectionMeta,
24 PageSectionSummary,
25 PageSectionTitle,
26} from 'ui-patterns/PageSection'
27
28import {
29 AuthErrorCodeRow,
30 fetchTopAuthErrorCodes,
31 fetchTopResponseErrors,
32 ResponseErrorRow,
33} from './OverviewErrors.constants'
34import { OverviewTable } from './OverviewTable'
35import {
36 AuthMetricsResponse,
37 calculatePercentageChange,
38 getApiSuccessRates,
39 getAuthSuccessRates,
40 getMetricValues,
41} from './OverviewUsage.constants'
42import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
43import AlertError from '@/components/ui/AlertError'
44import { ErrorCodeTooltip } from '@/components/ui/ErrorCodeTooltip/ErrorCodeTooltip'
45import { Service } from '@/data/graphql/graphql'
46import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
47import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
48
49const StatCard = ({
50 title,
51 current,
52 previous,
53 loading,
54 suffix = '',
55 href,
56 tooltip,
57}: {
58 title: string
59 current: number
60 previous: number
61 loading: boolean
62 suffix?: string
63 invert?: boolean
64 href?: string
65 tooltip?: string
66}) => {
67 const router = useRouter()
68 const formattedCurrent =
69 suffix === 'ms'
70 ? current.toFixed(2)
71 : suffix === '%'
72 ? current.toFixed(1)
73 : Math.round(current).toLocaleString()
74 // const signChar = previous > 0 ? '+' : previous < 0 ? '-' : ''
75
76 const actions = [
77 {
78 label: 'Go to Auth Report',
79 icon: <ExternalLink size={12} />,
80 onClick: href ? () => router.push(href) : undefined,
81 },
82 ]
83
84 return (
85 <Chart isLoading={loading}>
86 <ChartCard>
87 <ChartHeader align="start">
88 <ChartMetric
89 className="pb-4"
90 label={title}
91 tooltip={tooltip}
92 diffValue={`${previous.toFixed(1)}%`}
93 value={`${formattedCurrent}${suffix}`}
94 />
95 <ChartActions actions={actions} />
96 </ChartHeader>
97 </ChartCard>
98 </Chart>
99 )
100}
101
102const LogsLink = ({ href }: { href: string }) => (
103 <Tooltip>
104 <TooltipTrigger asChild>
105 <Button
106 type="text"
107 size="tiny"
108 className="p-1.5 text-foreground-lighter hover:text-foreground"
109 asChild
110 >
111 <Link href={href} aria-label="Go to Logs">
112 <ChevronRight size={12} />
113 </Link>
114 </Button>
115 </TooltipTrigger>
116 <TooltipContent>Go to Logs</TooltipContent>
117 </Tooltip>
118)
119
120function isResponseErrorRow(row: unknown): row is ResponseErrorRow {
121 if (!row || typeof row !== 'object') return false
122 const r = row as Record<string, unknown>
123 return (
124 typeof r.method === 'string' &&
125 typeof r.path === 'string' &&
126 typeof r.status_code === 'number' &&
127 typeof r.count === 'number'
128 )
129}
130
131function isAuthErrorCodeRow(row: unknown): row is AuthErrorCodeRow {
132 if (!row || typeof row !== 'object') return false
133 const r = row as Record<string, unknown>
134 return typeof r.error_code === 'string' && typeof r.count === 'number'
135}
136
137interface OverviewMetricsProps {
138 metrics?: AuthMetricsResponse
139 isLoading: boolean
140 error: unknown
141}
142
143export const OverviewMetrics = ({ metrics, isLoading, error }: OverviewMetricsProps) => {
144 const { ref } = useParams()
145 const endDate = dayjs().toISOString()
146 const startDate = dayjs().subtract(24, 'hour').toISOString()
147 const aiSnap = useAiAssistantStateSnapshot()
148 const { openSidebar } = useSidebarManagerSnapshot()
149
150 const { current: activeUsersCurrent, previous: activeUsersPrevious } = getMetricValues(
151 metrics,
152 'activeUsers'
153 )
154
155 const { current: signUpsCurrent, previous: signUpsPrevious } = getMetricValues(
156 metrics,
157 'signUpCount'
158 )
159
160 const activeUsersChange = calculatePercentageChange(activeUsersCurrent, activeUsersPrevious)
161 const signUpsChange = calculatePercentageChange(signUpsCurrent, signUpsPrevious)
162
163 const { current: apiSuccessRateCurrent, previous: apiSuccessRatePrevious } =
164 getApiSuccessRates(metrics)
165 const { current: authSuccessRateCurrent, previous: authSuccessRatePrevious } =
166 getAuthSuccessRates(metrics)
167
168 const apiSuccessRateChange = calculatePercentageChange(
169 apiSuccessRateCurrent,
170 apiSuccessRatePrevious
171 )
172 const authSuccessRateChange = calculatePercentageChange(
173 authSuccessRateCurrent,
174 authSuccessRatePrevious
175 )
176
177 const { data: respErrData, isPending: isLoadingResp } = useQuery({
178 queryKey: ['auth-overview', ref, 'top-response-errors'],
179 queryFn: () => fetchTopResponseErrors(ref as string),
180 enabled: !!ref,
181 })
182
183 const { data: codeErrData, isPending: isLoadingCodes } = useQuery({
184 queryKey: ['auth-overview', ref, 'top-auth-error-codes'],
185 queryFn: () => fetchTopAuthErrorCodes(ref as string),
186 enabled: !!ref,
187 })
188
189 const responseErrors: ResponseErrorRow[] = Array.isArray(respErrData?.result)
190 ? (respErrData?.result as unknown[]).filter(isResponseErrorRow)
191 : []
192 const errorCodes: AuthErrorCodeRow[] = Array.isArray(codeErrData?.result)
193 ? (codeErrData?.result as unknown[]).filter(isAuthErrorCodeRow)
194 : []
195
196 const errorCodesActions = [
197 {
198 label: 'Ask Assistant about Error Codes',
199 icon: <AiIconAnimation size={12} />,
200 onClick: () => {
201 openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
202 aiSnap.newChat({
203 name: 'Auth Help',
204 initialInput: `Can you explain to me what the authentication error codes mean?`,
205 })
206 },
207 },
208 ]
209
210 return (
211 <>
212 <PageSection>
213 {!!error && (
214 <AlertError
215 className="mb-4"
216 subject="Error fetching auth metrics"
217 error={{
218 message: 'There was an error fetching the auth metrics.',
219 }}
220 />
221 )}
222 <PageSectionMeta>
223 <PageSectionSummary>
224 <div className="flex items-center justify-between">
225 <PageSectionTitle>Usage</PageSectionTitle>
226 <Link
227 href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}&isHelper=true&helperText=Last+24+hours`}
228 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"
229 >
230 <Telescope size={14} className="text-foreground-lighter" />
231 <span>Go to observability</span>
232 <ChevronRight size={14} className="text-foreground-lighter" />
233 </Link>
234 </div>
235 </PageSectionSummary>
236 </PageSectionMeta>
237 <PageSectionContent>
238 <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
239 <StatCard
240 title="Auth Activity"
241 current={activeUsersCurrent}
242 previous={activeUsersChange}
243 loading={isLoading}
244 href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#usage`}
245 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."
246 />
247 <StatCard
248 title="Sign ups"
249 current={signUpsCurrent}
250 previous={signUpsChange}
251 loading={isLoading}
252 href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#usage`}
253 />
254 </div>
255 </PageSectionContent>
256 </PageSection>
257
258 <PageSection>
259 <PageSectionMeta>
260 <PageSectionSummary>
261 <PageSectionTitle>Monitoring</PageSectionTitle>
262 </PageSectionSummary>
263 </PageSectionMeta>
264 <PageSectionContent>
265 <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
266 <StatCard
267 title="Auth API Success Rate"
268 current={apiSuccessRateCurrent}
269 previous={apiSuccessRateChange}
270 loading={isLoading}
271 suffix="%"
272 href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#monitoring`}
273 />
274 <StatCard
275 title="Auth Server Success Rate"
276 current={authSuccessRateCurrent}
277 previous={authSuccessRateChange}
278 loading={isLoading}
279 suffix="%"
280 href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#monitoring`}
281 />
282 </div>
283
284 <div className="grid grid-cols-1 gap-4">
285 <Chart isLoading={isLoadingResp}>
286 <ChartCard>
287 <ChartHeader>
288 <ChartTitle>Auth API Errors</ChartTitle>
289 </ChartHeader>
290 <ChartContent
291 className="p-0!"
292 isEmpty={responseErrors.length === 0}
293 emptyState={
294 <div className="p-6">
295 <ChartEmptyState
296 icon={<BarChart2 size={16} />}
297 title="No data to show"
298 description="It may take up to 24 hours for data to refresh"
299 />
300 </div>
301 }
302 loadingState={
303 <div className="p-6">
304 <ChartLoadingState />
305 </div>
306 }
307 >
308 <OverviewTable<ResponseErrorRow>
309 isLoading={isLoadingResp}
310 data={responseErrors}
311 columns={[
312 {
313 key: 'request',
314 header: 'Request',
315 className: 'w-auto pr-0!',
316 render: (row) => {
317 return <StatusCode method={row.method} statusCode={row.status_code} />
318 },
319 },
320 {
321 key: 'path',
322 header: 'Path',
323 className: 'w-full',
324 render: (row) => (
325 <span className="line-clamp-1 font-mono text-foreground-light text-xs">
326 {row.path}
327 </span>
328 ),
329 },
330 {
331 key: 'count',
332 header: 'Count',
333 className: 'text-right shrink-0 ml-auto justify-end',
334 render: (row) => (
335 <div className="flex justify-end items-center gap-2">
336 <div className="text-right text-xs tabular-nums">{row.count}</div>
337 <LogsLink href={`/project/${ref}/logs/edge-logs?s=${row.path}`} />
338 </div>
339 ),
340 },
341 ]}
342 />
343 </ChartContent>
344 </ChartCard>
345 </Chart>
346
347 <Chart isLoading={isLoadingCodes}>
348 <ChartCard>
349 <ChartHeader>
350 <ChartTitle>Auth Server Errors</ChartTitle>
351 <ChartActions actions={errorCodesActions} />
352 </ChartHeader>
353 <ChartContent
354 className="p-0!"
355 isEmpty={errorCodes.length === 0}
356 emptyState={
357 <div className="p-6">
358 <ChartEmptyState
359 icon={<BarChart2 size={16} />}
360 title="No data to show"
361 description="It may take up to 24 hours for data to refresh"
362 />
363 </div>
364 }
365 loadingState={
366 <div className="p-6">
367 <ChartLoadingState />
368 </div>
369 }
370 >
371 <OverviewTable<AuthErrorCodeRow>
372 isLoading={isLoadingCodes}
373 data={errorCodes}
374 columns={[
375 {
376 key: 'error_code',
377 header: 'Error code',
378 className: 'w-full',
379 render: (row) => (
380 <ErrorCodeTooltip errorCode={row.error_code} service={Service.Auth}>
381 <span className="line-clamp-1 font-mono uppercase text-xs inline-flex text-foreground-light cursor-default">
382 {row.error_code}
383 </span>
384 </ErrorCodeTooltip>
385 ),
386 },
387 {
388 key: 'count',
389 header: 'Count',
390 className: 'text-right',
391 render: (row) => (
392 <div className="flex justify-end items-center gap-2">
393 <div className="text-right text-xs tabular-nums">{row.count}</div>
394 <LogsLink href={`/project/${ref}/logs/auth-logs?s=${row.error_code}`} />
395 </div>
396 ),
397 },
398 ]}
399 />
400 </ChartContent>
401 </ChartCard>
402 </Chart>
403 </div>
404 </PageSectionContent>
405 </PageSection>
406 </>
407 )
408}