feat(telemetry): generic event aggregates and usage range presets
- rollups emit event.count (dim name) and event.detail.count (dim name|detail from props.source/emoji/tournament) - usage screen: events section with tap-through breakdown; 7d/30d/180d/custom date-range presets
This commit is contained in:
@@ -11,6 +11,8 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { ListSkeleton, StatTileSkeleton } from "@/components/skeletons";
|
||||
import { useRollupRange } from "../queries";
|
||||
import type { RollupRow } from "../types";
|
||||
import { DatePickerInput } from "@mantine/dates";
|
||||
import { fillDailySeries, lastNDays, latestValue, listDays, sumByDim } from "../range";
|
||||
import { BarList, Sparkline, StatTile } from "./charts";
|
||||
import { DetailSheet } from "./shared";
|
||||
@@ -25,6 +27,8 @@ export const USAGE_METRICS = [
|
||||
"server_fn.count",
|
||||
"client_error.count",
|
||||
"vital.p75",
|
||||
"event.count",
|
||||
"event.detail.count",
|
||||
];
|
||||
|
||||
const TOP_LIST_SIZE = 8;
|
||||
@@ -122,10 +126,37 @@ const FnDurationDetail = ({ fn, from, to }: { fn: string; from: string; to: stri
|
||||
);
|
||||
};
|
||||
|
||||
const EventBreakdown = ({ name, rows }: { name: string; rows: RollupRow[] }) => {
|
||||
const items = useMemo(() => {
|
||||
const totals = new Map<string, number>();
|
||||
const prefix = `${name}|`;
|
||||
for (const row of rows) {
|
||||
if (row.metric !== "event.detail.count" || !row.dim.startsWith(prefix)) continue;
|
||||
const detail = row.dim.slice(prefix.length);
|
||||
totals.set(detail, (totals.get(detail) ?? 0) + row.value);
|
||||
}
|
||||
return [...totals.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([label, value]) => ({ label, value }));
|
||||
}, [name, rows]);
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<Text size="sm" c="dimmed">
|
||||
<Trans>No breakdown recorded</Trans>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return <BarList items={items} />;
|
||||
};
|
||||
|
||||
const UsageSections = ({ from, to }: { from: string; to: string }) => {
|
||||
const { t, i18n } = useLingui();
|
||||
const [selectedFn, setSelectedFn] = useState<string | null>(null);
|
||||
const { isOpen: fnDetailOpened, open: openFnDetail, close: closeFnDetail } = useSheet();
|
||||
const [selectedEvent, setSelectedEvent] = useState<string | null>(null);
|
||||
const { isOpen: eventDetailOpened, open: openEventDetail, close: closeEventDetail } = useSheet();
|
||||
|
||||
const { data: rows } = useRollupRange({ metrics: USAGE_METRICS, from, to });
|
||||
|
||||
@@ -142,6 +173,10 @@ const UsageSections = ({ from, to }: { from: string; to: string }) => {
|
||||
setSelectedFn(dim);
|
||||
openFnDetail();
|
||||
});
|
||||
const eventItems = topItems(sumByDim(rows, "event.count"), (dim) => {
|
||||
setSelectedEvent(dim);
|
||||
openEventDetail();
|
||||
});
|
||||
|
||||
const vitals = (["LCP", "INP", "CLS"] as VitalName[]).map((name) => ({
|
||||
name,
|
||||
@@ -186,6 +221,26 @@ const UsageSections = ({ from, to }: { from: string; to: string }) => {
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack gap="sm">
|
||||
<Title order={4}>
|
||||
<Trans>Events</Trans>
|
||||
</Title>
|
||||
{eventItems.length > 0 ? (
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
<Trans>Top interactions, tap for breakdown</Trans>
|
||||
</Text>
|
||||
<BarList items={eventItems} />
|
||||
</Stack>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
<Trans>No events recorded in this range</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack gap="sm">
|
||||
<Title order={4}>
|
||||
<Trans>Server functions</Trans>
|
||||
@@ -230,6 +285,25 @@ const UsageSections = ({ from, to }: { from: string; to: string }) => {
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
<DetailSheet
|
||||
title={selectedEvent ?? t`Event`}
|
||||
opened={eventDetailOpened}
|
||||
onClose={() => {
|
||||
setSelectedEvent(null);
|
||||
closeEventDetail();
|
||||
}}
|
||||
fields={
|
||||
selectedEvent
|
||||
? [
|
||||
{
|
||||
label: <Trans>Breakdown</Trans>,
|
||||
value: <EventBreakdown name={selectedEvent} rows={rows} />,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
|
||||
<DetailSheet
|
||||
title={selectedFn ?? t`Function`}
|
||||
opened={fnDetailOpened}
|
||||
@@ -251,36 +325,61 @@ const UsageSections = ({ from, to }: { from: string; to: string }) => {
|
||||
|
||||
export const UsageScreen = () => {
|
||||
const { t } = useLingui();
|
||||
const [rangeDays, setRangeDays] = useState<"7" | "30">("7");
|
||||
const range = useMemo(() => lastNDays(Number(rangeDays)), [rangeDays]);
|
||||
const [preset, setPreset] = useState<"7" | "30" | "180" | "custom">("7");
|
||||
const [customRange, setCustomRange] = useState<[string | null, string | null]>([null, null]);
|
||||
|
||||
const range = useMemo(() => {
|
||||
if (preset !== "custom") return lastNDays(Number(preset));
|
||||
const [start, end] = customRange;
|
||||
if (!start || !end) return null;
|
||||
return { from: start, to: end };
|
||||
}, [preset, customRange]);
|
||||
|
||||
return (
|
||||
<Stack gap="md" px="md" pb="md">
|
||||
<SegmentedControl
|
||||
value={rangeDays}
|
||||
onChange={(value) => setRangeDays(value as "7" | "30")}
|
||||
value={preset}
|
||||
onChange={(value) => setPreset(value as typeof preset)}
|
||||
data={[
|
||||
{ value: "7", label: t`7d` },
|
||||
{ value: "30", label: t`30d` },
|
||||
{ value: "180", label: t`180d` },
|
||||
{ value: "custom", label: t`Custom` },
|
||||
]}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<Suspense
|
||||
fallback={
|
||||
<Stack gap="lg">
|
||||
<StatTileSkeleton align="row" />
|
||||
<SimpleGrid cols={3} spacing="sm">
|
||||
<StatTileSkeleton />
|
||||
<StatTileSkeleton />
|
||||
<StatTileSkeleton />
|
||||
</SimpleGrid>
|
||||
<ListSkeleton rows={6} withTrailing="text" />
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<UsageSections from={range.from} to={range.to} />
|
||||
</Suspense>
|
||||
{preset === "custom" && (
|
||||
<DatePickerInput
|
||||
type="range"
|
||||
value={customRange}
|
||||
onChange={setCustomRange}
|
||||
maxDate={new Date()}
|
||||
placeholder={t`Pick a date range`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{range ? (
|
||||
<Suspense
|
||||
fallback={
|
||||
<Stack gap="lg">
|
||||
<StatTileSkeleton align="row" />
|
||||
<SimpleGrid cols={3} spacing="sm">
|
||||
<StatTileSkeleton />
|
||||
<StatTileSkeleton />
|
||||
<StatTileSkeleton />
|
||||
</SimpleGrid>
|
||||
<ListSkeleton rows={6} withTrailing="text" />
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<UsageSections from={range.from} to={range.to} />
|
||||
</Suspense>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed" ta="center" py="xl">
|
||||
<Trans>Pick a date range to see usage</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -134,7 +134,7 @@ export const resolveClientErrorGroup = createServerFn({ method: "POST" })
|
||||
);
|
||||
|
||||
const rollupRangeSchema = z.object({
|
||||
metrics: z.array(z.string()).min(1).max(8),
|
||||
metrics: z.array(z.string()).min(1).max(12),
|
||||
from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
dim: z.string().optional(),
|
||||
|
||||
Reference in New Issue
Block a user