Files
umami/src/components/metrics/PerformanceCard.tsx
T
Mike CaoandClaude Opus 4.6 ce9e2416fb Add Web Vitals performance tracking (LCP, INP, CLS, FCP, TTFB)
End-to-end performance metrics tracking: dedicated database table with
percentile aggregation, tracker collection behind data-perf attribute,
/api/send handling, report API endpoints, and Performance dashboard page
with threshold-based metric cards, time-series chart, and per-page breakdown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 19:25:20 -08:00

63 lines
1.9 KiB
TypeScript

import { useSpring } from '@react-spring/web';
import { Column, Text } from '@umami/react-zen';
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
import { useMessages } from '@/components/hooks';
import { WEB_VITALS_THRESHOLDS } from '@/lib/constants';
import { formatNumber } from '@/lib/format';
import styles from './PerformanceCard.module.css';
export interface PerformanceCardProps {
metric: 'lcp' | 'inp' | 'cls' | 'fcp' | 'ttfb';
value: number;
label: string;
formatValue?: (n: any) => string;
onClick?: () => void;
selected?: boolean;
}
function getRating(metric: string, value: number): 'good' | 'needs-improvement' | 'poor' {
const threshold = WEB_VITALS_THRESHOLDS[metric as keyof typeof WEB_VITALS_THRESHOLDS];
if (!threshold || value <= 0) return 'good';
if (value <= threshold.good) return 'good';
if (value <= threshold.poor) return 'needs-improvement';
return 'poor';
}
export const PerformanceCard = ({
metric,
value = 0,
label,
formatValue = formatNumber,
onClick,
selected = false,
}: PerformanceCardProps) => {
const { t, labels } = useMessages();
const rating = getRating(metric, value);
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
return (
<Column
className={`${styles.card} ${styles[rating]} ${selected ? styles.selected : ''}`}
justifyContent="center"
paddingX="6"
paddingY="4"
borderRadius
backgroundColor="surface-base"
border
gap="4"
onClick={onClick}
style={{ cursor: onClick ? 'pointer' : undefined }}
>
<Text weight="bold" wrap="nowrap">
{label}
</Text>
<Text size="4xl" weight="bold" wrap="nowrap">
<AnimatedDiv title={value?.toString()}>{props?.x?.to(x => formatValue(x))}</AnimatedDiv>
</Text>
<Text size="sm" className={styles.rating}>
{t(labels[rating === 'needs-improvement' ? 'needsImprovement' : rating])}
</Text>
</Column>
);
};