188 lines
6.5 KiB
TypeScript
188 lines
6.5 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { prisma } from '@/lib/prisma';
|
|
import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
// Rate limiting
|
|
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
|
|
if (!checkRateLimit(ip, 100, 60000)) { // 100 requests per minute for tracking
|
|
return new NextResponse(
|
|
JSON.stringify({ error: 'Rate limit exceeded' }),
|
|
{
|
|
status: 429,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...getRateLimitHeaders(ip, 100, 60000)
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
const body = await request.json();
|
|
const { type, projectId, page, performance, session } = body;
|
|
const userAgent = request.headers.get('user-agent') || undefined;
|
|
const referrer = request.headers.get('referer') || undefined;
|
|
|
|
// Track page view
|
|
if (type === 'pageview' && page) {
|
|
let projectIdNum: number | null = null;
|
|
if (projectId != null) {
|
|
const raw = projectId.toString();
|
|
const parsed = parseInt(raw, 10);
|
|
if (Number.isFinite(parsed)) {
|
|
projectIdNum = parsed;
|
|
} else {
|
|
const bySlug = await prisma.project.findFirst({
|
|
where: { slug: raw },
|
|
select: { id: true },
|
|
});
|
|
projectIdNum = bySlug?.id ?? null;
|
|
}
|
|
}
|
|
|
|
// Create page view record
|
|
await prisma.pageView.create({
|
|
data: {
|
|
projectId: projectIdNum,
|
|
page,
|
|
ip,
|
|
userAgent,
|
|
referrer
|
|
}
|
|
});
|
|
|
|
// Update project analytics if projectId exists
|
|
if (projectIdNum) {
|
|
const project = await prisma.project.findUnique({
|
|
where: { id: projectIdNum }
|
|
});
|
|
|
|
if (project) {
|
|
const analytics = (project.analytics as Record<string, unknown>) || {};
|
|
const currentViews = (analytics.views as number) || 0;
|
|
|
|
await prisma.project.update({
|
|
where: { id: projectIdNum },
|
|
data: {
|
|
analytics: {
|
|
...analytics,
|
|
views: currentViews + 1,
|
|
lastUpdated: new Date().toISOString()
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Track performance metrics
|
|
if (type === 'performance' && performance) {
|
|
// Try to get projectId from page path if not provided
|
|
let projectIdNum: number | null = null;
|
|
if (projectId) {
|
|
projectIdNum = parseInt(projectId.toString());
|
|
} else if (page) {
|
|
// Try to extract from page path like /projects/123 or /projects/slug
|
|
const match = page.match(/\/projects\/(\d+)/);
|
|
if (match) {
|
|
projectIdNum = parseInt(match[1]);
|
|
} else {
|
|
// Try to find by slug
|
|
const slugMatch = page.match(/\/projects\/([^\/]+)/);
|
|
if (slugMatch) {
|
|
const slug = slugMatch[1];
|
|
const project = await prisma.project.findFirst({
|
|
where: {
|
|
OR: [
|
|
{ id: parseInt(slug) || 0 },
|
|
{ slug }
|
|
]
|
|
}
|
|
});
|
|
if (project) projectIdNum = project.id;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (projectIdNum) {
|
|
const project = await prisma.project.findUnique({
|
|
where: { id: projectIdNum }
|
|
});
|
|
|
|
if (project) {
|
|
const perf = (project.performance as Record<string, unknown>) || {};
|
|
const analytics = (project.analytics as Record<string, unknown>) || {};
|
|
|
|
// Calculate lighthouse score from web vitals
|
|
const lcp = performance.lcp || 0;
|
|
const fid = performance.fid || 0;
|
|
const cls = performance.cls || 0;
|
|
const fcp = performance.fcp || 0;
|
|
const ttfb = performance.ttfb || 0;
|
|
|
|
// Only calculate lighthouse score if we have real web vitals data
|
|
// Check if we have at least LCP and FCP (most important metrics)
|
|
if (lcp > 0 || fcp > 0) {
|
|
// Simple lighthouse score calculation (0-100)
|
|
let lighthouseScore = 100;
|
|
if (lcp > 4000) lighthouseScore -= 25;
|
|
else if (lcp > 2500) lighthouseScore -= 15;
|
|
if (fid > 300) lighthouseScore -= 25;
|
|
else if (fid > 100) lighthouseScore -= 15;
|
|
if (cls > 0.25) lighthouseScore -= 25;
|
|
else if (cls > 0.1) lighthouseScore -= 15;
|
|
if (fcp > 3000) lighthouseScore -= 15;
|
|
if (ttfb > 800) lighthouseScore -= 10;
|
|
|
|
lighthouseScore = Math.max(0, Math.min(100, lighthouseScore));
|
|
|
|
await prisma.project.update({
|
|
where: { id: projectIdNum },
|
|
data: {
|
|
performance: {
|
|
...perf,
|
|
lighthouse: lighthouseScore,
|
|
loadTime: performance.loadTime || perf.loadTime || 0,
|
|
firstContentfulPaint: fcp || perf.firstContentfulPaint || 0,
|
|
largestContentfulPaint: lcp || perf.largestContentfulPaint || 0,
|
|
cumulativeLayoutShift: cls || perf.cumulativeLayoutShift || 0,
|
|
totalBlockingTime: performance.tbt || perf.totalBlockingTime || 0,
|
|
speedIndex: performance.si || perf.speedIndex || 0,
|
|
coreWebVitals: {
|
|
lcp: lcp || (perf.coreWebVitals as Record<string, unknown>)?.lcp || 0,
|
|
fid: fid || (perf.coreWebVitals as Record<string, unknown>)?.fid || 0,
|
|
cls: cls || (perf.coreWebVitals as Record<string, unknown>)?.cls || 0
|
|
},
|
|
lastUpdated: new Date().toISOString()
|
|
},
|
|
analytics: {
|
|
...analytics,
|
|
lastUpdated: new Date().toISOString()
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Track session data (for bounce rate calculation)
|
|
if (type === 'session' && session) {
|
|
// Store session data in a way that allows bounce rate calculation
|
|
// A bounce is a session with only one pageview
|
|
// We'll track this via PageView records and calculate bounce rate from them
|
|
}
|
|
|
|
return NextResponse.json({ success: true });
|
|
} catch (error) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.error('Analytics tracking error:', error);
|
|
}
|
|
return NextResponse.json(
|
|
{ error: 'Failed to track analytics' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|