mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 21:29:08 +03:00
feat(studio): auth overview monitoring section (#39327)
* feat: begin monitoring with compute metrics * chore: simplify monitoring constants * chore: something got added * feat: add database connections chart * make it success rate instead of error rate * add new metrics * hide monitoring * rm unused * rm * rm --------- Co-authored-by: Jordi Enric <jordi.err@gmail.com>
This commit is contained in:
1 parent
7e83c8a389
commit
a4d7bc6231
4 files changed
+98
-125
No files matched your search
@@ -1,16 +0,0 @@
|
||||
import { ScaffoldSectionTitle, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
import { Card } from 'ui'
|
||||
|
||||
export const OverviewMonitoring = () => {
|
||||
return (
|
||||
<ScaffoldSection isFullWidth>
|
||||
<ScaffoldSectionTitle className="mb-4">Monitoring</ScaffoldSectionTitle>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 opacity-30 pointer-events-none">
|
||||
<Card className="h-36" />
|
||||
<Card className="h-36" />
|
||||
<Card className="aspect-video" />
|
||||
<Card className="aspect-video" />
|
||||
</div>
|
||||
</ScaffoldSection>
|
||||
)
|
||||
}
|
||||
@@ -43,16 +43,39 @@ export const AUTH_COMBINED_QUERY = () => `
|
||||
select 'signUpCount' as metric,
|
||||
cast(count(case when action = 'user_signedup' then 1 else null end) as float64)
|
||||
from base
|
||||
|
||||
|
||||
union all
|
||||
select 'signInLatency' as metric,
|
||||
coalesce(round(avg(case when action = 'login' then duration_ns else null end) / 1000000, 2), 0)
|
||||
from base
|
||||
|
||||
select 'apiTotalRequests' as metric,
|
||||
cast(count(*) as float64) as value
|
||||
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%'
|
||||
|
||||
union all
|
||||
select 'signUpLatency' as metric,
|
||||
coalesce(round(avg(case when action = 'user_signedup' then duration_ns else null end) / 1000000, 2), 0)
|
||||
from base
|
||||
select 'apiErrorRequests' as metric,
|
||||
cast(count(*) as float64) as value
|
||||
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 >= 400 and response.status_code <= 599
|
||||
|
||||
union all
|
||||
select 'authErrorRequests' as metric,
|
||||
cast(count(*) as float64) as value
|
||||
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 >= 400 and response.status_code <= 599
|
||||
and h.x_sb_error_code is not null
|
||||
`
|
||||
|
||||
export const fetchAllAuthMetrics = async (projectRef: string, period: 'current' | 'previous') => {
|
||||
@@ -66,22 +89,31 @@ export const fetchAllAuthMetrics = async (projectRef: string, period: 'current'
|
||||
export const processAllAuthMetrics = (currentData: any[], previousData: any[]) => {
|
||||
const processData = (data: any[]) => {
|
||||
if (!data || !Array.isArray(data)) {
|
||||
return { activeUsers: 0, passwordResets: 0, signInLatency: 0, signUpLatency: 0 }
|
||||
return { activeUsers: 0, signUps: 0, apiErrorRate: 0, authErrorRate: 0 }
|
||||
}
|
||||
|
||||
const result = data.reduce(
|
||||
(acc, row) => {
|
||||
const { metric, value } = row
|
||||
if (metric === 'activeUsers') acc.activeUsers = value || 0
|
||||
if (metric === 'passwordResetRequests') acc.passwordResets = value || 0
|
||||
if (metric === 'signInLatency') acc.signInLatency = value || 0
|
||||
if (metric === 'signUpLatency') acc.signUpLatency = value || 0
|
||||
if (metric === 'signUpCount') acc.signUps = value || 0
|
||||
if (metric === 'apiTotalRequests') acc._apiTotal = value || 0
|
||||
if (metric === 'apiErrorRequests') acc._apiErrors = value || 0
|
||||
if (metric === 'authErrorRequests') acc._authErrors = value || 0
|
||||
return acc
|
||||
},
|
||||
{ activeUsers: 0, passwordResets: 0, signInLatency: 0, signUpLatency: 0 }
|
||||
{ activeUsers: 0, signUps: 0, _apiTotal: 0, _apiErrors: 0, _authErrors: 0 } as any
|
||||
)
|
||||
|
||||
return result
|
||||
const apiErrorRate = result._apiTotal > 0 ? (result._apiErrors / result._apiTotal) * 100 : 0
|
||||
const authErrorRate = result._apiTotal > 0 ? (result._authErrors / result._apiTotal) * 100 : 0
|
||||
|
||||
return {
|
||||
activeUsers: result.activeUsers,
|
||||
signUps: result.signUps,
|
||||
apiErrorRate,
|
||||
authErrorRate,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -9,17 +9,14 @@ import { useParams } from 'common'
|
||||
import { ChevronRight, Loader2 } from 'lucide-react'
|
||||
import { Reports } from 'icons'
|
||||
import {
|
||||
getChangeSign,
|
||||
getChangeColor,
|
||||
fetchAllAuthMetrics,
|
||||
processAllAuthMetrics,
|
||||
calculatePercentageChange,
|
||||
} from './OverviewUsage.constants'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import { ReportChartV2 } from 'components/interfaces/Reports/v2/ReportChartV2'
|
||||
import { createAuthReportConfig } from 'data/reports/v2/auth.config'
|
||||
import dayjs from 'dayjs'
|
||||
import { ArrowUpIcon, ArrowDownIcon } from 'lucide-react'
|
||||
|
||||
const StatCard = ({
|
||||
title,
|
||||
@@ -27,19 +24,32 @@ const StatCard = ({
|
||||
previous,
|
||||
loading,
|
||||
suffix = '',
|
||||
invert = false,
|
||||
href,
|
||||
}: {
|
||||
title: string
|
||||
current: number
|
||||
previous: number
|
||||
loading: boolean
|
||||
suffix?: string
|
||||
invert?: boolean
|
||||
href?: string
|
||||
}) => {
|
||||
const changeColor = getChangeColor(previous)
|
||||
const changeSign = getChangeSign(previous)
|
||||
const formattedCurrent = suffix === 'ms' ? current.toFixed(2) : current
|
||||
const isZeroChange = previous === 0
|
||||
const changeColor = isZeroChange
|
||||
? 'text-foreground-lighter'
|
||||
: invert
|
||||
? previous >= 0
|
||||
? 'text-destructive'
|
||||
: 'text-brand'
|
||||
: getChangeColor(previous)
|
||||
const formattedCurrent =
|
||||
suffix === 'ms' ? current.toFixed(2) : suffix === '%' ? current.toFixed(1) : Math.round(current)
|
||||
const ArrowIcon = previous >= 0 ? ArrowUpIcon : ArrowDownIcon
|
||||
const signChar = previous > 0 ? '+' : previous < 0 ? '-' : ''
|
||||
|
||||
return (
|
||||
<Card>
|
||||
const Inner = (
|
||||
<Card className={cn(href && 'cursor-pointer hover:bg-muted')}>
|
||||
<CardContent
|
||||
className={cn(
|
||||
'flex flex-col my-1 gap-1',
|
||||
@@ -51,15 +61,18 @@ const StatCard = ({
|
||||
) : (
|
||||
<>
|
||||
<h4 className="text-sm text-foreground-lighter font-normal mb-0 truncate">{title}</h4>
|
||||
<p className="text-xl">{`${formattedCurrent}${suffix}`}</p>
|
||||
<p className={cn('text-sm text-foreground-lighter', changeColor)}>
|
||||
{`${changeSign}${previous.toFixed(1)}%`}
|
||||
</p>
|
||||
<p className="text-xl font-mono">{`${formattedCurrent}${suffix}`}</p>
|
||||
<div className={cn('flex items-center gap-1 font-mono text-sm', changeColor)}>
|
||||
{!isZeroChange && <ArrowIcon className="size-3" />}
|
||||
<span>{`${signChar}${Math.abs(previous).toFixed(1)}%`}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
return href ? <Link href={href}>{Inner}</Link> : Inner
|
||||
}
|
||||
|
||||
export const OverviewUsage = () => {
|
||||
@@ -84,55 +97,13 @@ export const OverviewUsage = () => {
|
||||
metrics.current.activeUsers,
|
||||
metrics.previous.activeUsers
|
||||
)
|
||||
const passwordResetChange = calculatePercentageChange(
|
||||
metrics.current.passwordResets,
|
||||
metrics.previous.passwordResets
|
||||
)
|
||||
const signInLatencyChange = calculatePercentageChange(
|
||||
metrics.current.signInLatency,
|
||||
metrics.previous.signInLatency
|
||||
)
|
||||
const signUpLatencyChange = calculatePercentageChange(
|
||||
metrics.current.signUpLatency,
|
||||
metrics.previous.signUpLatency
|
||||
)
|
||||
|
||||
const signUpsChange = calculatePercentageChange(metrics.current.signUps, metrics.previous.signUps)
|
||||
|
||||
const endDate = dayjs().toISOString()
|
||||
const startDate = dayjs().subtract(24, 'hour').toISOString()
|
||||
|
||||
const signUpChartConfig = useMemo(() => {
|
||||
const config = createAuthReportConfig({
|
||||
projectRef: ref as string,
|
||||
startDate,
|
||||
endDate,
|
||||
interval: '1h',
|
||||
filters: { status_code: null },
|
||||
})
|
||||
const chart = config.find((c) => c.id === 'signups')
|
||||
if (chart) {
|
||||
return { ...chart, defaultChartStyle: 'bar' }
|
||||
}
|
||||
return chart
|
||||
}, [ref, startDate, endDate])
|
||||
|
||||
const signInChartConfig = useMemo(() => {
|
||||
const config = createAuthReportConfig({
|
||||
projectRef: ref as string,
|
||||
startDate,
|
||||
endDate,
|
||||
interval: '1h',
|
||||
filters: { status_code: null },
|
||||
})
|
||||
const chart = config.find((c) => c.id === 'sign-in-attempts')
|
||||
if (chart) {
|
||||
return { ...chart, defaultChartStyle: 'bar' }
|
||||
}
|
||||
return chart
|
||||
}, [ref, startDate, endDate])
|
||||
|
||||
const updateDateRange = (from: string, to: string) => {
|
||||
console.log('Date range update:', from, to)
|
||||
}
|
||||
// No charts on overview; keep date range for link only
|
||||
|
||||
return (
|
||||
<ScaffoldSection isFullWidth>
|
||||
@@ -150,54 +121,42 @@ export const OverviewUsage = () => {
|
||||
<ScaffoldSectionContent className="gap-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<StatCard
|
||||
title="Active Users"
|
||||
title="Active users"
|
||||
current={metrics.current.activeUsers}
|
||||
previous={activeUsersChange}
|
||||
loading={isLoading}
|
||||
href={`/project/${ref}/reports/auth#usage`}
|
||||
/>
|
||||
<StatCard
|
||||
title="Password Reset Requests"
|
||||
current={metrics.current.passwordResets}
|
||||
previous={passwordResetChange}
|
||||
title="Sign ups"
|
||||
current={metrics.current.signUps}
|
||||
previous={signUpsChange}
|
||||
loading={isLoading}
|
||||
href={`/project/${ref}/reports/auth#usage`}
|
||||
/>
|
||||
<StatCard
|
||||
title="Sign up Latency"
|
||||
current={Number(metrics.current.signUpLatency.toFixed(2))}
|
||||
previous={signUpLatencyChange}
|
||||
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={isLoading}
|
||||
suffix="ms"
|
||||
suffix="%"
|
||||
href={`/project/${ref}/reports/auth#monitoring`}
|
||||
/>
|
||||
<StatCard
|
||||
title="Sign in Latency"
|
||||
current={Number(metrics.current.signInLatency.toFixed(2))}
|
||||
previous={signInLatencyChange}
|
||||
title="Auth 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={isLoading}
|
||||
suffix="ms"
|
||||
suffix="%"
|
||||
href={`/project/${ref}/reports/auth#monitoring`}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{signUpChartConfig && (
|
||||
<ReportChartV2
|
||||
report={signUpChartConfig}
|
||||
projectRef={ref as string}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
interval="1h"
|
||||
updateDateRange={updateDateRange}
|
||||
/>
|
||||
)}
|
||||
{signInChartConfig && (
|
||||
<ReportChartV2
|
||||
report={signInChartConfig}
|
||||
projectRef={ref as string}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
interval="1h"
|
||||
updateDateRange={updateDateRange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ScaffoldSectionContent>
|
||||
</ScaffoldSection>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
|
||||
import { DocsButton } from 'components/ui/DocsButton'
|
||||
import { DOCS_URL } from 'lib/constants'
|
||||
import { OverviewMonitoring } from 'components/interfaces/Auth/Overview/OverviewMonitoring'
|
||||
import { OverviewUsage } from 'components/interfaces/Auth/Overview/OverviewUsage'
|
||||
import { OverviewLearnMore } from 'components/interfaces/Auth/Overview/OverviewLearnMore'
|
||||
import { useRouter } from 'next/router'
|
||||
@@ -32,7 +31,6 @@ const AuthOverview: NextPageWithLayout = () => {
|
||||
return (
|
||||
<ScaffoldContainer size="large">
|
||||
<div className="mb-4 flex flex-col gap-2">
|
||||
<OverviewMonitoring />
|
||||
<OverviewUsage />
|
||||
<OverviewLearnMore />
|
||||
</div>
|
||||
@@ -48,7 +46,7 @@ AuthOverview.getLayout = (page) => (
|
||||
secondaryActions={
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-foreground-light">
|
||||
All reports <span className="text-foreground">Last 24 hours</span>
|
||||
<span className="text-foreground">Last 24 hours</span>
|
||||
</span>
|
||||
<DocsButton href={`${DOCS_URL}/guides/auth`} />
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user