feat(traffic): expose external actor source/response details in monitor
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s
This commit is contained in:
@@ -422,6 +422,18 @@ export async function GET(request: NextRequest) {
|
||||
error_message: string;
|
||||
created_at_ms: number;
|
||||
}> = [];
|
||||
const externalActorActivities: Map<string, {
|
||||
actor_id: string;
|
||||
events: number;
|
||||
latest_action: string;
|
||||
latest_surface: string;
|
||||
latest_source_ip: string;
|
||||
latest_user_agent: string;
|
||||
latest_response_status: number | null;
|
||||
latest_response_summary: string;
|
||||
latest_reason: string;
|
||||
latest_created_at_ms: number;
|
||||
}> = 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,
|
||||
|
||||
@@ -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<string, ExternalActorActivity>();
|
||||
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({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">外部 Actor 行為追蹤(可追溯)</h2>
|
||||
<div className="overflow-auto max-h-96">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-gray-400 border-b border-gray-700">
|
||||
<th className="text-left py-2">Actor</th>
|
||||
<th className="text-left py-2">事件</th>
|
||||
<th className="text-left py-2">最新行為</th>
|
||||
<th className="text-left py-2">來源 IP</th>
|
||||
<th className="text-left py-2">User-Agent</th>
|
||||
<th className="text-left py-2">最新回應</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{summary.externalActorActivities.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-gray-500 py-3">
|
||||
尚無可追蹤的外部 AGENT 行為,可能目前仍未有 AGENT 類型入口流量。
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
summary.externalActorActivities.map((actor) => (
|
||||
<tr key={actor.actorId} className="border-b border-gray-800">
|
||||
<td className="py-2 text-gray-300">{actor.actorId}</td>
|
||||
<td className="py-2 text-emerald-300">{actor.events}</td>
|
||||
<td className="py-2">
|
||||
<div className="text-gray-200">{actor.latestAction}</div>
|
||||
<div className="text-xs text-gray-500">{actor.latestSurface}</div>
|
||||
</td>
|
||||
<td className="py-2 text-gray-300">{actor.latestSourceIp}</td>
|
||||
<td className="py-2 text-gray-300">{actor.latestUserAgent}</td>
|
||||
<td className="py-2">
|
||||
<div className="text-gray-200">{actor.latestResponseStatus ?? "n/a"}</div>
|
||||
<div className="text-xs text-gray-500">{actor.latestResponseSummary}</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">外部事件說明(可讀)</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mb-4 text-sm">
|
||||
@@ -542,6 +656,9 @@ export default async function TrafficDashboard({
|
||||
<div className="text-gray-400">
|
||||
actor={event.actorType}:{event.actorId || "unknown"} | entity={event.entityType}/{event.entityId} | surface={String(event.surface || "-")} | {ts}
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs mt-1">
|
||||
response={event.response_status ?? "n/a"} / summary={event.response_summary}
|
||||
</div>
|
||||
{event.reason ? <div className="text-gray-500 text-xs mt-1">{event.reason}</div> : null}
|
||||
{event.metadata ? (
|
||||
<pre className="text-xs text-gray-500 mt-1 whitespace-pre-wrap">
|
||||
|
||||
Reference in New Issue
Block a user