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"