mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
swap error rate for success rate and add tests (#39774)
* swap error rate for success rate and add tests * rm comment * format delta
This commit is contained in:
1 parent
aa30698cc2
commit
a37ae0a3dc
3 files changed
+121
-45
No files matched your search
@@ -0,0 +1,56 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
computeChangePercent,
|
||||
computeSuccessAndNonSuccessRates,
|
||||
sumErrors,
|
||||
sumTotal,
|
||||
sumWarnings,
|
||||
toLogsBarChartData,
|
||||
} from './ProjectUsage.metrics'
|
||||
|
||||
describe('ProjectUsage.metrics', () => {
|
||||
const rows = [
|
||||
{ timestamp: '2025-10-22T13:00:00Z', ok_count: 90, warning_count: 5, error_count: 5 },
|
||||
{ timestamp: '2025-10-22T13:01:00Z', ok_count: 50, warning_count: 10, error_count: 0 },
|
||||
]
|
||||
|
||||
it('toLogsBarChartData maps and coerces fields correctly', () => {
|
||||
const data = toLogsBarChartData(rows)
|
||||
expect(data).toHaveLength(2)
|
||||
expect(data[0]).toEqual({
|
||||
timestamp: '2025-10-22T13:00:00Z',
|
||||
ok_count: 90,
|
||||
warning_count: 5,
|
||||
error_count: 5,
|
||||
})
|
||||
})
|
||||
|
||||
it('sum helpers compute totals correctly', () => {
|
||||
const data = toLogsBarChartData(rows)
|
||||
expect(sumTotal(data)).toBe(160)
|
||||
expect(sumWarnings(data)).toBe(15)
|
||||
expect(sumErrors(data)).toBe(5)
|
||||
})
|
||||
|
||||
it('computeSuccessAndNonSuccessRates returns expected percentages', () => {
|
||||
const data = toLogsBarChartData(rows)
|
||||
const total = sumTotal(data)
|
||||
const warns = sumWarnings(data)
|
||||
const errs = sumErrors(data)
|
||||
const { successRate, nonSuccessRate } = computeSuccessAndNonSuccessRates(total, warns, errs)
|
||||
|
||||
// success = 160 - (15 + 5) = 140 → 87.5%
|
||||
expect(successRate).toBeCloseTo(87.5)
|
||||
expect(nonSuccessRate).toBeCloseTo(12.5)
|
||||
})
|
||||
|
||||
it('computeChangePercent handles zero previous safely', () => {
|
||||
expect(computeChangePercent(10, 0)).toBe(100)
|
||||
expect(computeChangePercent(0, 0)).toBe(0)
|
||||
})
|
||||
|
||||
it('computeChangePercent returns standard percentage delta', () => {
|
||||
expect(computeChangePercent(120, 100)).toBe(20)
|
||||
expect(computeChangePercent(80, 100)).toBe(-20)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
export type LogsBarChartDatum = {
|
||||
timestamp: string
|
||||
error_count: number
|
||||
ok_count: number
|
||||
warning_count: number
|
||||
}
|
||||
|
||||
export const toLogsBarChartData = (
|
||||
rows: Array<Record<string, unknown>> = []
|
||||
): LogsBarChartDatum[] => {
|
||||
return rows.map((r) => ({
|
||||
timestamp: r.timestamp?.toString() ?? '',
|
||||
ok_count: Number(r.ok_count) || 0,
|
||||
warning_count: Number(r.warning_count) || 0,
|
||||
error_count: Number(r.error_count) || 0,
|
||||
}))
|
||||
}
|
||||
|
||||
export const sumTotal = (data: LogsBarChartDatum[]): number =>
|
||||
data.reduce((acc, r) => acc + r.ok_count + r.warning_count + r.error_count, 0)
|
||||
|
||||
export const sumWarnings = (data: LogsBarChartDatum[]): number =>
|
||||
data.reduce((acc, r) => acc + r.warning_count, 0)
|
||||
|
||||
export const sumErrors = (data: LogsBarChartDatum[]): number =>
|
||||
data.reduce((acc, r) => acc + r.error_count, 0)
|
||||
|
||||
export const computeSuccessAndNonSuccessRates = (
|
||||
totalRequests: number,
|
||||
totalWarnings: number,
|
||||
totalErrors: number
|
||||
): { successRate: number; nonSuccessRate: number } => {
|
||||
if (totalRequests <= 0) return { successRate: 0, nonSuccessRate: 0 }
|
||||
const nonSuccessRate = ((totalWarnings + totalErrors) / totalRequests) * 100
|
||||
const successRate = 100 - nonSuccessRate
|
||||
return { successRate, nonSuccessRate }
|
||||
}
|
||||
|
||||
export const computeChangePercent = (current: number, previous: number): number => {
|
||||
if (previous === 0) return current > 0 ? 100 : 0
|
||||
return ((current - previous) / previous) * 100
|
||||
}
|
||||
|
||||
export const formatDelta = (v: number): string => `${v >= 0 ? '+' : ''}${v.toFixed(1)}%`
|
||||
@@ -33,6 +33,16 @@ import {
|
||||
import { Row } from 'ui-patterns'
|
||||
import { LogsBarChart } from 'ui-patterns/LogsBarChart'
|
||||
import { useServiceStats } from './ProjectUsageSection.utils'
|
||||
import type { LogsBarChartDatum } from './ProjectUsage.metrics'
|
||||
import {
|
||||
toLogsBarChartData,
|
||||
sumTotal,
|
||||
sumWarnings,
|
||||
sumErrors,
|
||||
computeSuccessAndNonSuccessRates,
|
||||
computeChangePercent,
|
||||
formatDelta,
|
||||
} from './ProjectUsage.metrics'
|
||||
|
||||
const LOG_RETENTION = { free: 1, pro: 7, team: 28, enterprise: 90 }
|
||||
|
||||
@@ -65,13 +75,6 @@ const CHART_INTERVALS: ChartIntervals[] = [
|
||||
|
||||
type ChartIntervalKey = '1hr' | '1day' | '7day'
|
||||
|
||||
type LogsBarChartDatum = {
|
||||
timestamp: string
|
||||
error_count: number
|
||||
ok_count: number
|
||||
warning_count: number
|
||||
}
|
||||
|
||||
type ServiceKey = 'db' | 'functions' | 'auth' | 'storage' | 'realtime'
|
||||
|
||||
type ServiceEntry = {
|
||||
@@ -138,21 +141,6 @@ export const ProjectUsageSection = () => {
|
||||
previousEnd
|
||||
)
|
||||
|
||||
const toLogsBarChartData = (rows: any[] = []): LogsBarChartDatum[] => {
|
||||
return rows.map((r) => ({
|
||||
timestamp: String(r.timestamp),
|
||||
ok_count: Number(r.ok_count || 0),
|
||||
warning_count: Number(r.warning_count || 0),
|
||||
error_count: Number(r.error_count || 0),
|
||||
}))
|
||||
}
|
||||
|
||||
const sumTotal = (data: LogsBarChartDatum[]) =>
|
||||
data.reduce((acc, r) => acc + r.ok_count + r.warning_count + r.error_count, 0)
|
||||
const sumWarnings = (data: LogsBarChartDatum[]) =>
|
||||
data.reduce((acc, r) => acc + r.warning_count, 0)
|
||||
const sumErrors = (data: LogsBarChartDatum[]) => data.reduce((acc, r) => acc + r.error_count, 0)
|
||||
|
||||
const serviceBase: ServiceEntry[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -240,7 +228,12 @@ export const ProjectUsageSection = () => {
|
||||
const enabledServices = services.filter((s) => s.enabled)
|
||||
const totalRequests = enabledServices.reduce((sum, s) => sum + (s.total || 0), 0)
|
||||
const totalErrors = enabledServices.reduce((sum, s) => sum + (s.err || 0), 0)
|
||||
const errorRate = totalRequests > 0 ? (totalErrors / totalRequests) * 100 : 0
|
||||
const totalWarnings = enabledServices.reduce((sum, s) => sum + (s.warn || 0), 0)
|
||||
const { successRate, nonSuccessRate } = computeSuccessAndNonSuccessRates(
|
||||
totalRequests,
|
||||
totalWarnings,
|
||||
totalErrors
|
||||
)
|
||||
|
||||
const prevServiceTotals = useMemo(
|
||||
() =>
|
||||
@@ -250,7 +243,6 @@ export const ProjectUsageSection = () => {
|
||||
return {
|
||||
enabled: s.enabled,
|
||||
total: sumTotal(data),
|
||||
err: sumErrors(data),
|
||||
}
|
||||
}),
|
||||
[serviceBase, statsByService]
|
||||
@@ -258,24 +250,10 @@ export const ProjectUsageSection = () => {
|
||||
|
||||
const enabledPrev = prevServiceTotals.filter((s) => s.enabled)
|
||||
const prevTotalRequests = enabledPrev.reduce((sum, s) => sum + (s.total || 0), 0)
|
||||
const prevTotalErrors = enabledPrev.reduce((sum, s) => sum + (s.err || 0), 0)
|
||||
const prevErrorRate = prevTotalRequests > 0 ? (prevTotalErrors / prevTotalRequests) * 100 : 0
|
||||
|
||||
const totalRequestsChangePct =
|
||||
prevTotalRequests === 0
|
||||
? totalRequests > 0
|
||||
? 100
|
||||
: 0
|
||||
: ((totalRequests - prevTotalRequests) / prevTotalRequests) * 100
|
||||
const errorRateChangePct =
|
||||
prevErrorRate === 0
|
||||
? errorRate > 0
|
||||
? 100
|
||||
: 0
|
||||
: ((errorRate - prevErrorRate) / prevErrorRate) * 100
|
||||
const formatDelta = (v: number) => `${v >= 0 ? '+' : ''}${v.toFixed(1)}%`
|
||||
const totalRequestsChangePct = computeChangePercent(totalRequests, prevTotalRequests)
|
||||
const totalDeltaClass = totalRequestsChangePct >= 0 ? 'text-brand' : 'text-destructive'
|
||||
const errorDeltaClass = errorRateChangePct <= 0 ? 'text-brand' : 'text-destructive'
|
||||
const nonSuccessClass = nonSuccessRate > 0 ? 'text-destructive' : 'text-brand'
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -289,11 +267,9 @@ export const ProjectUsageSection = () => {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 heading-section text-foreground-light">
|
||||
<span className="text-foreground">{errorRate.toFixed(1)}%</span>
|
||||
<span>Error Rate</span>
|
||||
<span className={cn('text-sm', errorDeltaClass)}>
|
||||
{formatDelta(errorRateChangePct)}
|
||||
</span>
|
||||
<span className="text-foreground">{successRate.toFixed(1)}%</span>
|
||||
<span>Success Rate</span>
|
||||
<span className={cn('text-sm', nonSuccessClass)}>{formatDelta(nonSuccessRate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
|
||||
Reference in new issue
Block a user