feat(telemetry): session journey view replaces raw events table
- sessions listed from client_events grouped by session id (7d window) - timeline sheet merges page views, client events, errors, and server-fn activities joined by player within the session window - vitals excluded from timelines; gap markers over 5 minutes
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { Fragment } from "react";
|
||||
import { Box, Group, Stack, Text } from "@mantine/core";
|
||||
import { Plural, useLingui } from "@lingui/react/macro";
|
||||
import { useSessionTimeline } from "../../queries";
|
||||
import type { TimelineEntry } from "../../types";
|
||||
|
||||
const GAP_MARKER_MS = 5 * 60 * 1000;
|
||||
|
||||
const parseTs = (ts: string) => Date.parse(ts.replace(" ", "T"));
|
||||
|
||||
const dotColor = (entry: TimelineEntry): string => {
|
||||
if (entry.kind === "error" || entry.success === false) return "var(--mantine-color-red-6)";
|
||||
if (entry.kind === "server_fn") return "var(--mantine-color-green-6)";
|
||||
return "var(--mantine-color-dimmed)";
|
||||
};
|
||||
|
||||
const Dot = ({ color }: { color: string }) => (
|
||||
<Box
|
||||
w={6}
|
||||
h={6}
|
||||
style={{ borderRadius: "50%", backgroundColor: color, flexShrink: 0 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SessionTimelineView = ({ sessionId }: { sessionId: string }) => {
|
||||
const { i18n } = useLingui();
|
||||
const { data } = useSessionTimeline(sessionId);
|
||||
|
||||
const clock = (ts: string) =>
|
||||
i18n.date(new Date(parseTs(ts)), { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
|
||||
return (
|
||||
<Stack gap={0}>
|
||||
<Group gap="md" pb="sm">
|
||||
<Text size="xs" c="dimmed">
|
||||
{i18n.date(new Date(parseTs(data.session.started)), {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
<Plural value={data.session.pageViews} one="# page" other="# pages" />
|
||||
</Text>
|
||||
{data.session.errors > 0 && (
|
||||
<Text size="xs" c="red">
|
||||
<Plural value={data.session.errors} one="# error" other="# errors" />
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{data.entries.map((entry, index) => {
|
||||
const prev = data.entries[index - 1];
|
||||
const gapMs = prev ? parseTs(entry.ts) - parseTs(prev.ts) : 0;
|
||||
return (
|
||||
<Fragment key={`${entry.ts}-${index}`}>
|
||||
{gapMs > GAP_MARKER_MS && (
|
||||
<Text size="xs" c="dimmed" py={4} pl={46}>
|
||||
+{Math.round(gapMs / 60000)}m
|
||||
</Text>
|
||||
)}
|
||||
<Group gap="xs" py={5} wrap="nowrap" align="center">
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
w={64}
|
||||
style={{ flexShrink: 0, fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
{clock(entry.ts)}
|
||||
</Text>
|
||||
<Dot color={dotColor(entry)} />
|
||||
<Text
|
||||
size="sm"
|
||||
lineClamp={1}
|
||||
c={entry.kind === "error" ? "red" : undefined}
|
||||
style={{ minWidth: 0, flex: 1 }}
|
||||
>
|
||||
{entry.label}
|
||||
</Text>
|
||||
{entry.kind === "server_fn" && entry.success === false && (
|
||||
<Text size="xs" c="red" style={{ flexShrink: 0 }}>
|
||||
✗
|
||||
</Text>
|
||||
)}
|
||||
{entry.kind === "server_fn" && typeof entry.duration === "number" && entry.duration > 0 && (
|
||||
<Text size="xs" c="dimmed" style={{ flexShrink: 0, fontVariantNumeric: "tabular-nums" }}>
|
||||
{entry.duration}ms
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionTimelineView;
|
||||
Reference in New Issue
Block a user