feat(telemetry): matchup and direction context in timelines; live error groups
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 9s
CI/CD Pipeline / i18n Catalog Check (push) Successful in 47s
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 2m18s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 8m29s

- h2h_viewed carries who-v-who, stats_sorted carries asc/desc via a
  timeline-only context prop
- error browser aggregates raw rows so new groups appear immediately
- catalogs re-extracted for shifted line references
This commit is contained in:
2026-08-27 10:47:57 -07:00
parent 9b51d61519
commit 7bda138d69
11 changed files with 280 additions and 245 deletions
@@ -176,7 +176,12 @@ const MatchDock = ({ match, onClose }: MatchDockProps) => {
variant="subtle"
size="sm"
onClick={() => {
track("h2h_viewed", { source: "match_dock" });
track("h2h_viewed", {
source: "match_dock",
context: [match?.home?.name, match?.away?.name]
.filter(Boolean)
.join(" v "),
});
h2hSheet.open();
}}
aria-label={t`View head-to-head`}
@@ -42,7 +42,10 @@ const MatchCard = ({ match, hideH2H = false }: MatchCardProps) => {
const handleH2HClick = (e: React.MouseEvent) => {
e.stopPropagation();
track("h2h_viewed", { source: "match_card" });
track("h2h_viewed", {
source: "match_card",
context: [match.home?.name, match.away?.name].filter(Boolean).join(" v "),
});
h2hSheet.open();
};
@@ -53,7 +53,12 @@ const LeagueHeadToHead = () => {
setPlayer1Id(playerId);
} else {
setPlayer2Id(playerId);
track("h2h_viewed", { source: "stats" });
const opponent =
allPlayerStats?.find((p) => p.player_id === playerId)?.player_name || "";
track("h2h_viewed", {
source: "stats",
context: [player1Name, opponent].filter(Boolean).join(" v "),
});
h2hSheet.open();
}
};
@@ -293,11 +293,10 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
}, [navigate]);
const handleSort = (key: SortKey) => {
track("stats_sorted", { value: key });
setSortConfig((prev) => ({
key,
direction: prev.key === key && prev.direction === "desc" ? "asc" : "desc",
}));
const direction =
sortConfig.key === key && sortConfig.direction === "desc" ? "asc" : "desc";
track("stats_sorted", { value: key, context: direction });
setSortConfig({ key, direction });
};
const getSortIcon = (key: SortKey) => {
@@ -55,7 +55,10 @@ const Header = ({ player }: HeaderProps) => {
size="sm"
radius="xl"
onClick={() => {
track("h2h_viewed", { source: "profile" });
track("h2h_viewed", {
source: "profile",
context: [player.first_name, player.last_name].filter(Boolean).join(" "),
});
h2hSheet.open();
}}
w={40}
@@ -218,7 +218,10 @@ export const PredictionEditor: React.FC<PredictionEditorProps> = ({
radius="md"
onClick={() => {
if (activeResolved?.home && activeResolved?.away) {
track("h2h_viewed", { source: "predictions" });
track("h2h_viewed", {
source: "predictions",
context: `${activeResolved.home.name} v ${activeResolved.away.name}`,
});
}
matchupSheet.open();
}}
+56 -39
View File
@@ -73,6 +73,8 @@ const errorGroupsSchema = z.object({
limit: z.number().max(100).optional(),
});
// Aggregates raw rows, not rollups: a brand-new error must appear immediately,
// not on the next rollup tick.
export const getClientErrorGroups = createServerFn()
.validator(errorGroupsSchema)
.middleware([superTokensAdminFunctionMiddleware])
@@ -80,46 +82,60 @@ export const getClientErrorGroups = createServerFn()
toServerResult<ClientErrorGroup[]>(async () => {
const { from, to, includeResolved = false, limit = 50 } = data;
const rollups = await pbAdmin.getRollupRange({
metrics: ["client_error.count"],
from,
to,
});
const toExclusive = new Date(`${to}T00:00:00.000Z`);
toExclusive.setUTCDate(toExclusive.getUTCDate() + 1);
const filter = `created >= '${from} 00:00:00.000Z' && created < '${toExclusive
.toISOString()
.replace("T", " ")}'`;
const countByGroup = new Map<string, number>();
for (const rollup of rollups) {
if (!rollup.dim) continue;
countByGroup.set(rollup.dim, (countByGroup.get(rollup.dim) ?? 0) + rollup.value);
}
const byGroup = new Map<string, ClientErrorGroup>();
await pbAdmin.pageCollection<{
group_hash: string;
message: string;
stack?: string;
source?: string;
path?: string;
resolved?: boolean;
created: string;
}>(
"client_errors",
{ filter, fields: "group_hash,message,stack,source,path,resolved,created" },
(rows) => {
for (const row of rows) {
const existing = byGroup.get(row.group_hash);
if (existing) {
existing.count++;
// Paged in created-asc order, so each row is the newest so far.
existing.lastSeen = row.created;
existing.resolved = !!row.resolved;
existing.sample = {
message: row.message,
stack: row.stack,
path: row.path,
source: row.source,
};
} else {
byGroup.set(row.group_hash, {
group_hash: row.group_hash,
count: 1,
lastSeen: row.created,
resolved: !!row.resolved,
sample: {
message: row.message,
stack: row.stack,
path: row.path,
source: row.source,
},
});
}
}
}
);
const topGroups = [...countByGroup.entries()]
.sort((a, b) => b[1] - a[1])
return [...byGroup.values()]
.filter((group) => includeResolved || !group.resolved)
.sort((a, b) => b.count - a.count)
.slice(0, limit);
const groups: ClientErrorGroup[] = [];
for (const [groupHash, count] of topGroups) {
const latest = await pbAdmin.searchClientErrors({ groupHash, perPage: 1 });
const sample = latest.items[0];
if (!sample) continue;
const resolved = !!sample.resolved;
if (resolved && !includeResolved) continue;
groups.push({
group_hash: groupHash,
count,
lastSeen: sample.created,
resolved,
sample: {
message: sample.message,
stack: sample.stack,
path: sample.path,
source: sample.source,
},
});
}
return groups;
})
);
@@ -303,16 +319,17 @@ export const getSessionTimeline = createServerFn()
for (const event of events.items) {
if (event.name.startsWith("vital.")) continue;
const props = event.props as
| { emoji?: string; source?: string; tournament?: string; value?: string }
| { emoji?: string; source?: string; tournament?: string; value?: string; context?: string }
| null
| undefined;
const breakdown =
props?.emoji ?? props?.value ?? props?.source ?? props?.tournament;
const detail = [breakdown, props?.context].filter(Boolean).join(" · ");
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 : (breakdown ?? event.path),
detail: event.name === "page_view" ? undefined : (detail || event.path),
});
}