From a37ae0a3dcb2d6d048b3b3fda00a9c90c8087942 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Wed, 22 Oct 2025 17:15:15 +0200 Subject: [PATCH] swap error rate for success rate and add tests (#39774) * swap error rate for success rate and add tests * rm comment * format delta --- .../HomeNew/ProjectUsage.metrics.test.ts | 56 ++++++++++++++++ .../HomeNew/ProjectUsage.metrics.ts | 44 +++++++++++++ .../HomeNew/ProjectUsageSection.tsx | 66 ++++++------------- 3 files changed, 121 insertions(+), 45 deletions(-) create mode 100644 apps/studio/components/interfaces/HomeNew/ProjectUsage.metrics.test.ts create mode 100644 apps/studio/components/interfaces/HomeNew/ProjectUsage.metrics.ts diff --git a/apps/studio/components/interfaces/HomeNew/ProjectUsage.metrics.test.ts b/apps/studio/components/interfaces/HomeNew/ProjectUsage.metrics.test.ts new file mode 100644 index 00000000000..b6725227bf1 --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/ProjectUsage.metrics.test.ts @@ -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) + }) +}) diff --git a/apps/studio/components/interfaces/HomeNew/ProjectUsage.metrics.ts b/apps/studio/components/interfaces/HomeNew/ProjectUsage.metrics.ts new file mode 100644 index 00000000000..f98ec14a4b2 --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/ProjectUsage.metrics.ts @@ -0,0 +1,44 @@ +export type LogsBarChartDatum = { + timestamp: string + error_count: number + ok_count: number + warning_count: number +} + +export const toLogsBarChartData = ( + rows: Array> = [] +): 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)}%` diff --git a/apps/studio/components/interfaces/HomeNew/ProjectUsageSection.tsx b/apps/studio/components/interfaces/HomeNew/ProjectUsageSection.tsx index b29c45a6761..4984cc11d8f 100644 --- a/apps/studio/components/interfaces/HomeNew/ProjectUsageSection.tsx +++ b/apps/studio/components/interfaces/HomeNew/ProjectUsageSection.tsx @@ -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 (
@@ -289,11 +267,9 @@ export const ProjectUsageSection = () => {
- {errorRate.toFixed(1)}% - Error Rate - - {formatDelta(errorRateChangePct)} - + {successRate.toFixed(1)}% + Success Rate + {formatDelta(nonSuccessRate)}