diff --git a/src/app/routeTree.gen.ts b/src/app/routeTree.gen.ts
index 99fca68..298cff7 100644
--- a/src/app/routeTree.gen.ts
+++ b/src/app/routeTree.gen.ts
@@ -48,9 +48,9 @@ import { Route as AuthedTournamentsIdPredictionsRouteImport } from './routes/_au
import { Route as AuthedTournamentsIdGroupsRouteImport } from './routes/_authed/tournaments/$id.groups'
import { Route as AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket'
import { Route as AuthedAdminTelemetryUsageRouteImport } from './routes/_authed/admin/telemetry/usage'
+import { Route as AuthedAdminTelemetrySessionsRouteImport } from './routes/_authed/admin/telemetry/sessions'
import { Route as AuthedAdminTelemetryPlayersRouteImport } from './routes/_authed/admin/telemetry/players'
import { Route as AuthedAdminTelemetryFunctionsRouteImport } from './routes/_authed/admin/telemetry/functions'
-import { Route as AuthedAdminTelemetryEventsRouteImport } from './routes/_authed/admin/telemetry/events'
import { Route as AuthedAdminTelemetryErrorsRouteImport } from './routes/_authed/admin/telemetry/errors'
import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_authed/admin/tournaments/$id/index'
import { Route as ApiFilesCollectionRecordIdFileRouteImport } from './routes/api/files/$collection/$recordId/$file'
@@ -263,6 +263,12 @@ const AuthedAdminTelemetryUsageRoute =
path: '/usage',
getParentRoute: () => AuthedAdminTelemetryRoute,
} as any)
+const AuthedAdminTelemetrySessionsRoute =
+ AuthedAdminTelemetrySessionsRouteImport.update({
+ id: '/sessions',
+ path: '/sessions',
+ getParentRoute: () => AuthedAdminTelemetryRoute,
+ } as any)
const AuthedAdminTelemetryPlayersRoute =
AuthedAdminTelemetryPlayersRouteImport.update({
id: '/players',
@@ -275,12 +281,6 @@ const AuthedAdminTelemetryFunctionsRoute =
path: '/functions',
getParentRoute: () => AuthedAdminTelemetryRoute,
} as any)
-const AuthedAdminTelemetryEventsRoute =
- AuthedAdminTelemetryEventsRouteImport.update({
- id: '/events',
- path: '/events',
- getParentRoute: () => AuthedAdminTelemetryRoute,
- } as any)
const AuthedAdminTelemetryErrorsRoute =
AuthedAdminTelemetryErrorsRouteImport.update({
id: '/errors',
@@ -370,9 +370,9 @@ export interface FileRoutesByFullPath {
'/admin/': typeof AuthedAdminIndexRoute
'/tournaments/': typeof AuthedTournamentsIndexRoute
'/admin/telemetry/errors': typeof AuthedAdminTelemetryErrorsRoute
- '/admin/telemetry/events': typeof AuthedAdminTelemetryEventsRoute
'/admin/telemetry/functions': typeof AuthedAdminTelemetryFunctionsRoute
'/admin/telemetry/players': typeof AuthedAdminTelemetryPlayersRoute
+ '/admin/telemetry/sessions': typeof AuthedAdminTelemetrySessionsRoute
'/admin/telemetry/usage': typeof AuthedAdminTelemetryUsageRoute
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
'/tournaments/$id/groups': typeof AuthedTournamentsIdGroupsRoute
@@ -420,9 +420,9 @@ export interface FileRoutesByTo {
'/admin': typeof AuthedAdminIndexRoute
'/tournaments': typeof AuthedTournamentsIndexRoute
'/admin/telemetry/errors': typeof AuthedAdminTelemetryErrorsRoute
- '/admin/telemetry/events': typeof AuthedAdminTelemetryEventsRoute
'/admin/telemetry/functions': typeof AuthedAdminTelemetryFunctionsRoute
'/admin/telemetry/players': typeof AuthedAdminTelemetryPlayersRoute
+ '/admin/telemetry/sessions': typeof AuthedAdminTelemetrySessionsRoute
'/admin/telemetry/usage': typeof AuthedAdminTelemetryUsageRoute
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
'/tournaments/$id/groups': typeof AuthedTournamentsIdGroupsRoute
@@ -474,9 +474,9 @@ export interface FileRoutesById {
'/_authed/admin/': typeof AuthedAdminIndexRoute
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
'/_authed/admin/telemetry/errors': typeof AuthedAdminTelemetryErrorsRoute
- '/_authed/admin/telemetry/events': typeof AuthedAdminTelemetryEventsRoute
'/_authed/admin/telemetry/functions': typeof AuthedAdminTelemetryFunctionsRoute
'/_authed/admin/telemetry/players': typeof AuthedAdminTelemetryPlayersRoute
+ '/_authed/admin/telemetry/sessions': typeof AuthedAdminTelemetrySessionsRoute
'/_authed/admin/telemetry/usage': typeof AuthedAdminTelemetryUsageRoute
'/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
'/_authed/tournaments/$id/groups': typeof AuthedTournamentsIdGroupsRoute
@@ -528,9 +528,9 @@ export interface FileRouteTypes {
| '/admin/'
| '/tournaments/'
| '/admin/telemetry/errors'
- | '/admin/telemetry/events'
| '/admin/telemetry/functions'
| '/admin/telemetry/players'
+ | '/admin/telemetry/sessions'
| '/admin/telemetry/usage'
| '/tournaments/$id/bracket'
| '/tournaments/$id/groups'
@@ -578,9 +578,9 @@ export interface FileRouteTypes {
| '/admin'
| '/tournaments'
| '/admin/telemetry/errors'
- | '/admin/telemetry/events'
| '/admin/telemetry/functions'
| '/admin/telemetry/players'
+ | '/admin/telemetry/sessions'
| '/admin/telemetry/usage'
| '/tournaments/$id/bracket'
| '/tournaments/$id/groups'
@@ -631,9 +631,9 @@ export interface FileRouteTypes {
| '/_authed/admin/'
| '/_authed/tournaments/'
| '/_authed/admin/telemetry/errors'
- | '/_authed/admin/telemetry/events'
| '/_authed/admin/telemetry/functions'
| '/_authed/admin/telemetry/players'
+ | '/_authed/admin/telemetry/sessions'
| '/_authed/admin/telemetry/usage'
| '/_authed/tournaments/$id/bracket'
| '/_authed/tournaments/$id/groups'
@@ -948,6 +948,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthedAdminTelemetryUsageRouteImport
parentRoute: typeof AuthedAdminTelemetryRoute
}
+ '/_authed/admin/telemetry/sessions': {
+ id: '/_authed/admin/telemetry/sessions'
+ path: '/sessions'
+ fullPath: '/admin/telemetry/sessions'
+ preLoaderRoute: typeof AuthedAdminTelemetrySessionsRouteImport
+ parentRoute: typeof AuthedAdminTelemetryRoute
+ }
'/_authed/admin/telemetry/players': {
id: '/_authed/admin/telemetry/players'
path: '/players'
@@ -962,13 +969,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthedAdminTelemetryFunctionsRouteImport
parentRoute: typeof AuthedAdminTelemetryRoute
}
- '/_authed/admin/telemetry/events': {
- id: '/_authed/admin/telemetry/events'
- path: '/events'
- fullPath: '/admin/telemetry/events'
- preLoaderRoute: typeof AuthedAdminTelemetryEventsRouteImport
- parentRoute: typeof AuthedAdminTelemetryRoute
- }
'/_authed/admin/telemetry/errors': {
id: '/_authed/admin/telemetry/errors'
path: '/errors'
@@ -1037,18 +1037,18 @@ declare module '@tanstack/react-router' {
interface AuthedAdminTelemetryRouteChildren {
AuthedAdminTelemetryErrorsRoute: typeof AuthedAdminTelemetryErrorsRoute
- AuthedAdminTelemetryEventsRoute: typeof AuthedAdminTelemetryEventsRoute
AuthedAdminTelemetryFunctionsRoute: typeof AuthedAdminTelemetryFunctionsRoute
AuthedAdminTelemetryPlayersRoute: typeof AuthedAdminTelemetryPlayersRoute
+ AuthedAdminTelemetrySessionsRoute: typeof AuthedAdminTelemetrySessionsRoute
AuthedAdminTelemetryUsageRoute: typeof AuthedAdminTelemetryUsageRoute
AuthedAdminTelemetryIndexRoute: typeof AuthedAdminTelemetryIndexRoute
}
const AuthedAdminTelemetryRouteChildren: AuthedAdminTelemetryRouteChildren = {
AuthedAdminTelemetryErrorsRoute: AuthedAdminTelemetryErrorsRoute,
- AuthedAdminTelemetryEventsRoute: AuthedAdminTelemetryEventsRoute,
AuthedAdminTelemetryFunctionsRoute: AuthedAdminTelemetryFunctionsRoute,
AuthedAdminTelemetryPlayersRoute: AuthedAdminTelemetryPlayersRoute,
+ AuthedAdminTelemetrySessionsRoute: AuthedAdminTelemetrySessionsRoute,
AuthedAdminTelemetryUsageRoute: AuthedAdminTelemetryUsageRoute,
AuthedAdminTelemetryIndexRoute: AuthedAdminTelemetryIndexRoute,
}
diff --git a/src/app/routes/_authed/admin/telemetry.tsx b/src/app/routes/_authed/admin/telemetry.tsx
index c4e6735..5093635 100644
--- a/src/app/routes/_authed/admin/telemetry.tsx
+++ b/src/app/routes/_authed/admin/telemetry.tsx
@@ -7,7 +7,7 @@ const TAB_ROUTES = {
usage: "/admin/telemetry/usage",
players: "/admin/telemetry/players",
functions: "/admin/telemetry/functions",
- events: "/admin/telemetry/events",
+ sessions: "/admin/telemetry/sessions",
errors: "/admin/telemetry/errors",
} as const;
@@ -50,8 +50,8 @@ function TelemetryLayout() {
Functions
-
- Events
+
+ Sessions
Errors
diff --git a/src/app/routes/_authed/admin/telemetry/events.tsx b/src/app/routes/_authed/admin/telemetry/events.tsx
deleted file mode 100644
index cc030b6..0000000
--- a/src/app/routes/_authed/admin/telemetry/events.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { createFileRoute } from "@tanstack/react-router";
-import { Suspense } from "react";
-import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
-import { telemetryQueries, defaultClientEventParams, ClientEventsTable } from "@/features/telemetry";
-import { playerQueries } from "@/features/players";
-import { ListSkeleton } from "@/components/skeletons";
-
-export const Route = createFileRoute("/_authed/admin/telemetry/events")({
- component: EventsTab,
- beforeLoad: ({ context }) => {
- const queryClient = context.queryClient;
- prefetchServerQuery(queryClient, telemetryQueries.events(defaultClientEventParams));
- prefetchServerQuery(queryClient, playerQueries.list());
- },
- loader: () => ({
- withPadding: false,
- fullWidth: true,
- refresh: [telemetryQueries.events(defaultClientEventParams).queryKey],
- }),
-});
-
-function EventsTab() {
- return (
- }>
-
-
- );
-}
diff --git a/src/app/routes/_authed/admin/telemetry/sessions.tsx b/src/app/routes/_authed/admin/telemetry/sessions.tsx
new file mode 100644
index 0000000..ea22cb7
--- /dev/null
+++ b/src/app/routes/_authed/admin/telemetry/sessions.tsx
@@ -0,0 +1,25 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { Suspense } from "react";
+import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
+import { telemetryQueries, SessionsList, defaultSessionParams } from "@/features/telemetry";
+import { ListSkeleton } from "@/components/skeletons";
+
+export const Route = createFileRoute("/_authed/admin/telemetry/sessions")({
+ component: SessionsTab,
+ beforeLoad: ({ context }) => {
+ prefetchServerQuery(context.queryClient, telemetryQueries.sessions(defaultSessionParams));
+ },
+ loader: () => ({
+ withPadding: false,
+ fullWidth: true,
+ refresh: [telemetryQueries.sessions(defaultSessionParams).queryKey],
+ }),
+});
+
+function SessionsTab() {
+ return (
+ }>
+
+
+ );
+}
diff --git a/src/features/telemetry/components/client-events-table.tsx b/src/features/telemetry/components/client-events-table.tsx
deleted file mode 100644
index b69b695..0000000
--- a/src/features/telemetry/components/client-events-table.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-import { useState, useMemo } from "react";
-import { Trans, Plural, useLingui } from "@lingui/react/macro";
-import { Code, Container, Stack } from "@mantine/core";
-import { usePlayers } from "@/features/players/queries";
-import { useSheet } from "@/hooks/use-sheet";
-import { useClientEvents } from "../queries";
-import type { ClientEvent, ClientEventSearchParams } from "../types";
-import {
- DetailSheet,
- PaginatedList,
- RelativeTime,
- SearchFilterBar,
- TelemetryListItem,
-} from "./shared";
-
-export const defaultClientEventParams: ClientEventSearchParams = {
- page: 1,
- perPage: 50,
- sortBy: "-created",
-};
-
-export const ClientEventsTable = () => {
- const { t, i18n } = useLingui();
- const [page, setPage] = useState(1);
- const [search, setSearch] = useState("");
- const [playerFilter, setPlayerFilter] = useState(null);
- const [selectedEvent, setSelectedEvent] = useState(null);
- const { isOpen: detailsOpened, open: openDetails, close: closeDetails } = useSheet();
-
- const { data: players } = usePlayers();
-
- const searchParams: ClientEventSearchParams = useMemo(
- () => ({
- page,
- perPage: 50,
- name: search || undefined,
- player: playerFilter || undefined,
- sortBy: "-created",
- }),
- [page, search, playerFilter]
- );
-
- const { data: result } = useClientEvents(searchParams);
-
- const playerName = (event: ClientEvent) =>
- typeof event.player === "object" && event.player
- ? `${event.player.first_name} ${event.player.last_name}`
- : t`Anonymous`;
-
- const handleEventClick = (event: ClientEvent) => {
- setSelectedEvent(event);
- openDetails();
- };
-
- const handleCloseDetails = () => {
- setSelectedEvent(null);
- closeDetails();
- };
-
- return (
-
-
- {
- setSearch(value);
- setPage(1);
- }}
- searchPlaceholder={t`Event name`}
- filters={[
- {
- placeholder: t`Player`,
- value: playerFilter,
- onChange: (value) => {
- setPlayerFilter(value);
- setPage(1);
- },
- data: players.map((player) => ({
- value: player.id,
- label: `${player.first_name} ${player.last_name}`,
- })),
- searchable: true,
- },
- ]}
- summary={
-
- }
- />
-
- event.id}
- empty={t`No Events Found`}
- page={page}
- totalPages={result.totalPages}
- onPageChange={setPage}
- renderItem={(event) => (
- ,
- ]}
- onClick={() => handleEventClick(event)}
- />
- )}
- />
-
-
- Name, value: selectedEvent.name },
- { label: Player, value: playerName(selectedEvent) },
- ...(selectedEvent.path
- ? [{ label: Path, value: selectedEvent.path }]
- : []),
- ...(selectedEvent.route_id
- ? [{ label: Route, value: selectedEvent.route_id }]
- : []),
- ...(selectedEvent.session_id
- ? [{ label: Session, value: selectedEvent.session_id }]
- : []),
- ...(selectedEvent.value !== undefined && selectedEvent.value !== null
- ? [{ label: Value, value: selectedEvent.value }]
- : []),
- ...(selectedEvent.props
- ? [
- {
- label: Props,
- value: (
-
- {JSON.stringify(selectedEvent.props, null, 2)}
-
- ),
- },
- ]
- : []),
- ...(selectedEvent.user_agent
- ? [{ label: User Agent, value: selectedEvent.user_agent }]
- : []),
- {
- label: Created,
- value: new Date(selectedEvent.created).toLocaleString(i18n.locale),
- },
- ]
- : []
- }
- />
-
- );
-};
diff --git a/src/features/telemetry/components/sessions/session-timeline.tsx b/src/features/telemetry/components/sessions/session-timeline.tsx
new file mode 100644
index 0000000..8237783
--- /dev/null
+++ b/src/features/telemetry/components/sessions/session-timeline.tsx
@@ -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 }) => (
+
+);
+
+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 (
+
+
+
+ {i18n.date(new Date(parseTs(data.session.started)), {
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ })}
+
+
+
+
+ {data.session.errors > 0 && (
+
+
+
+ )}
+
+
+ {data.entries.map((entry, index) => {
+ const prev = data.entries[index - 1];
+ const gapMs = prev ? parseTs(entry.ts) - parseTs(prev.ts) : 0;
+ return (
+
+ {gapMs > GAP_MARKER_MS && (
+
+ +{Math.round(gapMs / 60000)}m
+
+ )}
+
+
+ {clock(entry.ts)}
+
+
+
+ {entry.label}
+
+ {entry.kind === "server_fn" && entry.success === false && (
+
+ ✗
+
+ )}
+ {entry.kind === "server_fn" && typeof entry.duration === "number" && entry.duration > 0 && (
+
+ {entry.duration}ms
+
+ )}
+
+
+ );
+ })}
+
+ );
+};
+
+export default SessionTimelineView;
diff --git a/src/features/telemetry/components/sessions/sessions-list.tsx b/src/features/telemetry/components/sessions/sessions-list.tsx
new file mode 100644
index 0000000..6d08461
--- /dev/null
+++ b/src/features/telemetry/components/sessions/sessions-list.tsx
@@ -0,0 +1,81 @@
+import { Suspense, useState } from "react";
+import { Plural, Trans, useLingui } from "@lingui/react/macro";
+import { Text } from "@mantine/core";
+import Sheet from "@/components/sheet/sheet";
+import { ListSkeleton } from "@/components/skeletons";
+import { useSessions } from "../../queries";
+import type { SessionSummary } from "../../types";
+import { PaginatedList, RelativeTime, TelemetryListItem } from "../shared";
+import SessionTimelineView from "./session-timeline";
+
+export const defaultSessionParams = { days: 7, page: 1 } as const;
+
+const playerName = (player?: SessionSummary["player"]): string | null => {
+ if (!player) return null;
+ return [player.first_name, player.last_name].filter(Boolean).join(" ") || null;
+};
+
+const durationLabel = (session: SessionSummary): string => {
+ const ms =
+ Date.parse(session.lastSeen.replace(" ", "T")) -
+ Date.parse(session.started.replace(" ", "T"));
+ const minutes = Math.round(ms / 60000);
+ return minutes < 1 ? "<1m" : `${minutes}m`;
+};
+
+export const SessionsList = () => {
+ const { t } = useLingui();
+ const [page, setPage] = useState(1);
+ const [selected, setSelected] = useState(null);
+
+ const { data } = useSessions({ days: 7, page });
+
+ return (
+ <>
+ session.session_id}
+ empty={t`No sessions yet`}
+ page={data.page}
+ totalPages={data.totalPages}
+ onPageChange={setPage}
+ renderItem={(session) => (
+ ,
+ ,
+ durationLabel(session),
+ ]}
+ error={
+ session.errors > 0 ? (
+
+ ) : undefined
+ }
+ onClick={() => setSelected(session)}
+ />
+ )}
+ />
+
+ setSelected(null)}
+ >
+ {selected && (
+ }>
+
+
+ )}
+
+
+ {data.totalItems > 0 && (
+
+ Last 7 days
+
+ )}
+ >
+ );
+};
+
+export default SessionsList;
diff --git a/src/features/telemetry/index.ts b/src/features/telemetry/index.ts
index 65ab60b..bb7200f 100644
--- a/src/features/telemetry/index.ts
+++ b/src/features/telemetry/index.ts
@@ -3,6 +3,7 @@ export * from "./queries";
export * from "./range";
export * from "./components/charts";
export * from "./components/shared";
-export { ClientEventsTable, defaultClientEventParams } from "./components/client-events-table";
+export { SessionsList, defaultSessionParams } from "./components/sessions/sessions-list";
+export { SessionTimelineView } from "./components/sessions/session-timeline";
export { ErrorBrowser } from "./components/error-browser";
export { UsageScreen, USAGE_METRICS } from "./components/usage-screen";
diff --git a/src/features/telemetry/queries.ts b/src/features/telemetry/queries.ts
index 6f41bed..87f7952 100644
--- a/src/features/telemetry/queries.ts
+++ b/src/features/telemetry/queries.ts
@@ -2,7 +2,9 @@ import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
import {
getClientErrorGroups,
getRollupRange,
+ getSessionTimeline,
getTelemetryRuntimeStatus,
+ listSessions,
listTelemetryAlerts,
searchClientErrors,
searchClientEvents,
@@ -30,6 +32,10 @@ export const telemetryKeys = {
errorGroups: (params: ErrorGroupParams) => ["telemetry", "errorGroups", params] as const,
rollups: (params: RollupRangeParams) => ["telemetry", "rollups", params] as const,
alerts: (page: number) => ["telemetry", "alerts", page] as const,
+ sessions: (params: { days?: number; page?: number }) =>
+ ["telemetry", "sessions", params] as const,
+ sessionTimeline: (sessionId: string) =>
+ ["telemetry", "sessionTimeline", sessionId] as const,
runtimeStatus: ["telemetry", "runtimeStatus"] as const,
};
@@ -54,6 +60,14 @@ export const telemetryQueries = {
queryKey: telemetryKeys.alerts(page),
queryFn: () => listTelemetryAlerts({ data: { page } }),
}),
+ sessions: (params: { days?: number; page?: number } = {}) => ({
+ queryKey: telemetryKeys.sessions(params),
+ queryFn: () => listSessions({ data: params }),
+ }),
+ sessionTimeline: (sessionId: string) => ({
+ queryKey: telemetryKeys.sessionTimeline(sessionId),
+ queryFn: () => getSessionTimeline({ data: { sessionId } }),
+ }),
runtimeStatus: () => ({
queryKey: telemetryKeys.runtimeStatus,
queryFn: () => getTelemetryRuntimeStatus(),
@@ -72,5 +86,11 @@ export const useClientErrorGroups = (params: ErrorGroupParams) =>
export const useRollupRange = (params: RollupRangeParams) =>
useServerSuspenseQuery(telemetryQueries.rollups(params));
+export const useSessions = (params: { days?: number; page?: number } = {}) =>
+ useServerSuspenseQuery(telemetryQueries.sessions(params));
+
+export const useSessionTimeline = (sessionId: string) =>
+ useServerSuspenseQuery(telemetryQueries.sessionTimeline(sessionId));
+
export const useTelemetryRuntimeStatus = () =>
useServerSuspenseQuery(telemetryQueries.runtimeStatus());
diff --git a/src/features/telemetry/server.ts b/src/features/telemetry/server.ts
index f239fa3..ec356dc 100644
--- a/src/features/telemetry/server.ts
+++ b/src/features/telemetry/server.ts
@@ -15,9 +15,12 @@ import type {
ClientErrorGroup,
ClientEvent,
RollupRow,
+ SessionSummary,
+ SessionTimeline,
TelemetryAlert,
TelemetryListResult,
TelemetryRuntimeStatus,
+ TimelineEntry,
} from "./types";
const clientEventSearchSchema = z.object({
@@ -155,6 +158,214 @@ export const listTelemetryAlerts = createServerFn()
)
);
+const pbTime = (date: Date): string => date.toISOString().replace("T", " ");
+
+const playerInfoById = async (ids: Set) => {
+ if (ids.size === 0) return new Map();
+ const players = await pbAdmin.listPlayers();
+ return new Map(
+ players
+ .filter((player) => ids.has(player.id))
+ .map((player) => [
+ player.id,
+ { id: player.id, first_name: player.first_name, last_name: player.last_name },
+ ])
+ );
+};
+
+export const listSessions = createServerFn()
+ .validator(
+ z.object({
+ days: z.number().min(1).max(90).optional(),
+ page: z.number().optional(),
+ perPage: z.number().max(100).optional(),
+ })
+ )
+ .middleware([superTokensAdminFunctionMiddleware])
+ .handler(async ({ data }) =>
+ toServerResult>(async () => {
+ const { days = 7, page = 1, perPage = 30 } = data;
+ const from = pbTime(new Date(Date.now() - days * 86400000));
+
+ const sessions = new Map<
+ string,
+ { playerId?: string; started: string; lastSeen: string; pageViews: number; events: number; errors: number; user_agent?: string }
+ >();
+
+ await pbAdmin.pageCollection<{
+ session_id?: string;
+ player?: string;
+ name: string;
+ user_agent?: string;
+ created: string;
+ }>(
+ "client_events",
+ { filter: `created >= '${from}'`, fields: "session_id,player,name,user_agent,created" },
+ (rows) => {
+ for (const row of rows) {
+ if (!row.session_id) continue;
+ const session = sessions.get(row.session_id) ?? {
+ playerId: row.player,
+ started: row.created,
+ lastSeen: row.created,
+ pageViews: 0,
+ events: 0,
+ errors: 0,
+ user_agent: row.user_agent,
+ };
+ session.playerId ||= row.player;
+ if (row.created < session.started) session.started = row.created;
+ if (row.created > session.lastSeen) session.lastSeen = row.created;
+ if (row.name === "page_view") session.pageViews++;
+ else if (!row.name.startsWith("vital.")) session.events++;
+ sessions.set(row.session_id, session);
+ }
+ }
+ );
+
+ await pbAdmin.pageCollection<{ session_id?: string; created: string }>(
+ "client_errors",
+ { filter: `created >= '${from}'`, fields: "session_id,created" },
+ (rows) => {
+ for (const row of rows) {
+ if (!row.session_id) continue;
+ const session = sessions.get(row.session_id);
+ if (session) session.errors++;
+ }
+ }
+ );
+
+ const playerIds = new Set(
+ [...sessions.values()].map((s) => s.playerId).filter((id): id is string => !!id)
+ );
+ const players = await playerInfoById(playerIds);
+
+ const all: SessionSummary[] = [...sessions.entries()]
+ .map(([session_id, s]) => ({
+ session_id,
+ player: s.playerId ? players.get(s.playerId) : undefined,
+ started: s.started,
+ lastSeen: s.lastSeen,
+ pageViews: s.pageViews,
+ events: s.events,
+ errors: s.errors,
+ user_agent: s.user_agent,
+ }))
+ .sort((a, b) => (a.lastSeen < b.lastSeen ? 1 : -1));
+
+ const start = (page - 1) * perPage;
+ return {
+ items: all.slice(start, start + perPage),
+ page,
+ perPage,
+ totalPages: Math.max(1, Math.ceil(all.length / perPage)),
+ totalItems: all.length,
+ };
+ })
+ );
+
+const SESSION_WINDOW_PAD_MS = 30_000;
+const MAX_TIMELINE_ENTRIES = 500;
+
+export const getSessionTimeline = createServerFn()
+ .validator(z.object({ sessionId: z.string().min(8).max(64) }))
+ .middleware([superTokensAdminFunctionMiddleware])
+ .handler(async ({ data }) =>
+ toServerResult(async () => {
+ const events = await pbAdmin.searchClientEvents({
+ sessionId: data.sessionId,
+ perPage: 200,
+ sortBy: "created",
+ });
+ const errors = await pbAdmin.searchClientErrors({
+ sessionId: data.sessionId,
+ perPage: 200,
+ sortBy: "created",
+ });
+
+ const sessionErrors = errors.items;
+ if (events.items.length === 0 && sessionErrors.length === 0) {
+ throw new Error("Session not found");
+ }
+
+ const timestamps = [
+ ...events.items.map((e) => e.created),
+ ...sessionErrors.map((e) => e.created),
+ ].sort();
+ const started = timestamps[0];
+ const lastSeen = timestamps[timestamps.length - 1];
+
+ const playerId = events.items.find((e) => typeof e.player === "string" && e.player)
+ ?.player as string | undefined;
+
+ const entries: TimelineEntry[] = [];
+
+ for (const event of events.items) {
+ if (event.name.startsWith("vital.")) continue;
+ entries.push({
+ ts: event.created,
+ kind: event.name === "page_view" ? "page_view" : "event",
+ label: event.name === "page_view" ? event.path || event.route_id || "?" : event.name,
+ detail: event.name === "page_view" ? undefined : event.path,
+ });
+ }
+
+ for (const error of sessionErrors) {
+ entries.push({
+ ts: error.created,
+ kind: "error",
+ label: error.message,
+ detail: error.path,
+ success: false,
+ });
+ }
+
+ // Server-fn activities have no session id; join by player inside the
+ // session's padded time window.
+ if (playerId) {
+ const windowFrom = pbTime(new Date(Date.parse(started.replace(" ", "T")) - SESSION_WINDOW_PAD_MS));
+ const windowTo = pbTime(new Date(Date.parse(lastSeen.replace(" ", "T")) + SESSION_WINDOW_PAD_MS));
+ const activities = await pbAdmin.searchActivities({
+ player: playerId,
+ from: windowFrom,
+ to: windowTo,
+ perPage: 200,
+ sortBy: "created",
+ });
+ for (const activity of activities.items) {
+ entries.push({
+ ts: activity.created,
+ kind: "server_fn",
+ label: activity.name,
+ detail: activity.error,
+ success: activity.success,
+ duration: activity.duration,
+ });
+ }
+ }
+
+ entries.sort((a, b) => (a.ts < b.ts ? -1 : 1));
+
+ const players = await playerInfoById(new Set(playerId ? [playerId] : []));
+
+ return {
+ session: {
+ session_id: data.sessionId,
+ player: playerId ? players.get(playerId) : undefined,
+ started,
+ lastSeen,
+ pageViews: events.items.filter((e) => e.name === "page_view").length,
+ events: events.items.filter(
+ (e) => e.name !== "page_view" && !e.name.startsWith("vital.")
+ ).length,
+ errors: sessionErrors.length,
+ user_agent: events.items[0]?.user_agent,
+ },
+ entries: entries.slice(0, MAX_TIMELINE_ENTRIES),
+ };
+ })
+ );
+
export const getTelemetryRuntimeStatus = createServerFn()
.middleware([superTokensAdminFunctionMiddleware])
.handler(async () =>
diff --git a/src/features/telemetry/types.ts b/src/features/telemetry/types.ts
index 716f077..0924763 100644
--- a/src/features/telemetry/types.ts
+++ b/src/features/telemetry/types.ts
@@ -22,6 +22,33 @@ export interface ClientErrorGroup {
};
}
+export interface SessionSummary {
+ session_id: string;
+ player?: { id: string; first_name?: string; last_name?: string };
+ started: string;
+ lastSeen: string;
+ pageViews: number;
+ events: number;
+ errors: number;
+ user_agent?: string;
+}
+
+export type TimelineKind = "page_view" | "event" | "error" | "server_fn";
+
+export interface TimelineEntry {
+ ts: string;
+ kind: TimelineKind;
+ label: string;
+ detail?: string;
+ success?: boolean;
+ duration?: number;
+}
+
+export interface SessionTimeline {
+ session: SessionSummary;
+ entries: TimelineEntry[];
+}
+
export interface TelemetryRuntimeStatus {
health: {
status: "ok" | "degraded";
diff --git a/src/lib/pocketbase/services/activities.ts b/src/lib/pocketbase/services/activities.ts
index a6d9bfb..780a344 100644
--- a/src/lib/pocketbase/services/activities.ts
+++ b/src/lib/pocketbase/services/activities.ts
@@ -40,6 +40,8 @@ export interface ActivitySearchParams {
name?: string;
player?: string;
success?: boolean;
+ from?: string;
+ to?: string;
sortBy?: string;
}
@@ -57,6 +59,8 @@ export function createActivitiesService(pb: PocketBase) {
name,
player,
success,
+ from,
+ to,
sortBy = "-created"
} = params;
@@ -74,6 +78,14 @@ export function createActivitiesService(pb: PocketBase) {
filters.push(pbFilter(pb, "success = {:success}", { success }));
}
+ if (from) {
+ filters.push(pbFilter(pb, "created >= {:from}", { from }));
+ }
+
+ if (to) {
+ filters.push(pbFilter(pb, "created < {:to}", { to }));
+ }
+
const filterString = filters.length > 0 ? filters.join(" && ") : "";
const result = await pb.collection("activities").getList(page, perPage, {
diff --git a/src/lib/pocketbase/services/telemetry.ts b/src/lib/pocketbase/services/telemetry.ts
index a29f900..9f001d9 100644
--- a/src/lib/pocketbase/services/telemetry.ts
+++ b/src/lib/pocketbase/services/telemetry.ts
@@ -125,6 +125,7 @@ export interface ClientErrorSearchParams {
groupHash?: string;
resolved?: boolean;
player?: string;
+ sessionId?: string;
from?: string;
to?: string;
sortBy?: string;
@@ -175,12 +176,13 @@ export function createTelemetryService(pb: PocketBase) {
async searchClientErrors(
params: ClientErrorSearchParams = {}
): Promise> {
- const { page = 1, perPage = 100, groupHash, resolved, player, from, to, sortBy = "-created" } = params;
+ const { page = 1, perPage = 100, groupHash, resolved, player, sessionId, from, to, sortBy = "-created" } = params;
const filters: string[] = [];
if (groupHash) filters.push(pbFilter(pb, "group_hash = {:groupHash}", { groupHash }));
if (resolved !== undefined) filters.push(pbFilter(pb, "resolved = {:resolved}", { resolved }));
if (player) filters.push(pbFilter(pb, "player = {:player}", { player }));
+ if (sessionId) filters.push(pbFilter(pb, "session_id = {:sessionId}", { sessionId }));
if (from) filters.push(pbFilter(pb, "created >= {:from}", { from }));
if (to) filters.push(pbFilter(pb, "created < {:to}", { to }));
diff --git a/src/locales/de/messages.po b/src/locales/de/messages.po
index a6e83e1..f6fa61a 100644
--- a/src/locales/de/messages.po
+++ b/src/locales/de/messages.po
@@ -35,6 +35,20 @@ msgstr "(Aktiv)"
msgid "{0, plural, one {# bracket in} other {# brackets in}}"
msgstr "{0, plural, one {# Bracket in} other {# Brackets in}}"
+#. placeholder {0}: data.session.errors
+#. placeholder {0}: session.errors
+#: src/features/telemetry/components/sessions/session-timeline.tsx:48
+#: src/features/telemetry/components/sessions/sessions-list.tsx:52
+msgid "{0, plural, one {# error} other {# errors}}"
+msgstr "{0, plural, one {# Fehler} other {# Fehler}}"
+
+#. placeholder {0}: data.session.pageViews
+#. placeholder {0}: session.pageViews
+#: src/features/telemetry/components/sessions/session-timeline.tsx:44
+#: src/features/telemetry/components/sessions/sessions-list.tsx:47
+msgid "{0, plural, one {# page} other {# pages}}"
+msgstr "{0, plural, one {# Seite} other {# Seiten}}"
+
#. placeholder {0}: enrolledPlayers.length
#. placeholder {0}: players.length
#: src/features/players/components/players-activity-table.tsx:100
@@ -57,11 +71,6 @@ msgstr "{0, plural, one {# Team} other {# Teams}}"
msgid "{0, plural, one {# total activity} other {# total activities}}"
msgstr "{0, plural, one {# Aktivität insgesamt} other {# Aktivitäten insgesamt}}"
-#. placeholder {0}: result.totalItems
-#: src/features/telemetry/components/client-events-table.tsx:86
-msgid "{0, plural, one {# total event} other {# total events}}"
-msgstr "{0, plural, one {# Ereignis insgesamt} other {# Ereignisse insgesamt}}"
-
#. placeholder {0}: selectedConfig.total_group_matches
#: src/features/tournaments/components/setup-group-stage.tsx:249
msgid "{0, plural, one {# total group stage match} other {# total group stage matches}}"
@@ -480,7 +489,8 @@ msgstr "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut."
msgid "Announce matchup"
msgstr "Matchup ansagen"
-#: src/features/telemetry/components/client-events-table.tsx:48
+#: src/features/telemetry/components/sessions/sessions-list.tsx:44
+#: src/features/telemetry/components/sessions/sessions-list.tsx:61
msgid "Anonymous"
msgstr "Anonym"
@@ -846,7 +856,6 @@ msgid "Create Tournament"
msgstr "Turnier erstellen"
#: src/features/activities/components/activities-table.tsx:198
-#: src/features/telemetry/components/client-events-table.tsx:148
msgid "Created"
msgstr "Erstellt"
@@ -1101,18 +1110,6 @@ msgstr "Fehler: {errorMessage}"
msgid "Errors"
msgstr "Fehler"
-#: src/features/telemetry/components/client-events-table.tsx:112
-msgid "Event Details"
-msgstr "Ereignisdetails"
-
-#: src/features/telemetry/components/client-events-table.tsx:69
-msgid "Event name"
-msgstr "Ereignisname"
-
-#: src/app/routes/_authed/admin/telemetry.tsx:54
-msgid "Events"
-msgstr "Ereignisse"
-
#: src/features/tournaments/components/group-stage-view.tsx:368
msgid "Expand standings"
msgstr "Tabelle erweitern"
@@ -1429,6 +1426,10 @@ msgstr "N"
msgid "Language"
msgstr "Sprache"
+#: src/features/telemetry/components/sessions/sessions-list.tsx:74
+msgid "Last 7 days"
+msgstr "Letzte 7 Tage"
+
#: src/features/login/components/name-prompt.tsx:43
#: src/features/players/components/profile/name-form.tsx:68
msgid "Last Name"
@@ -1638,7 +1639,6 @@ msgid "MMR Calculation:"
msgstr "MMR-Berechnung:"
#: src/features/teams/components/team-form/index.tsx:200
-#: src/features/telemetry/components/client-events-table.tsx:118
#: src/features/tournaments/components/tournament-form.tsx:137
msgid "Name"
msgstr "Name"
@@ -1734,10 +1734,6 @@ msgstr "Keine Abzeichen verfügbar"
msgid "No devices found."
msgstr "Keine Geräte gefunden."
-#: src/features/telemetry/components/client-events-table.tsx:93
-msgid "No Events Found"
-msgstr "Keine Ereignisse gefunden"
-
#: src/features/tournaments/components/group-stage-view.tsx:294
msgid "No groups have been created yet"
msgstr "Es wurden noch keine Gruppen erstellt"
@@ -1776,6 +1772,10 @@ msgstr "Keine Tipps"
msgid "No results found"
msgstr "Keine Ergebnisse gefunden"
+#: src/features/telemetry/components/sessions/sessions-list.tsx:38
+msgid "No sessions yet"
+msgstr "Noch keine Sitzungen"
+
#: src/app/routes/api/spotify/resume.ts:37
msgid "No snapshot provided"
msgstr "Kein Snapshot angegeben"
@@ -1949,10 +1949,6 @@ msgstr "Partner-Zuweisungen"
msgid "Partners will be randomly assigned when enrollment closes"
msgstr "Partner werden nach Anmeldeschluss zufällig zugewiesen"
-#: src/features/telemetry/components/client-events-table.tsx:121
-msgid "Path"
-msgstr "Pfad"
-
#: src/features/spotify/components/spotify-controls-bar.tsx:116
msgid "Pause"
msgstr "Pause"
@@ -1995,8 +1991,6 @@ msgstr "Wiedergabesteuerung fehlgeschlagen"
#: src/features/activities/components/activities-table.tsx:98
#: src/features/activities/components/activities-table.tsx:188
#: src/features/admin/components/award-badges.tsx:31
-#: src/features/telemetry/components/client-events-table.tsx:72
-#: src/features/telemetry/components/client-events-table.tsx:119
msgid "Player"
msgstr "Spieler"
@@ -2076,10 +2070,6 @@ msgstr "Profil"
msgid "Progress "
msgstr "Fortschritt "
-#: src/features/telemetry/components/client-events-table.tsx:135
-msgid "Props"
-msgstr "Props"
-
#: src/app/routes/_authed/tournaments/$id.predictions_.$playerId.tsx:106
#: src/features/predictions/components/prediction-leaderboard.tsx:261
msgid "PTS"
@@ -2167,10 +2157,6 @@ msgstr "rofl"
msgid "Round {0}"
msgstr "Runde {0}"
-#: src/features/telemetry/components/client-events-table.tsx:124
-msgid "Route"
-msgstr "Route"
-
#: src/features/tournaments/components/started-tournament/sections/nav-grid.tsx:123
#: src/features/tournaments/components/upcoming-tournament/rules-list-button.tsx:23
msgid "Rules"
@@ -2351,10 +2337,7 @@ msgstr "Serverfunktionen"
msgid "serverFn name"
msgstr "serverFn-Name"
-#: src/features/telemetry/components/client-events-table.tsx:127
-msgid "Session"
-msgstr "Sitzung"
-
+#: src/app/routes/_authed/admin/telemetry.tsx:54
#: src/features/telemetry/components/usage-screen.tsx:177
msgid "Sessions"
msgstr "Sitzungen"
@@ -2809,14 +2792,9 @@ msgid "Usage"
msgstr "Nutzung"
#: src/features/activities/components/activities-table.tsx:202
-#: src/features/telemetry/components/client-events-table.tsx:145
msgid "User Agent"
msgstr "User-Agent"
-#: src/features/telemetry/components/client-events-table.tsx:130
-msgid "Value"
-msgstr "Wert"
-
#: src/features/bracket/components/match-card.tsx:201
#: src/features/bracket/components/match-card.tsx:226
msgid "versus"
diff --git a/src/locales/en/messages.po b/src/locales/en/messages.po
index da0c7e5..817065e 100644
--- a/src/locales/en/messages.po
+++ b/src/locales/en/messages.po
@@ -35,6 +35,20 @@ msgstr "(Active)"
msgid "{0, plural, one {# bracket in} other {# brackets in}}"
msgstr "{0, plural, one {# bracket in} other {# brackets in}}"
+#. placeholder {0}: data.session.errors
+#. placeholder {0}: session.errors
+#: src/features/telemetry/components/sessions/session-timeline.tsx:48
+#: src/features/telemetry/components/sessions/sessions-list.tsx:52
+msgid "{0, plural, one {# error} other {# errors}}"
+msgstr "{0, plural, one {# error} other {# errors}}"
+
+#. placeholder {0}: data.session.pageViews
+#. placeholder {0}: session.pageViews
+#: src/features/telemetry/components/sessions/session-timeline.tsx:44
+#: src/features/telemetry/components/sessions/sessions-list.tsx:47
+msgid "{0, plural, one {# page} other {# pages}}"
+msgstr "{0, plural, one {# page} other {# pages}}"
+
#. placeholder {0}: enrolledPlayers.length
#. placeholder {0}: players.length
#: src/features/players/components/players-activity-table.tsx:100
@@ -57,11 +71,6 @@ msgstr "{0, plural, one {# team} other {# teams}}"
msgid "{0, plural, one {# total activity} other {# total activities}}"
msgstr "{0, plural, one {# total activity} other {# total activities}}"
-#. placeholder {0}: result.totalItems
-#: src/features/telemetry/components/client-events-table.tsx:86
-msgid "{0, plural, one {# total event} other {# total events}}"
-msgstr "{0, plural, one {# total event} other {# total events}}"
-
#. placeholder {0}: selectedConfig.total_group_matches
#: src/features/tournaments/components/setup-group-stage.tsx:249
msgid "{0, plural, one {# total group stage match} other {# total group stage matches}}"
@@ -480,7 +489,8 @@ msgstr "An unexpected error occurred. Please try again."
msgid "Announce matchup"
msgstr "Announce matchup"
-#: src/features/telemetry/components/client-events-table.tsx:48
+#: src/features/telemetry/components/sessions/sessions-list.tsx:44
+#: src/features/telemetry/components/sessions/sessions-list.tsx:61
msgid "Anonymous"
msgstr "Anonymous"
@@ -846,7 +856,6 @@ msgid "Create Tournament"
msgstr "Create Tournament"
#: src/features/activities/components/activities-table.tsx:198
-#: src/features/telemetry/components/client-events-table.tsx:148
msgid "Created"
msgstr "Created"
@@ -1101,18 +1110,6 @@ msgstr "Error: {errorMessage}"
msgid "Errors"
msgstr "Errors"
-#: src/features/telemetry/components/client-events-table.tsx:112
-msgid "Event Details"
-msgstr "Event Details"
-
-#: src/features/telemetry/components/client-events-table.tsx:69
-msgid "Event name"
-msgstr "Event name"
-
-#: src/app/routes/_authed/admin/telemetry.tsx:54
-msgid "Events"
-msgstr "Events"
-
#: src/features/tournaments/components/group-stage-view.tsx:368
msgid "Expand standings"
msgstr "Expand standings"
@@ -1429,6 +1426,10 @@ msgstr "L"
msgid "Language"
msgstr "Language"
+#: src/features/telemetry/components/sessions/sessions-list.tsx:74
+msgid "Last 7 days"
+msgstr "Last 7 days"
+
#: src/features/login/components/name-prompt.tsx:43
#: src/features/players/components/profile/name-form.tsx:68
msgid "Last Name"
@@ -1638,7 +1639,6 @@ msgid "MMR Calculation:"
msgstr "MMR Calculation:"
#: src/features/teams/components/team-form/index.tsx:200
-#: src/features/telemetry/components/client-events-table.tsx:118
#: src/features/tournaments/components/tournament-form.tsx:137
msgid "Name"
msgstr "Name"
@@ -1734,10 +1734,6 @@ msgstr "No Badges Available"
msgid "No devices found."
msgstr "No devices found."
-#: src/features/telemetry/components/client-events-table.tsx:93
-msgid "No Events Found"
-msgstr "No Events Found"
-
#: src/features/tournaments/components/group-stage-view.tsx:294
msgid "No groups have been created yet"
msgstr "No groups have been created yet"
@@ -1776,6 +1772,10 @@ msgstr "No predictions"
msgid "No results found"
msgstr "No results found"
+#: src/features/telemetry/components/sessions/sessions-list.tsx:38
+msgid "No sessions yet"
+msgstr "No sessions yet"
+
#: src/app/routes/api/spotify/resume.ts:37
msgid "No snapshot provided"
msgstr "No snapshot provided"
@@ -1949,10 +1949,6 @@ msgstr "Partner Assignments"
msgid "Partners will be randomly assigned when enrollment closes"
msgstr "Partners will be randomly assigned when enrollment closes"
-#: src/features/telemetry/components/client-events-table.tsx:121
-msgid "Path"
-msgstr "Path"
-
#: src/features/spotify/components/spotify-controls-bar.tsx:116
msgid "Pause"
msgstr "Pause"
@@ -1995,8 +1991,6 @@ msgstr "Playback control failed"
#: src/features/activities/components/activities-table.tsx:98
#: src/features/activities/components/activities-table.tsx:188
#: src/features/admin/components/award-badges.tsx:31
-#: src/features/telemetry/components/client-events-table.tsx:72
-#: src/features/telemetry/components/client-events-table.tsx:119
msgid "Player"
msgstr "Player"
@@ -2076,10 +2070,6 @@ msgstr "Profile"
msgid "Progress "
msgstr "Progress "
-#: src/features/telemetry/components/client-events-table.tsx:135
-msgid "Props"
-msgstr "Props"
-
#: src/app/routes/_authed/tournaments/$id.predictions_.$playerId.tsx:106
#: src/features/predictions/components/prediction-leaderboard.tsx:261
msgid "PTS"
@@ -2167,10 +2157,6 @@ msgstr "rofl"
msgid "Round {0}"
msgstr "Round {0}"
-#: src/features/telemetry/components/client-events-table.tsx:124
-msgid "Route"
-msgstr "Route"
-
#: src/features/tournaments/components/started-tournament/sections/nav-grid.tsx:123
#: src/features/tournaments/components/upcoming-tournament/rules-list-button.tsx:23
msgid "Rules"
@@ -2351,10 +2337,7 @@ msgstr "Server functions"
msgid "serverFn name"
msgstr "serverFn name"
-#: src/features/telemetry/components/client-events-table.tsx:127
-msgid "Session"
-msgstr "Session"
-
+#: src/app/routes/_authed/admin/telemetry.tsx:54
#: src/features/telemetry/components/usage-screen.tsx:177
msgid "Sessions"
msgstr "Sessions"
@@ -2809,14 +2792,9 @@ msgid "Usage"
msgstr "Usage"
#: src/features/activities/components/activities-table.tsx:202
-#: src/features/telemetry/components/client-events-table.tsx:145
msgid "User Agent"
msgstr "User Agent"
-#: src/features/telemetry/components/client-events-table.tsx:130
-msgid "Value"
-msgstr "Value"
-
#: src/features/bracket/components/match-card.tsx:201
#: src/features/bracket/components/match-card.tsx:226
msgid "versus"
diff --git a/src/locales/es/messages.po b/src/locales/es/messages.po
index 9a42bbf..2039edb 100644
--- a/src/locales/es/messages.po
+++ b/src/locales/es/messages.po
@@ -35,6 +35,20 @@ msgstr "(Activo)"
msgid "{0, plural, one {# bracket in} other {# brackets in}}"
msgstr "{0, plural, one {# bracket en} other {# brackets en}}"
+#. placeholder {0}: data.session.errors
+#. placeholder {0}: session.errors
+#: src/features/telemetry/components/sessions/session-timeline.tsx:48
+#: src/features/telemetry/components/sessions/sessions-list.tsx:52
+msgid "{0, plural, one {# error} other {# errors}}"
+msgstr "{0, plural, one {# error} other {# errores}}"
+
+#. placeholder {0}: data.session.pageViews
+#. placeholder {0}: session.pageViews
+#: src/features/telemetry/components/sessions/session-timeline.tsx:44
+#: src/features/telemetry/components/sessions/sessions-list.tsx:47
+msgid "{0, plural, one {# page} other {# pages}}"
+msgstr "{0, plural, one {# página} other {# páginas}}"
+
#. placeholder {0}: enrolledPlayers.length
#. placeholder {0}: players.length
#: src/features/players/components/players-activity-table.tsx:100
@@ -57,11 +71,6 @@ msgstr "{0, plural, one {# equipo} other {# equipos}}"
msgid "{0, plural, one {# total activity} other {# total activities}}"
msgstr "{0, plural, one {# actividad total} other {# actividades totales}}"
-#. placeholder {0}: result.totalItems
-#: src/features/telemetry/components/client-events-table.tsx:86
-msgid "{0, plural, one {# total event} other {# total events}}"
-msgstr "{0, plural, one {# evento total} other {# eventos totales}}"
-
#. placeholder {0}: selectedConfig.total_group_matches
#: src/features/tournaments/components/setup-group-stage.tsx:249
msgid "{0, plural, one {# total group stage match} other {# total group stage matches}}"
@@ -480,7 +489,8 @@ msgstr "Ocurrió un error inesperado. Intenta de nuevo."
msgid "Announce matchup"
msgstr "Anunciar enfrentamiento"
-#: src/features/telemetry/components/client-events-table.tsx:48
+#: src/features/telemetry/components/sessions/sessions-list.tsx:44
+#: src/features/telemetry/components/sessions/sessions-list.tsx:61
msgid "Anonymous"
msgstr "Anónimo"
@@ -846,7 +856,6 @@ msgid "Create Tournament"
msgstr "Crear torneo"
#: src/features/activities/components/activities-table.tsx:198
-#: src/features/telemetry/components/client-events-table.tsx:148
msgid "Created"
msgstr "Creado"
@@ -1101,18 +1110,6 @@ msgstr "Error: {errorMessage}"
msgid "Errors"
msgstr "Errores"
-#: src/features/telemetry/components/client-events-table.tsx:112
-msgid "Event Details"
-msgstr "Detalles del evento"
-
-#: src/features/telemetry/components/client-events-table.tsx:69
-msgid "Event name"
-msgstr "Nombre del evento"
-
-#: src/app/routes/_authed/admin/telemetry.tsx:54
-msgid "Events"
-msgstr "Eventos"
-
#: src/features/tournaments/components/group-stage-view.tsx:368
msgid "Expand standings"
msgstr "Expandir tabla de posiciones"
@@ -1429,6 +1426,10 @@ msgstr "D"
msgid "Language"
msgstr "Idioma"
+#: src/features/telemetry/components/sessions/sessions-list.tsx:74
+msgid "Last 7 days"
+msgstr "Últimos 7 días"
+
#: src/features/login/components/name-prompt.tsx:43
#: src/features/players/components/profile/name-form.tsx:68
msgid "Last Name"
@@ -1638,7 +1639,6 @@ msgid "MMR Calculation:"
msgstr "Cálculo de MMR:"
#: src/features/teams/components/team-form/index.tsx:200
-#: src/features/telemetry/components/client-events-table.tsx:118
#: src/features/tournaments/components/tournament-form.tsx:137
msgid "Name"
msgstr "Nombre"
@@ -1734,10 +1734,6 @@ msgstr "No hay insignias disponibles"
msgid "No devices found."
msgstr "No se encontraron dispositivos."
-#: src/features/telemetry/components/client-events-table.tsx:93
-msgid "No Events Found"
-msgstr "No se encontraron eventos"
-
#: src/features/tournaments/components/group-stage-view.tsx:294
msgid "No groups have been created yet"
msgstr "Aún no se han creado grupos"
@@ -1776,6 +1772,10 @@ msgstr "Sin predicciones"
msgid "No results found"
msgstr "No se encontraron resultados"
+#: src/features/telemetry/components/sessions/sessions-list.tsx:38
+msgid "No sessions yet"
+msgstr "Aún no hay sesiones"
+
#: src/app/routes/api/spotify/resume.ts:37
msgid "No snapshot provided"
msgstr "No se proporcionó ninguna instantánea"
@@ -1949,10 +1949,6 @@ msgstr "Asignación de compañeros"
msgid "Partners will be randomly assigned when enrollment closes"
msgstr "Los compañeros se asignarán al azar cuando cierre la inscripción"
-#: src/features/telemetry/components/client-events-table.tsx:121
-msgid "Path"
-msgstr "Ruta"
-
#: src/features/spotify/components/spotify-controls-bar.tsx:116
msgid "Pause"
msgstr "Pausar"
@@ -1995,8 +1991,6 @@ msgstr "Falló el control de reproducción"
#: src/features/activities/components/activities-table.tsx:98
#: src/features/activities/components/activities-table.tsx:188
#: src/features/admin/components/award-badges.tsx:31
-#: src/features/telemetry/components/client-events-table.tsx:72
-#: src/features/telemetry/components/client-events-table.tsx:119
msgid "Player"
msgstr "Jugador"
@@ -2076,10 +2070,6 @@ msgstr "Perfil"
msgid "Progress "
msgstr "Progreso "
-#: src/features/telemetry/components/client-events-table.tsx:135
-msgid "Props"
-msgstr "Props"
-
#: src/app/routes/_authed/tournaments/$id.predictions_.$playerId.tsx:106
#: src/features/predictions/components/prediction-leaderboard.tsx:261
msgid "PTS"
@@ -2167,10 +2157,6 @@ msgstr "carcajada"
msgid "Round {0}"
msgstr "Ronda {0}"
-#: src/features/telemetry/components/client-events-table.tsx:124
-msgid "Route"
-msgstr "Ruta"
-
#: src/features/tournaments/components/started-tournament/sections/nav-grid.tsx:123
#: src/features/tournaments/components/upcoming-tournament/rules-list-button.tsx:23
msgid "Rules"
@@ -2351,10 +2337,7 @@ msgstr "Funciones del servidor"
msgid "serverFn name"
msgstr "nombre de serverFn"
-#: src/features/telemetry/components/client-events-table.tsx:127
-msgid "Session"
-msgstr "Sesión"
-
+#: src/app/routes/_authed/admin/telemetry.tsx:54
#: src/features/telemetry/components/usage-screen.tsx:177
msgid "Sessions"
msgstr "Sesiones"
@@ -2809,14 +2792,9 @@ msgid "Usage"
msgstr "Uso"
#: src/features/activities/components/activities-table.tsx:202
-#: src/features/telemetry/components/client-events-table.tsx:145
msgid "User Agent"
msgstr "User Agent"
-#: src/features/telemetry/components/client-events-table.tsx:130
-msgid "Value"
-msgstr "Valor"
-
#: src/features/bracket/components/match-card.tsx:201
#: src/features/bracket/components/match-card.tsx:226
msgid "versus"
diff --git a/src/locales/ja/messages.po b/src/locales/ja/messages.po
index fc8e375..fb0e762 100644
--- a/src/locales/ja/messages.po
+++ b/src/locales/ja/messages.po
@@ -35,6 +35,20 @@ msgstr "(アクティブ)"
msgid "{0, plural, one {# bracket in} other {# brackets in}}"
msgstr "{0, plural, one {#ブラケット参加} other {#ブラケット参加}}"
+#. placeholder {0}: data.session.errors
+#. placeholder {0}: session.errors
+#: src/features/telemetry/components/sessions/session-timeline.tsx:48
+#: src/features/telemetry/components/sessions/sessions-list.tsx:52
+msgid "{0, plural, one {# error} other {# errors}}"
+msgstr "{0, plural, one {# 件のエラー} other {# 件のエラー}}"
+
+#. placeholder {0}: data.session.pageViews
+#. placeholder {0}: session.pageViews
+#: src/features/telemetry/components/sessions/session-timeline.tsx:44
+#: src/features/telemetry/components/sessions/sessions-list.tsx:47
+msgid "{0, plural, one {# page} other {# pages}}"
+msgstr "{0, plural, one {# ページ} other {# ページ}}"
+
#. placeholder {0}: enrolledPlayers.length
#. placeholder {0}: players.length
#: src/features/players/components/players-activity-table.tsx:100
@@ -57,11 +71,6 @@ msgstr "{0, plural, one {#チーム} other {#チーム}}"
msgid "{0, plural, one {# total activity} other {# total activities}}"
msgstr "{0, plural, one {アクティビティ合計#件} other {アクティビティ合計#件}}"
-#. placeholder {0}: result.totalItems
-#: src/features/telemetry/components/client-events-table.tsx:86
-msgid "{0, plural, one {# total event} other {# total events}}"
-msgstr "{0, plural, one {イベント合計#件} other {イベント合計#件}}"
-
#. placeholder {0}: selectedConfig.total_group_matches
#: src/features/tournaments/components/setup-group-stage.tsx:249
msgid "{0, plural, one {# total group stage match} other {# total group stage matches}}"
@@ -480,7 +489,8 @@ msgstr "予期しないエラーが発生しました。もう一度お試しく
msgid "Announce matchup"
msgstr "対戦カードを発表"
-#: src/features/telemetry/components/client-events-table.tsx:48
+#: src/features/telemetry/components/sessions/sessions-list.tsx:44
+#: src/features/telemetry/components/sessions/sessions-list.tsx:61
msgid "Anonymous"
msgstr "匿名"
@@ -846,7 +856,6 @@ msgid "Create Tournament"
msgstr "トーナメントを作成"
#: src/features/activities/components/activities-table.tsx:198
-#: src/features/telemetry/components/client-events-table.tsx:148
msgid "Created"
msgstr "作成日"
@@ -1101,18 +1110,6 @@ msgstr "エラー: {errorMessage}"
msgid "Errors"
msgstr "エラー"
-#: src/features/telemetry/components/client-events-table.tsx:112
-msgid "Event Details"
-msgstr "イベント詳細"
-
-#: src/features/telemetry/components/client-events-table.tsx:69
-msgid "Event name"
-msgstr "イベント名"
-
-#: src/app/routes/_authed/admin/telemetry.tsx:54
-msgid "Events"
-msgstr "イベント"
-
#: src/features/tournaments/components/group-stage-view.tsx:368
msgid "Expand standings"
msgstr "順位表を展開"
@@ -1429,6 +1426,10 @@ msgstr "L"
msgid "Language"
msgstr "言語"
+#: src/features/telemetry/components/sessions/sessions-list.tsx:74
+msgid "Last 7 days"
+msgstr "過去7日間"
+
#: src/features/login/components/name-prompt.tsx:43
#: src/features/players/components/profile/name-form.tsx:68
msgid "Last Name"
@@ -1638,7 +1639,6 @@ msgid "MMR Calculation:"
msgstr "MMR計算:"
#: src/features/teams/components/team-form/index.tsx:200
-#: src/features/telemetry/components/client-events-table.tsx:118
#: src/features/tournaments/components/tournament-form.tsx:137
msgid "Name"
msgstr "名前"
@@ -1734,10 +1734,6 @@ msgstr "利用可能なバッジがありません"
msgid "No devices found."
msgstr "デバイスが見つかりません。"
-#: src/features/telemetry/components/client-events-table.tsx:93
-msgid "No Events Found"
-msgstr "イベントが見つかりません"
-
#: src/features/tournaments/components/group-stage-view.tsx:294
msgid "No groups have been created yet"
msgstr "まだグループが作成されていません"
@@ -1776,6 +1772,10 @@ msgstr "予想はありません"
msgid "No results found"
msgstr "結果が見つかりません"
+#: src/features/telemetry/components/sessions/sessions-list.tsx:38
+msgid "No sessions yet"
+msgstr "まだセッションがありません"
+
#: src/app/routes/api/spotify/resume.ts:37
msgid "No snapshot provided"
msgstr "スナップショットが提供されていません"
@@ -1949,10 +1949,6 @@ msgstr "パートナー割り当て"
msgid "Partners will be randomly assigned when enrollment closes"
msgstr "登録が締め切られると、パートナーはランダムに割り当てられます"
-#: src/features/telemetry/components/client-events-table.tsx:121
-msgid "Path"
-msgstr "パス"
-
#: src/features/spotify/components/spotify-controls-bar.tsx:116
msgid "Pause"
msgstr "一時停止"
@@ -1995,8 +1991,6 @@ msgstr "再生コントロールに失敗しました"
#: src/features/activities/components/activities-table.tsx:98
#: src/features/activities/components/activities-table.tsx:188
#: src/features/admin/components/award-badges.tsx:31
-#: src/features/telemetry/components/client-events-table.tsx:72
-#: src/features/telemetry/components/client-events-table.tsx:119
msgid "Player"
msgstr "プレイヤー"
@@ -2076,10 +2070,6 @@ msgstr "プロフィール"
msgid "Progress "
msgstr "進行状況 "
-#: src/features/telemetry/components/client-events-table.tsx:135
-msgid "Props"
-msgstr "プロパティ"
-
#: src/app/routes/_authed/tournaments/$id.predictions_.$playerId.tsx:106
#: src/features/predictions/components/prediction-leaderboard.tsx:261
msgid "PTS"
@@ -2167,10 +2157,6 @@ msgstr "大笑い"
msgid "Round {0}"
msgstr "ラウンド{0}"
-#: src/features/telemetry/components/client-events-table.tsx:124
-msgid "Route"
-msgstr "ルート"
-
#: src/features/tournaments/components/started-tournament/sections/nav-grid.tsx:123
#: src/features/tournaments/components/upcoming-tournament/rules-list-button.tsx:23
msgid "Rules"
@@ -2351,10 +2337,7 @@ msgstr "サーバー関数"
msgid "serverFn name"
msgstr "serverFn名"
-#: src/features/telemetry/components/client-events-table.tsx:127
-msgid "Session"
-msgstr "セッション"
-
+#: src/app/routes/_authed/admin/telemetry.tsx:54
#: src/features/telemetry/components/usage-screen.tsx:177
msgid "Sessions"
msgstr "セッション数"
@@ -2809,14 +2792,9 @@ msgid "Usage"
msgstr "利用状況"
#: src/features/activities/components/activities-table.tsx:202
-#: src/features/telemetry/components/client-events-table.tsx:145
msgid "User Agent"
msgstr "ユーザーエージェント"
-#: src/features/telemetry/components/client-events-table.tsx:130
-msgid "Value"
-msgstr "値"
-
#: src/features/bracket/components/match-card.tsx:201
#: src/features/bracket/components/match-card.tsx:226
msgid "versus"