From 0b07ae4344f9697ff42db31f7c7276b36a1dcd0f Mon Sep 17 00:00:00 2001 From: OG T Date: Sun, 7 Jun 2026 19:31:27 +0800 Subject: [PATCH] feat(traffic): expose external actor source/response details in monitor --- apps/web/src/app/api/traffic/route.ts | 42 +++++++++ apps/web/src/app/traffic/page.tsx | 117 ++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/apps/web/src/app/api/traffic/route.ts b/apps/web/src/app/api/traffic/route.ts index 531963e..bbce9c2 100644 --- a/apps/web/src/app/api/traffic/route.ts +++ b/apps/web/src/app/api/traffic/route.ts @@ -422,6 +422,18 @@ export async function GET(request: NextRequest) { error_message: string; created_at_ms: number; }> = []; + const externalActorActivities: Map = new Map(); recentEvents.forEach((event) => { const actorId = event.actorId || "agent:unknown"; @@ -442,12 +454,32 @@ export async function GET(request: NextRequest) { const errorName = typeof metadata?.error_name === "string" ? metadata.error_name : ""; const errorMessage = typeof metadata?.error_message === "string" ? metadata.error_message : ""; const taskId = typeof metadata?.task_id === "string" ? metadata.task_id : (event.entityId || "-"); + const responseSummary = + typeof metadata?.response_summary === "string" ? metadata.response_summary : "unknown"; updateActorBucket(externalSourceSurfaceMap, normalizedSurface, actorId, eventAt, { surface: normalizedSurface }); updateActorBucket(externalIpSurfaceMap, normalizedIp, actorId, eventAt, { sourceIp: normalizedIp }); updateActorBucket(externalUserAgentMap, normalizedUa, actorId, eventAt, { userAgent: normalizedUa }); addCountedBucket(responseStatusSummary, String(responseStatus ?? "n/a"), actorId, eventAt); + const existingActorActivity = externalActorActivities.get(actorId); + if (!existingActorActivity) { + externalActorActivities.set(actorId, { + actor_id: actorId, + events: 1, + latest_action: event.action, + latest_surface: normalizedSurface, + latest_source_ip: normalizedIp, + latest_user_agent: normalizedUa, + latest_response_status: responseStatus, + latest_response_summary: responseSummary, + latest_reason: event.reason || "unknown", + latest_created_at_ms: eventAt, + }); + } else { + existingActorActivity.events += 1; + } + if ( event.action.includes("ERROR") || event.action.includes("FORBIDDEN") || @@ -521,6 +553,15 @@ export async function GET(request: NextRequest) { .sort((left, right) => right.created_at_ms - left.created_at_ms) .slice(0, 30); + const externalActorActivityRows = Array.from(externalActorActivities.values()) + .sort((left, right) => { + if (right.events !== left.events) { + return right.events - left.events; + } + return right.latest_created_at_ms - left.latest_created_at_ms; + }) + .slice(0, 40); + return NextResponse.json({ period_minutes: minutes, total_events: totalRows, @@ -536,6 +577,7 @@ export async function GET(request: NextRequest) { external_source_ip_summary: externalSourceIpSummary, external_user_agent_summary: externalUserAgentSummary, external_response_status_summary: externalResponseStatusSummary, + external_actor_activities: externalActorActivityRows, external_error_rows: externalErrorRowsSorted, recent_external_events: recentExternalEvents, recent_internal_events: recentInternalEvents, diff --git a/apps/web/src/app/traffic/page.tsx b/apps/web/src/app/traffic/page.tsx index 9730b40..1d4f92e 100644 --- a/apps/web/src/app/traffic/page.tsx +++ b/apps/web/src/app/traffic/page.tsx @@ -91,6 +91,19 @@ function explainAction(action: string) { return EVENT_LABELS[action] || action; } +type ExternalActorActivity = { + actorId: string; + events: number; + latestAction: string; + latestSurface: string; + latestSourceIp: string; + latestUserAgent: string; + latestResponseStatus: number | null; + latestResponseSummary: string; + latestReason: string; + latestCreatedAt: number; +}; + async function getTrafficSummary(minutes: number) { const since = new Date(Date.now() - minutes * 60 * 1000); @@ -213,6 +226,8 @@ async function getTrafficSummary(minutes: number) { ...event, surface: metadata?.surface, level: metadata?.level, + response_status: typeof metadata?.response_status === "number" ? metadata.response_status : null, + response_summary: typeof metadata?.response_summary === "string" ? metadata.response_summary : "unknown", metadata, }; }); @@ -252,6 +267,60 @@ async function getTrafficSummary(minutes: number) { .filter((event) => event.action.includes("ERROR")) .map((event) => event.action); + const externalActorActivityMap = new Map(); + for (const event of recentEvents) { + if (!event.action.startsWith("EXTERNAL_")) { + continue; + } + + const actorId = event.actorId || "agent:unknown"; + const isInternal = isInternalActor({ + actorType: event.actorType, + actorId: event.actorId, + }); + if (isInternal || event.actorType !== "AGENT") { + continue; + } + + const existing = externalActorActivityMap.get(actorId); + if (!existing) { + const metadata = asRecordJson(event.metadata); + const normalizedIp = (() => { + const value = typeof metadata?.source_ip === "string" ? metadata.source_ip : "unknown"; + return value.trim().length > 0 ? value.trim() : "unknown"; + })(); + const normalizedUa = (() => { + const value = typeof metadata?.user_agent === "string" ? metadata.user_agent : "unknown"; + return value.trim().length > 0 ? value.trim() : "unknown"; + })(); + + externalActorActivityMap.set(actorId, { + actorId, + events: 1, + latestAction: event.action, + latestSurface: String(event.surface || "unknown"), + latestSourceIp: normalizedIp, + latestUserAgent: normalizedUa, + latestResponseStatus: + typeof event.response_status === "number" ? event.response_status : null, + latestResponseSummary: typeof event.response_summary === "string" + ? event.response_summary + : "unknown", + latestReason: typeof event.reason === "string" ? event.reason : "unknown", + latestCreatedAt: event.createdAt.getTime(), + }); + } else { + existing.events += 1; + } + } + + const externalActorActivities = Array.from(externalActorActivityMap.values()) + .sort((left, right) => { + if (right.events !== left.events) return right.events - left.events; + return right.latestCreatedAt - left.latestCreatedAt; + }) + .slice(0, 40); + return { periodMinutes: minutes, totalEvents: totalRows, @@ -271,6 +340,7 @@ async function getTrafficSummary(minutes: number) { recentInternalEvents: recentEvents.filter((event) => !event.action.startsWith("EXTERNAL_")), conversionSummary, conversionRates, + externalActorActivities, externalErrors, }; } @@ -487,6 +557,50 @@ export default async function TrafficDashboard({ +
+

外部 Actor 行為追蹤(可追溯)

+
+ + + + + + + + + + + + + {summary.externalActorActivities.length === 0 ? ( + + + + ) : ( + summary.externalActorActivities.map((actor) => ( + + + + + + + + + )) + )} + +
Actor事件最新行為來源 IPUser-Agent最新回應
+ 尚無可追蹤的外部 AGENT 行為,可能目前仍未有 AGENT 類型入口流量。 +
{actor.actorId}{actor.events} +
{actor.latestAction}
+
{actor.latestSurface}
+
{actor.latestSourceIp}{actor.latestUserAgent} +
{actor.latestResponseStatus ?? "n/a"}
+
{actor.latestResponseSummary}
+
+
+
+

外部事件說明(可讀)

@@ -542,6 +656,9 @@ export default async function TrafficDashboard({
actor={event.actorType}:{event.actorId || "unknown"} | entity={event.entityType}/{event.entityId} | surface={String(event.surface || "-")} | {ts}
+
+ response={event.response_status ?? "n/a"} / summary={event.response_summary} +
{event.reason ?
{event.reason}
: null} {event.metadata ? (