feat: classify traffic audit into external vs system channels
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 8s

This commit is contained in:
OG T
2026-06-07 15:46:41 +08:00
parent 9f78778132
commit 031d5af8b0
2 changed files with 116 additions and 1 deletions

View File

@@ -15,7 +15,7 @@ export async function GET(request: NextRequest) {
const since = new Date(Date.now() - 24 * 60 * 60 * 1000);
const [summaryRows, totalRows] = await Promise.all([
const [summaryRows, actorSummaryRows, totalRows, latestEvents] = await Promise.all([
prisma.auditEvent.groupBy({
by: ["action"],
where: {
@@ -23,19 +23,91 @@ export async function GET(request: NextRequest) {
},
_count: { _all: true },
}),
prisma.auditEvent.groupBy({
by: ["actorType"],
where: {
createdAt: { gte: since },
},
_count: { _all: true },
}),
prisma.auditEvent.count({
where: { createdAt: { gte: since } },
}),
prisma.auditEvent.findMany({
where: {
createdAt: { gte: since },
},
orderBy: {
createdAt: "desc",
},
take: 80,
select: {
id: true,
action: true,
actorType: true,
actorId: true,
entityType: true,
entityId: true,
reason: true,
metadata: true,
createdAt: true,
},
}),
]);
const actionSummary = Object.fromEntries(
summaryRows.map((row) => [row.action, row._count._all])
);
const actorSummary = Object.fromEntries(
actorSummaryRows.map((row) => [row.actorType, row._count._all])
);
const channelSummary = Object.entries(actionSummary).reduce(
(acc, [action, count]) => {
if (action.startsWith("EXTERNAL_")) {
acc.external += count;
} else {
acc.internal += count;
}
return acc;
},
{ external: 0, internal: 0 } as Record<string, number>
);
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<string, unknown>)
: undefined;
return {
id: event.id,
action: event.action,
actorType: event.actorType,
actorId: event.actorId,
entityType: event.entityType,
entityId: event.entityId,
reason: event.reason,
createdAt: event.createdAt,
surface: metadata?.surface,
level: metadata?.level,
};
});
return NextResponse.json({
period_hours: 24,
total_events: totalRows,
action_summary: actionSummary,
channel_summary: channelSummary,
actor_summary: actorSummary,
external_event_types: externalEventTypes,
internal_event_types: internalEventTypes,
recent_events: recentEvents,
updated_at: new Date().toISOString(),
});
}

View File

@@ -1,3 +1,6 @@
import { prisma } from "./prisma";
import { Prisma } from "../../prisma/generated/client";
export type TrafficAlertEvent = {
level: "info" | "warning" | "error";
action: string;
@@ -11,7 +14,47 @@ export type TrafficAlertEvent = {
const TRAFFIC_WEBHOOK_URL = process.env.VIBEWORK_TRAFFIC_WEBHOOK_URL?.trim();
function resolveEntityFromTrafficEvent(event: TrafficAlertEvent) {
if (event.taskId) {
return { entityType: "TASK", entityId: event.taskId };
}
return {
entityType: "TASK",
entityId: `surface:${event.surface}`,
};
}
async function writeTrafficAuditEvent(event: TrafficAlertEvent) {
const { entityType, entityId } = resolveEntityFromTrafficEvent(event);
try {
await prisma.auditEvent.create({
data: {
actorType: event.actorType,
actorId: event.actorId,
action: event.action,
entityType,
entityId,
beforeState: Prisma.JsonNull,
afterState: Prisma.JsonNull,
reason: event.message,
metadata: {
...event.metadata,
level: event.level,
surface: event.surface,
source: "traffic-alert",
},
},
});
} catch (error) {
console.error("[Traffic alert persistence error]", error);
}
}
export async function sendTrafficAlert(event: TrafficAlertEvent): Promise<void> {
void writeTrafficAuditEvent(event);
if (!TRAFFIC_WEBHOOK_URL) return;
const payload = {