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:
2026-01-10 03:08:25 +01:00
parent b051d9d2ef
commit 40d9489395
12 changed files with 1156 additions and 445 deletions

View File

@@ -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
}
};