feat(dashboard): refine activity telemetry and maps
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { fillDailySeries } from "./admin-metrics";
|
||||
import { fillDailySeries, mergeRiskActivity } from "./admin-metrics";
|
||||
|
||||
describe("admin dashboard metrics", () => {
|
||||
it("fills missing UTC registration days with zero", () => {
|
||||
@@ -13,4 +13,20 @@ describe("admin dashboard metrics", () => {
|
||||
{ day: "2026-08-01", count: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("merges complete per-user VPN summaries with each user's latest observation", () => {
|
||||
const latest = [
|
||||
{ userId: "user-2", classification: "tor", observedAt: new Date("2026-08-01T11:00:00Z") },
|
||||
{ userId: "user-1", classification: "proxy", observedAt: new Date("2026-08-01T12:00:00Z") },
|
||||
];
|
||||
const summaries = [
|
||||
{ userId: "user-1", count: 2000, classifications: ["proxy", "vpn"], sources: ["game", "web"] },
|
||||
{ userId: "user-2", count: 1, classifications: ["tor"], sources: ["web"] },
|
||||
];
|
||||
|
||||
expect(mergeRiskActivity(latest, summaries)).toEqual([
|
||||
expect.objectContaining({ userId: "user-1", count: 2000, classification: "proxy", classifications: ["proxy", "vpn"], sources: ["game", "web"] }),
|
||||
expect.objectContaining({ userId: "user-2", count: 1, classification: "tor" }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,19 @@ export interface DailyCount {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export function mergeRiskActivity<
|
||||
T extends { userId: string; observedAt: Date },
|
||||
S extends { userId: string | null },
|
||||
>(latestRows: T[], summaryRows: S[]) {
|
||||
const summaries = new Map(summaryRows.flatMap((summary) => summary.userId ? [[summary.userId, summary] as const] : []));
|
||||
return latestRows
|
||||
.flatMap((activity) => {
|
||||
const summary = summaries.get(activity.userId);
|
||||
return summary ? [{ ...activity, ...summary }] : [];
|
||||
})
|
||||
.sort((left, right) => right.observedAt.getTime() - left.observedAt.getTime());
|
||||
}
|
||||
|
||||
export function fillDailySeries(rows: DailyCount[], end: Date, days: number) {
|
||||
const counts = new Map(rows.map((row) => [row.day, Number(row.count)]));
|
||||
const endDay = new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate()));
|
||||
|
||||
@@ -10,6 +10,7 @@ describe("event filters", () => {
|
||||
it("classifies events into operator-friendly views", () => {
|
||||
expect(eventCategory("games.minecraft.account-manager.group.deleted")).toBe("groups");
|
||||
expect(eventCategory("games.minecraft.account-manager.game.login.denied")).toBe("admission");
|
||||
expect(eventCategory("games.minecraft.account-manager.game.player.connected")).toBe("admission");
|
||||
expect(eventCategory("games.minecraft.account-manager.network.vpn-blocked")).toBe("security");
|
||||
expect(eventCategory("games.minecraft.account-manager.auth.magic-link.consumed")).toBe("security");
|
||||
expect(eventCategory("games.minecraft.account-manager.discord.nickname.updated")).toBe("identity");
|
||||
|
||||
@@ -4,7 +4,7 @@ export type EventCategory = (typeof eventCategoryValues)[number];
|
||||
export function eventCategory(type: string): Exclude<EventCategory, "all"> {
|
||||
if (type.includes(".group.")) return "groups";
|
||||
if (type.includes(".network.") || type.includes(".auth.") || type.includes("authentication") || type.includes("replay")) return "security";
|
||||
if (type.includes(".game.login.")) return "admission";
|
||||
if (type.includes(".game.")) return "admission";
|
||||
if (type.includes(".discord.") || type.includes(".user.") || type.includes("minecraft-account")) return "identity";
|
||||
return "operations";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user