Files
NextEdu/src/app/api/web-vitals/route.ts
SpecialX 9ce8d6d3fd refactor(data-access): 补全剩余 37 个 data-access 文件迁移至 cacheFn
迁移范围:lesson-preparation 11 文件、attendance 3 文件、settings 4 文件、classes-invitations、跨模块接口 4 文件、adaptive-practice/ai/onboarding/invitation-codes/search/standards/scheduling/leave-requests/audit。修复 3 个 cacheFn 块位置错误。同步架构文档迁移范围至 83 文件 250+ 函数。
2026-07-06 15:12:30 +08:00

74 lines
1.8 KiB
TypeScript

import { NextResponse } from "next/server";
/**
* Web Vitals 上报端点
*
* 接收前端 navigator.sendBeacon 上报的 LCP/CLS/INP/FCP/TTFP 指标,
* 当前实现:写入服务端日志(便于开发期查看),生产可接入监控系统。
*
* 性能预算基线(见 docs/architecture/audit/performance-budget-audit-report.md):
* - LCP ≤ 2.5s,INP ≤ 200ms,CLS ≤ 0.05,TTFB ≤ 800ms,FCP ≤ 1.8s
* - 超出预算的 "poor" 级别会以 warn 级别日志记录便于排查
*/
export const runtime = "edge";
type WebVitalEntry = {
name: string;
startTime: number;
duration: number;
entryType: string;
};
type WebVitalsPayload = {
id: string;
name: string;
value: number;
rating: "good" | "needs-improvement" | "poor";
delta: number;
entries: WebVitalEntry[];
path: string;
timestamp: number;
};
// 性能预算基线,超出即标记为 poor
const BUDGETS: Record<string, number> = {
LCP: 2500,
INP: 200,
CLS: 0.05,
FCP: 1800,
TTFB: 800,
};
export async function POST(request: Request): Promise<NextResponse> {
let payload: WebVitalsPayload;
try {
payload = (await request.json()) as WebVitalsPayload;
} catch {
return NextResponse.json({ ok: false }, { status: 400 });
}
const budget = BUDGETS[payload.name];
const isOverBudget = budget !== undefined && payload.value > budget;
// good/info 级别用 info,rating poor 或超预算用 warn
if (payload.rating === "poor" || isOverBudget) {
console.warn("[web-vitals] over-budget", {
name: payload.name,
value: payload.value,
rating: payload.rating,
budget,
path: payload.path,
id: payload.id,
});
} else {
console.info("[web-vitals] reported", {
name: payload.name,
value: payload.value,
rating: payload.rating,
path: payload.path,
});
}
return NextResponse.json({ ok: true });
}