feat: classify traffic audit into external vs system channels
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 8s
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 8s
This commit is contained in:
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user