feat: enhance analytics and performance tracking with real data metrics
- Integrate real page view data from the database for accurate analytics. - Implement cache-busting for fresh data retrieval in analytics dashboard. - Calculate and display bounce rate, average session duration, and unique users. - Refactor performance metrics to ensure only real data is considered. - Improve user experience with toast notifications for success and error messages. - Update project editor with undo/redo functionality and enhanced content management.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { projectService } from '@/lib/prisma';
|
||||
import { prisma, projectService } from '@/lib/prisma';
|
||||
import { analyticsCache } from '@/lib/redis';
|
||||
import { requireSessionAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
|
||||
|
||||
@@ -30,10 +30,15 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
const cachedStats = await analyticsCache.getOverallStats();
|
||||
if (cachedStats) {
|
||||
return NextResponse.json(cachedStats);
|
||||
// Check cache first (but allow bypass with cache-bust parameter)
|
||||
const url = new URL(request.url);
|
||||
const bypassCache = url.searchParams.get('nocache') === 'true';
|
||||
|
||||
if (!bypassCache) {
|
||||
const cachedStats = await analyticsCache.getOverallStats();
|
||||
if (cachedStats) {
|
||||
return NextResponse.json(cachedStats);
|
||||
}
|
||||
}
|
||||
|
||||
// Get analytics data
|
||||
@@ -41,28 +46,84 @@ export async function GET(request: NextRequest) {
|
||||
const projects = projectsResult.projects || projectsResult;
|
||||
const performanceStats = await projectService.getPerformanceStats();
|
||||
|
||||
// Get real page view data from database
|
||||
const allPageViews = await prisma.pageView.findMany({
|
||||
where: {
|
||||
timestamp: {
|
||||
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // Last 30 days
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate bounce rate (sessions with only 1 pageview)
|
||||
const pageViewsByIP = allPageViews.reduce((acc, pv) => {
|
||||
const ip = pv.ip || 'unknown';
|
||||
if (!acc[ip]) acc[ip] = [];
|
||||
acc[ip].push(pv);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof allPageViews>);
|
||||
|
||||
const totalSessions = Object.keys(pageViewsByIP).length;
|
||||
const bouncedSessions = Object.values(pageViewsByIP).filter(session => session.length === 1).length;
|
||||
const bounceRate = totalSessions > 0 ? Math.round((bouncedSessions / totalSessions) * 100) : 0;
|
||||
|
||||
// Calculate average session duration (simplified - time between first and last pageview per IP)
|
||||
const sessionDurations = Object.values(pageViewsByIP)
|
||||
.map(session => {
|
||||
if (session.length < 2) return 0;
|
||||
const sorted = session.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
||||
return sorted[sorted.length - 1].timestamp.getTime() - sorted[0].timestamp.getTime();
|
||||
})
|
||||
.filter(d => d > 0);
|
||||
const avgSessionDuration = sessionDurations.length > 0
|
||||
? Math.round(sessionDurations.reduce((a, b) => a + b, 0) / sessionDurations.length / 1000) // in seconds
|
||||
: 0;
|
||||
|
||||
// Get total unique users (unique IPs)
|
||||
const totalUsers = new Set(allPageViews.map(pv => pv.ip).filter(Boolean)).size;
|
||||
|
||||
// Calculate real views from PageView table
|
||||
const viewsByProject = allPageViews.reduce((acc, pv) => {
|
||||
if (pv.projectId) {
|
||||
acc[pv.projectId] = (acc[pv.projectId] || 0) + 1;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<number, number>);
|
||||
|
||||
// Calculate analytics metrics
|
||||
const analytics = {
|
||||
overview: {
|
||||
totalProjects: projects.length,
|
||||
publishedProjects: projects.filter(p => p.published).length,
|
||||
featuredProjects: projects.filter(p => p.featured).length,
|
||||
totalViews: projects.reduce((sum, p) => sum + ((p.analytics as Record<string, unknown>)?.views as number || 0), 0),
|
||||
totalLikes: projects.reduce((sum, p) => sum + ((p.analytics as Record<string, unknown>)?.likes as number || 0), 0),
|
||||
totalShares: projects.reduce((sum, p) => sum + ((p.analytics as Record<string, unknown>)?.shares as number || 0), 0),
|
||||
avgLighthouse: projects.length > 0
|
||||
? Math.round(projects.reduce((sum, p) => sum + ((p.performance as Record<string, unknown>)?.lighthouse as number || 0), 0) / projects.length)
|
||||
: 0
|
||||
totalViews: allPageViews.length, // Real views from PageView table
|
||||
totalLikes: 0, // Not implemented - no like buttons
|
||||
totalShares: 0, // Not implemented - no share buttons
|
||||
avgLighthouse: (() => {
|
||||
// Only calculate if we have real performance data (not defaults)
|
||||
const projectsWithPerf = projects.filter(p => {
|
||||
const perf = (p.performance as Record<string, unknown>) || {};
|
||||
const lighthouse = perf.lighthouse as number || 0;
|
||||
return lighthouse > 0; // Only count projects with actual performance data
|
||||
});
|
||||
return projectsWithPerf.length > 0
|
||||
? Math.round(projectsWithPerf.reduce((sum, p) => sum + ((p.performance as Record<string, unknown>)?.lighthouse as number || 0), 0) / projectsWithPerf.length)
|
||||
: 0;
|
||||
})()
|
||||
},
|
||||
projects: projects.map(project => ({
|
||||
id: project.id,
|
||||
title: project.title,
|
||||
category: project.category,
|
||||
difficulty: project.difficulty,
|
||||
views: (project.analytics as Record<string, unknown>)?.views as number || 0,
|
||||
likes: (project.analytics as Record<string, unknown>)?.likes as number || 0,
|
||||
shares: (project.analytics as Record<string, unknown>)?.shares as number || 0,
|
||||
lighthouse: (project.performance as Record<string, unknown>)?.lighthouse as number || 0,
|
||||
views: viewsByProject[project.id] || 0, // Only real views from PageView table
|
||||
likes: 0, // Not implemented
|
||||
shares: 0, // Not implemented
|
||||
lighthouse: (() => {
|
||||
const perf = (project.performance as Record<string, unknown>) || {};
|
||||
const score = perf.lighthouse as number || 0;
|
||||
return score > 0 ? score : 0; // Only return if we have real data
|
||||
})(),
|
||||
published: project.published,
|
||||
featured: project.featured,
|
||||
createdAt: project.createdAt,
|
||||
@@ -71,10 +132,25 @@ export async function GET(request: NextRequest) {
|
||||
categories: performanceStats.byCategory,
|
||||
difficulties: performanceStats.byDifficulty,
|
||||
performance: {
|
||||
avgLighthouse: performanceStats.avgLighthouse,
|
||||
totalViews: performanceStats.totalViews,
|
||||
totalLikes: performanceStats.totalLikes,
|
||||
totalShares: performanceStats.totalShares
|
||||
avgLighthouse: (() => {
|
||||
const projectsWithPerf = projects.filter(p => {
|
||||
const perf = (p.performance as Record<string, unknown>) || {};
|
||||
return (perf.lighthouse as number || 0) > 0;
|
||||
});
|
||||
return projectsWithPerf.length > 0
|
||||
? Math.round(projectsWithPerf.reduce((sum, p) => sum + ((p.performance as Record<string, unknown>)?.lighthouse as number || 0), 0) / projectsWithPerf.length)
|
||||
: 0;
|
||||
})(),
|
||||
totalViews: allPageViews.length, // Real total views
|
||||
totalLikes: 0,
|
||||
totalShares: 0
|
||||
},
|
||||
metrics: {
|
||||
bounceRate,
|
||||
avgSessionDuration,
|
||||
pagesPerSession: totalSessions > 0 ? (allPageViews.length / totalSessions).toFixed(1) : '0',
|
||||
newUsers: totalUsers,
|
||||
totalUsers
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -24,8 +24,72 @@ export async function GET(request: NextRequest) {
|
||||
take: 1000 // Last 1000 interactions
|
||||
});
|
||||
|
||||
// Get all projects for performance data
|
||||
const projects = await prisma.project.findMany();
|
||||
|
||||
// Calculate real performance metrics from projects
|
||||
const projectsWithPerformance = projects.map(p => ({
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
lighthouse: ((p.performance as Record<string, unknown>)?.lighthouse as number) || 0,
|
||||
loadTime: ((p.performance as Record<string, unknown>)?.loadTime as number) || 0,
|
||||
fcp: ((p.performance as Record<string, unknown>)?.firstContentfulPaint as number) || 0,
|
||||
lcp: ((p.performance as Record<string, unknown>)?.coreWebVitals as Record<string, unknown>)?.lcp as number || 0,
|
||||
cls: ((p.performance as Record<string, unknown>)?.coreWebVitals as Record<string, unknown>)?.cls as number || 0
|
||||
}));
|
||||
|
||||
const avgLighthouse = projectsWithPerformance.length > 0
|
||||
? Math.round(projectsWithPerformance.reduce((sum, p) => sum + p.lighthouse, 0) / projectsWithPerformance.length)
|
||||
: 0;
|
||||
|
||||
// Calculate bounce rate from page views
|
||||
const pageViewsByIP = pageViews.reduce((acc, pv) => {
|
||||
const ip = pv.ip || 'unknown';
|
||||
if (!acc[ip]) acc[ip] = [];
|
||||
acc[ip].push(pv);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof pageViews>);
|
||||
|
||||
const totalSessions = Object.keys(pageViewsByIP).length;
|
||||
const bouncedSessions = Object.values(pageViewsByIP).filter(session => session.length === 1).length;
|
||||
const bounceRate = totalSessions > 0 ? Math.round((bouncedSessions / totalSessions) * 100) : 0;
|
||||
|
||||
// Calculate average session duration
|
||||
const sessionDurations = Object.values(pageViewsByIP)
|
||||
.map(session => {
|
||||
if (session.length < 2) return 0;
|
||||
const sorted = session.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
||||
return sorted[sorted.length - 1].timestamp.getTime() - sorted[0].timestamp.getTime();
|
||||
})
|
||||
.filter(d => d > 0);
|
||||
const avgSessionDuration = sessionDurations.length > 0
|
||||
? Math.round(sessionDurations.reduce((a, b) => a + b, 0) / sessionDurations.length / 1000) // in seconds
|
||||
: 0;
|
||||
|
||||
// Calculate pages per session
|
||||
const pagesPerSession = totalSessions > 0 ? (pageViews.length / totalSessions).toFixed(1) : '0';
|
||||
|
||||
// Calculate performance metrics
|
||||
const performance = {
|
||||
avgLighthouse: (() => {
|
||||
const projectsWithPerf = projects.filter(p => {
|
||||
const perf = (p.performance as Record<string, unknown>) || {};
|
||||
return (perf.lighthouse as number || 0) > 0;
|
||||
});
|
||||
return projectsWithPerf.length > 0
|
||||
? Math.round(projectsWithPerf.reduce((sum, p) => {
|
||||
const perf = (p.performance as Record<string, unknown>) || {};
|
||||
return sum + (perf.lighthouse as number || 0);
|
||||
}, 0) / projectsWithPerf.length)
|
||||
: 0;
|
||||
})(),
|
||||
totalViews: pageViews.length,
|
||||
metrics: {
|
||||
bounceRate,
|
||||
avgSessionDuration: avgSessionDuration,
|
||||
pagesPerSession: parseFloat(pagesPerSession),
|
||||
newUsers: new Set(pageViews.map(pv => pv.ip).filter(Boolean)).size
|
||||
},
|
||||
pageViews: {
|
||||
total: pageViews.length,
|
||||
last24h: pageViews.filter(pv => {
|
||||
|
||||
@@ -33,86 +33,15 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
switch (type) {
|
||||
case 'analytics':
|
||||
// Reset all project analytics
|
||||
await prisma.project.updateMany({
|
||||
data: {
|
||||
analytics: {
|
||||
views: 0,
|
||||
likes: 0,
|
||||
shares: 0,
|
||||
comments: 0,
|
||||
bookmarks: 0,
|
||||
clickThroughs: 0,
|
||||
bounceRate: 0,
|
||||
avgTimeOnPage: 0,
|
||||
uniqueVisitors: 0,
|
||||
returningVisitors: 0,
|
||||
conversionRate: 0,
|
||||
socialShares: {
|
||||
twitter: 0,
|
||||
linkedin: 0,
|
||||
facebook: 0,
|
||||
github: 0
|
||||
},
|
||||
deviceStats: {
|
||||
mobile: 0,
|
||||
desktop: 0,
|
||||
tablet: 0
|
||||
},
|
||||
locationStats: {},
|
||||
referrerStats: {},
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'pageviews':
|
||||
// Clear PageView table
|
||||
await prisma.pageView.deleteMany({});
|
||||
break;
|
||||
|
||||
case 'interactions':
|
||||
// Clear UserInteraction table
|
||||
await prisma.userInteraction.deleteMany({});
|
||||
break;
|
||||
|
||||
case 'performance':
|
||||
// Reset performance metrics
|
||||
await prisma.project.updateMany({
|
||||
data: {
|
||||
performance: {
|
||||
lighthouse: 0,
|
||||
loadTime: 0,
|
||||
firstContentfulPaint: 0,
|
||||
largestContentfulPaint: 0,
|
||||
cumulativeLayoutShift: 0,
|
||||
totalBlockingTime: 0,
|
||||
speedIndex: 0,
|
||||
accessibility: 0,
|
||||
bestPractices: 0,
|
||||
seo: 0,
|
||||
performanceScore: 0,
|
||||
mobileScore: 0,
|
||||
desktopScore: 0,
|
||||
coreWebVitals: {
|
||||
lcp: 0,
|
||||
fid: 0,
|
||||
cls: 0
|
||||
},
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'all':
|
||||
// Reset everything
|
||||
await Promise.all([
|
||||
// Reset analytics
|
||||
prisma.project.updateMany({
|
||||
// Reset all project analytics (view counts in project.analytics JSON)
|
||||
const projects = await prisma.project.findMany();
|
||||
for (const project of projects) {
|
||||
const analytics = (project.analytics as Record<string, unknown>) || {};
|
||||
await prisma.project.update({
|
||||
where: { id: project.id },
|
||||
data: {
|
||||
analytics: {
|
||||
...analytics,
|
||||
views: 0,
|
||||
likes: 0,
|
||||
shares: 0,
|
||||
@@ -140,11 +69,30 @@ export async function POST(request: NextRequest) {
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
}),
|
||||
// Reset performance
|
||||
prisma.project.updateMany({
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'pageviews':
|
||||
// Clear PageView table
|
||||
await prisma.pageView.deleteMany({});
|
||||
break;
|
||||
|
||||
case 'interactions':
|
||||
// Clear UserInteraction table
|
||||
await prisma.userInteraction.deleteMany({});
|
||||
break;
|
||||
|
||||
case 'performance':
|
||||
// Reset performance metrics (preserve structure)
|
||||
const projectsForPerf = await prisma.project.findMany();
|
||||
for (const project of projectsForPerf) {
|
||||
const perf = (project.performance as Record<string, unknown>) || {};
|
||||
await prisma.project.update({
|
||||
where: { id: project.id },
|
||||
data: {
|
||||
performance: {
|
||||
...perf,
|
||||
lighthouse: 0,
|
||||
loadTime: 0,
|
||||
firstContentfulPaint: 0,
|
||||
@@ -166,6 +114,73 @@ export async function POST(request: NextRequest) {
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'all':
|
||||
// Reset everything
|
||||
const allProjects = await prisma.project.findMany();
|
||||
await Promise.all([
|
||||
// Reset analytics and performance for each project (preserve structure)
|
||||
...allProjects.map(project => {
|
||||
const analytics = (project.analytics as Record<string, unknown>) || {};
|
||||
const perf = (project.performance as Record<string, unknown>) || {};
|
||||
return prisma.project.update({
|
||||
where: { id: project.id },
|
||||
data: {
|
||||
analytics: {
|
||||
...analytics,
|
||||
views: 0,
|
||||
likes: 0,
|
||||
shares: 0,
|
||||
comments: 0,
|
||||
bookmarks: 0,
|
||||
clickThroughs: 0,
|
||||
bounceRate: 0,
|
||||
avgTimeOnPage: 0,
|
||||
uniqueVisitors: 0,
|
||||
returningVisitors: 0,
|
||||
conversionRate: 0,
|
||||
socialShares: {
|
||||
twitter: 0,
|
||||
linkedin: 0,
|
||||
facebook: 0,
|
||||
github: 0
|
||||
},
|
||||
deviceStats: {
|
||||
mobile: 0,
|
||||
desktop: 0,
|
||||
tablet: 0
|
||||
},
|
||||
locationStats: {},
|
||||
referrerStats: {},
|
||||
lastUpdated: new Date().toISOString()
|
||||
},
|
||||
performance: {
|
||||
...perf,
|
||||
lighthouse: 0,
|
||||
loadTime: 0,
|
||||
firstContentfulPaint: 0,
|
||||
largestContentfulPaint: 0,
|
||||
cumulativeLayoutShift: 0,
|
||||
totalBlockingTime: 0,
|
||||
speedIndex: 0,
|
||||
accessibility: 0,
|
||||
bestPractices: 0,
|
||||
seo: 0,
|
||||
performanceScore: 0,
|
||||
mobileScore: 0,
|
||||
desktopScore: 0,
|
||||
coreWebVitals: {
|
||||
lcp: 0,
|
||||
fid: 0,
|
||||
cls: 0
|
||||
},
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
});
|
||||
}),
|
||||
// Clear tracking tables
|
||||
prisma.pageView.deleteMany({}),
|
||||
|
||||
173
app/api/analytics/track/route.ts
Normal file
173
app/api/analytics/track/route.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
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<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 },
|
||||
{ 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<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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user