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
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:
@@ -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();
|
||||
}}
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user