From 2aa4fece7d2c61142a43dcaaa1fa5b98b9c5fc19 Mon Sep 17 00:00:00 2001 From: OG T Date: Sun, 7 Jun 2026 16:00:24 +0800 Subject: [PATCH] feat: enrich traffic API with external actor ranking and focused event streams --- apps/web/src/app/api/traffic/route.ts | 63 +++++++++++++++++++++------ 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/apps/web/src/app/api/traffic/route.ts b/apps/web/src/app/api/traffic/route.ts index d0582d7..2582e20 100644 --- a/apps/web/src/app/api/traffic/route.ts +++ b/apps/web/src/app/api/traffic/route.ts @@ -5,6 +5,13 @@ export const dynamic = "force-dynamic"; const MONITOR_TOKEN = process.env.TRAFFIC_MONITOR_TOKEN; +function asRecordJson(value: unknown): Record | undefined { + if (typeof value === "object" && value !== null && !Array.isArray(value)) { + return value as Record; + } + return undefined; +} + export async function GET(request: NextRequest) { if (MONITOR_TOKEN) { const token = request.headers.get("x-traffic-token"); @@ -13,9 +20,11 @@ export async function GET(request: NextRequest) { } } - const since = new Date(Date.now() - 24 * 60 * 60 * 1000); + const query = request.nextUrl.searchParams; + const minutes = Math.max(parseInt(query.get("minutes") || "1440", 10), 5); + const since = new Date(Date.now() - minutes * 60 * 1000); - const [summaryRows, actorSummaryRows, totalRows, latestEvents] = await Promise.all([ + const [summaryRows, actorSummaryRows, externalActorRows, totalRows, latestEvents] = await Promise.all([ prisma.auditEvent.groupBy({ by: ["action"], where: { @@ -30,6 +39,16 @@ export async function GET(request: NextRequest) { }, _count: { _all: true }, }), + prisma.auditEvent.groupBy({ + by: ["actorId"], + where: { + createdAt: { gte: since }, + action: { + startsWith: "EXTERNAL_", + }, + }, + _count: { _all: true }, + }), prisma.auditEvent.count({ where: { createdAt: { gte: since } }, }), @@ -40,7 +59,7 @@ export async function GET(request: NextRequest) { orderBy: { createdAt: "desc", }, - take: 80, + take: 120, select: { id: true, action: true, @@ -63,6 +82,14 @@ export async function GET(request: NextRequest) { actorSummaryRows.map((row) => [row.actorType, row._count._all]) ); + const externalActorSummary = externalActorRows + .map((row) => ({ + actorId: row.actorId || "unknown", + events: row._count._all, + })) + .sort((a, b) => b.events - a.events) + .slice(0, 20); + const channelSummary = Object.entries(actionSummary).reduce( (acc, [action, count]) => { if (action.startsWith("EXTERNAL_")) { @@ -75,16 +102,16 @@ export async function GET(request: NextRequest) { { external: 0, internal: 0 } as Record ); - const externalEventTypes = Object.entries(actionSummary).filter(([action]) => action.startsWith("EXTERNAL_")).map(([action, count]) => ({ action, count })); - const internalEventTypes = Object.entries(actionSummary).filter(([action]) => !action.startsWith("EXTERNAL_")).map(([action, count]) => ({ action, count })); + const externalEventTypes = Object.entries(actionSummary) + .filter(([action]) => action.startsWith("EXTERNAL_")) + .map(([action, count]) => ({ action, count })); + + const internalEventTypes = Object.entries(actionSummary) + .filter(([action]) => !action.startsWith("EXTERNAL_")) + .map(([action, count]) => ({ action, count })); const recentEvents = latestEvents.map((event) => { - // metadata is JSONB in Prisma; read only known optional keys for monitoring views. - const metadata = - event.metadata && typeof event.metadata === "object" && !Array.isArray(event.metadata) - ? (event.metadata as Record) - : undefined; - + const metadata = asRecordJson(event.metadata); return { id: event.id, action: event.action, @@ -99,15 +126,25 @@ export async function GET(request: NextRequest) { }; }); + const recentExternalEvents = recentEvents.filter((event) => + event.action.startsWith("EXTERNAL_") + ); + + const recentInternalEvents = recentEvents.filter( + (event) => !event.action.startsWith("EXTERNAL_") + ); + return NextResponse.json({ - period_hours: 24, + period_minutes: minutes, total_events: totalRows, action_summary: actionSummary, channel_summary: channelSummary, actor_summary: actorSummary, + external_actor_summary: externalActorSummary, external_event_types: externalEventTypes, internal_event_types: internalEventTypes, - recent_events: recentEvents, + recent_external_events: recentExternalEvents, + recent_internal_events: recentInternalEvents, updated_at: new Date().toISOString(), }); }