refactor(telemetry): chart scale labels, calmer vitals and error styling, usage-first tabs

- sparklines get start/end date and max-value footers
- vitals rating shown by value color instead of badges
- error rows use plain counts, subtle resolve button, terse empty state
- telemetry tab order: usage, players, functions, events, errors; usage is default
This commit is contained in:
2026-08-25 22:35:16 -07:00
parent 7ea189aa61
commit fd0489a22f
12 changed files with 222 additions and 238 deletions
@@ -54,7 +54,7 @@ export const ToolsList = () => {
<ListLink
label={t`Telemetry`}
Icon={PulseIcon}
to="/admin/telemetry/functions"
to="/admin/telemetry/usage"
/>
<ListLink
label={t`Award Badges`}
@@ -1,9 +1,15 @@
import { Text } from "@mantine/core";
import { Group, Text } from "@mantine/core";
import { niceExtent, pointsToPath } from "./chart-path";
const VIEW_WIDTH = 240;
const PAD = 3;
export interface SparklineScale {
start?: string;
end?: string;
formatValue?: (value: number) => string;
}
interface SparklineProps {
data: number[];
series2?: number[];
@@ -11,6 +17,7 @@ interface SparklineProps {
showArea?: boolean;
label?: string;
color?: string;
scale?: SparklineScale;
}
export const Sparkline = ({
@@ -20,6 +27,7 @@ export const Sparkline = ({
showArea = false,
label,
color = "var(--mantine-color-anchor)",
scale,
}: SparklineProps) => {
if (data.length === 0) {
return (
@@ -38,7 +46,11 @@ export const Sparkline = ({
? `${path} L${VIEW_WIDTH - PAD},${height - PAD} L${PAD},${height - PAD} Z`
: null;
return (
const allValues = series2?.length ? [...data, ...series2] : data;
const max = Math.max(...allValues);
const formatValue = scale?.formatValue ?? ((value: number) => value.toLocaleString());
const svg = (
<svg
viewBox={`0 0 ${VIEW_WIDTH} ${height}`}
preserveAspectRatio="none"
@@ -72,6 +84,25 @@ export const Sparkline = ({
)}
</svg>
);
if (!scale) return svg;
return (
<div>
{svg}
<Group justify="space-between" gap="xs" mt={2}>
<Text size="xs" c="dimmed">
{scale.start}
</Text>
<Text size="xs" c="dimmed" style={{ fontVariantNumeric: "tabular-nums" }}>
↑{formatValue(max)}
</Text>
<Text size="xs" c="dimmed">
{scale.end}
</Text>
</Group>
</div>
);
};
export default Sparkline;
@@ -1,21 +1,32 @@
import { ReactNode } from "react";
import { Box, Group, Stack, Text, UnstyledButton } from "@mantine/core";
import Sparkline from "./sparkline";
import Sparkline, { SparklineScale } from "./sparkline";
interface StatTileProps {
label: ReactNode;
value: ReactNode;
valueColor?: string;
delta?: number;
spark?: number[];
sparkScale?: SparklineScale;
caption?: ReactNode;
onClick?: () => void;
}
export const StatTile = ({ label, value, delta, spark, caption, onClick }: StatTileProps) => {
export const StatTile = ({
label,
value,
valueColor,
delta,
spark,
sparkScale,
caption,
onClick,
}: StatTileProps) => {
const content = (
<Stack gap={2}>
<Group gap="xs" align="baseline" wrap="nowrap">
<Text fz="xl" fw={700} lh={1.2}>
<Text fz="xl" fw={700} lh={1.2} c={valueColor}>
{value}
</Text>
{delta !== undefined && delta !== 0 && (
@@ -29,7 +40,9 @@ export const StatTile = ({ label, value, delta, spark, caption, onClick }: StatT
{label}
</Text>
{caption && <Box>{caption}</Box>}
{spark && spark.length > 0 && <Sparkline data={spark} height={28} />}
{spark && spark.length > 0 && (
<Sparkline data={spark} height={28} scale={sparkScale} />
)}
</Stack>
);
@@ -1,9 +1,7 @@
import { Suspense, useMemo, useState } from "react";
import { useLingui } from "@lingui/react/macro";
import { Container, Divider, Group, Stack, Switch } from "@mantine/core";
import { ChecksIcon } from "@phosphor-icons/react";
import { Trans, useLingui } from "@lingui/react/macro";
import { Container, Divider, Group, Stack, Switch, Text } from "@mantine/core";
import { ListSkeleton } from "@/components/skeletons";
import EmptyState from "@/components/empty-state";
import { useClientErrorGroups } from "../../queries";
import { lastNDays } from "../../range";
import { ErrorGroupRow } from "./error-group-row";
@@ -17,16 +15,13 @@ const ErrorGroupList = ({
to: string;
includeResolved: boolean;
}) => {
const { t } = useLingui();
const { data: groups } = useClientErrorGroups({ from, to, includeResolved });
if (groups.length === 0) {
return (
<EmptyState
icon={<ChecksIcon size={32} />}
title={t`No unresolved errors`}
description={t`Nothing has broken in the last 30 days. Keep it up.`}
/>
<Text size="sm" c="dimmed" px="md" py="xl" ta="center">
<Trans>No unresolved errors</Trans>
</Text>
);
}
@@ -1,7 +1,6 @@
import { useState } from "react";
import { Trans, useLingui } from "@lingui/react/macro";
import {
Badge,
Box,
Button,
Code,
@@ -42,9 +41,14 @@ export const ErrorGroupRow = ({ group }: { group: ClientErrorGroup }) => {
>
<Stack gap={4}>
<Group gap="xs" wrap="nowrap">
<Badge color={group.resolved ? "gray" : "red"} variant="light" size="sm">
{group.count.toLocaleString()}
</Badge>
<Text
size="sm"
fw={700}
c={group.resolved ? "dimmed" : "red"}
style={{ flexShrink: 0, fontVariantNumeric: "tabular-nums" }}
>
{group.count.toLocaleString()}×
</Text>
<Text size="sm" fw={600} lineClamp={2} style={{ minWidth: 0 }}>
{group.sample.message}
</Text>
@@ -85,7 +89,7 @@ export const ErrorGroupRow = ({ group }: { group: ClientErrorGroup }) => {
<Group justify="flex-end">
<Button
size="xs"
variant="light"
variant="subtle"
color={group.resolved ? "yellow" : "green"}
loading={resolveMutation.isPending}
onClick={() =>
@@ -1,7 +1,6 @@
import { Suspense, useMemo, useState } from "react";
import { Trans, useLingui } from "@lingui/react/macro";
import {
Badge,
Divider,
Group,
SegmentedControl,
@@ -45,25 +44,18 @@ const vitalRating = (name: VitalName, value: number): "good" | "ok" | "poor" =>
return "ok";
};
const VitalBadge = ({ name, value }: { name: VitalName; value: number }) => {
const vitalColor = (name: VitalName, value: number): string | undefined => {
const rating = vitalRating(name, value);
const color = rating === "good" ? "green" : rating === "ok" ? "yellow" : "red";
return (
<Badge color={color} variant="light" size="sm">
{rating === "good" ? (
<Trans>Good</Trans>
) : rating === "ok" ? (
<Trans>Needs improvement</Trans>
) : (
<Trans>Poor</Trans>
)}
</Badge>
);
if (rating === "good") return undefined;
return rating === "ok" ? "yellow.8" : "red";
};
const formatVital = (name: VitalName, value: number) =>
name === "CLS" ? value.toFixed(2) : `${Math.round(value)}ms`;
const dayLabel = (i18n: { date: (d: Date, opts?: Intl.DateTimeFormatOptions) => string }, day: string) =>
i18n.date(new Date(`${day}T12:00:00Z`), { month: "short", day: "numeric" });
const topItems = (totals: Map<string, number>, onClick?: (dim: string) => void) =>
[...totals.entries()]
.sort((a, b) => b[1] - a[1])
@@ -75,6 +67,7 @@ const topItems = (totals: Map<string, number>, onClick?: (dim: string) => void)
}));
const FnDurationDetail = ({ fn, from, to }: { fn: string; from: string; to: string }) => {
const { i18n } = useLingui();
const { data: rows } = useRollupRange({
metrics: ["server_fn.duration.p50", "server_fn.duration.p95"],
from,
@@ -87,7 +80,17 @@ const FnDurationDetail = ({ fn, from, to }: { fn: string; from: string; to: stri
return (
<Stack gap="xs">
<Sparkline data={p50} series2={p95} height={64} showArea />
<Sparkline
data={p50}
series2={p95}
height={64}
showArea
scale={{
start: dayLabel(i18n, from),
end: dayLabel(i18n, to),
formatValue: (value) => `${Math.round(value)}ms`,
}}
/>
<Group gap="md">
<Group gap={6}>
<svg width={18} height={8} aria-hidden>
@@ -119,7 +122,7 @@ const FnDurationDetail = ({ fn, from, to }: { fn: string; from: string; to: stri
};
const UsageSections = ({ from, to }: { from: string; to: string }) => {
const { t } = useLingui();
const { t, i18n } = useLingui();
const [selectedFn, setSelectedFn] = useState<string | null>(null);
const { isOpen: fnDetailOpened, open: openFnDetail, close: closeFnDetail } = useSheet();
@@ -159,6 +162,7 @@ const UsageSections = ({ from, to }: { from: string; to: string }) => {
label={<Trans>Daily active players</Trans>}
value={(dauSeries[dauSeries.length - 1] ?? 0).toLocaleString()}
spark={dauSeries}
sparkScale={{ start: dayLabel(i18n, from), end: dayLabel(i18n, to) }}
/>
<SimpleGrid cols={3} spacing="sm">
<StatTile
@@ -200,9 +204,10 @@ const UsageSections = ({ from, to }: { from: string; to: string }) => {
<Trans>Errors</Trans>
</Title>
<StatTile
label={<Trans>Client errors per day</Trans>}
label={<Trans>Client errors</Trans>}
value={errorSeries.reduce((sum, value) => sum + value, 0).toLocaleString()}
spark={errorSeries}
sparkScale={{ start: dayLabel(i18n, from), end: dayLabel(i18n, to) }}
/>
</Stack>
@@ -218,7 +223,7 @@ const UsageSections = ({ from, to }: { from: string; to: string }) => {
key={name}
label={name}
value={value !== undefined ? formatVital(name, value) : "—"}
caption={value !== undefined ? <VitalBadge name={name} value={value} /> : undefined}
valueColor={value !== undefined ? vitalColor(name, value) : undefined}
/>
))}
</SimpleGrid>