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) { const projectIdNum = projectId ? parseInt(projectId.toString()) : 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) || {}; 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 }, { title: { contains: slug, mode: 'insensitive' } } ] } }); if (project) projectIdNum = project.id; } } } if (projectIdNum) { const project = await prisma.project.findUnique({ where: { id: projectIdNum } }); if (project) { const perf = (project.performance as Record) || {}; const analytics = (project.analytics as Record) || {}; // 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)?.lcp || 0, fid: fid || (perf.coreWebVitals as Record)?.fid || 0, cls: cls || (perf.coreWebVitals as Record)?.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 } ); } }