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:
Jordi Enric authored and GitHub committed 2025-10-22 17:15:15 +02:00
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>