Add Web Vitals performance tracking (LCP, INP, CLS, FCP, TTFB)

End-to-end performance metrics tracking: dedicated database table with
percentile aggregation, tracker collection behind data-perf attribute,
/api/send handling, report API endpoints, and Performance dashboard page
with threshold-based metric cards, time-series chart, and per-page breakdown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mike CaoandClaude Opus 4.6 committed 2026-02-08 19:25:20 -08:00
1 parent 8cd3c03702
commit ce9e2416fb
24 files changed
+1028 -8

No files matched your search

+29 -2
View File
@@ -12,7 +12,7 @@ import { parseRequest } from '@/lib/request';
import { badRequest, forbidden, json, serverError } from '@/lib/response';
import { anyObjectParam, urlOrPathParam } from '@/lib/schema';
import { safeDecodeURI, safeDecodeURIComponent } from '@/lib/url';
import { createSession, saveEvent, saveSessionData } from '@/queries/sql';
import { createSession, saveEvent, savePerformance, saveSessionData } from '@/queries/sql';
interface Cache {
websiteId: string;
@@ -22,7 +22,7 @@ interface Cache {
}
const schema = z.object({
type: z.enum(['event', 'identify']),
type: z.enum(['event', 'identify', 'performance']),
payload: z
.object({
website: z.uuid().optional(),
@@ -44,6 +44,11 @@ const schema = z.object({
browser: z.string().optional(),
os: z.string().optional(),
device: z.string().optional(),
lcp: z.number().nonnegative().max(60000).optional(),
inp: z.number().nonnegative().max(60000).optional(),
cls: z.number().nonnegative().max(100).optional(),
fcp: z.number().nonnegative().max(60000).optional(),
ttfb: z.number().nonnegative().max(60000).optional(),
})
.refine(
data => {
@@ -83,6 +88,11 @@ export async function POST(request: Request) {
tag,
timestamp,
id,
lcp,
inp,
cls,
fcp,
ttfb,
} = payload;
const sourceId = websiteId || pixelId || linkId;
@@ -269,6 +279,23 @@ export async function POST(request: Request) {
createdAt,
});
}
} else if (type === COLLECTION_TYPE.performance) {
const base = hostname ? `https://${hostname}` : 'https://localhost';
const currentUrl = new URL(url, base);
const urlPath = currentUrl.pathname === '/undefined' ? '' : currentUrl.pathname;
await savePerformance({
websiteId: sourceId,
sessionId,
visitId,
urlPath,
lcp,
inp,
cls,
fcp,
ttfb,
createdAt,
});
}
const token = createToken({ websiteId, sessionId, visitId, iat }, secret());