From 32e621df14586a5db2ba01a20c53864295abee1d Mon Sep 17 00:00:00 2001 From: denshooter Date: Fri, 27 Feb 2026 23:12:50 +0100 Subject: [PATCH] fix: namespace rate limit buckets per endpoint, remove custom analytics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `prefix` param to checkRateLimit/getRateLimitHeaders so each endpoint has its own bucket (previously all shared `admin_${ip}`, causing 429s when analytics/track incremented past n8n endpoints' lower limits) - n8n/hardcover/currently-reading → prefix 'n8n-reading' - n8n/status → prefix 'n8n-status' - analytics/track → prefix 'analytics-track' - Remove custom analytics system (AnalyticsProvider, lib/analytics, lib/useWebVitals, all /api/analytics/* routes) — was causing 500s in production due to missing PostgreSQL PageView table - Remove analytics consent toggle from ConsentBanner/ConsentProvider Co-Authored-By: Claude Sonnet 4.6 --- app/_ui/ProjectDetailClient.tsx | 8 +- app/api/analytics/dashboard/route.ts | 174 --------- app/api/analytics/performance/route.ts | 139 ------- app/api/analytics/reset/route.ts | 211 ----------- app/api/analytics/route.ts | 51 --- app/api/analytics/track/route.ts | 187 --------- .../n8n/hardcover/currently-reading/route.ts | 2 +- app/api/n8n/status/route.ts | 2 +- app/components/ClientProviders.tsx | 12 +- app/components/ConsentBanner.tsx | 17 +- app/components/ConsentProvider.tsx | 2 - app/components/RootProviders.tsx | 20 +- app/projects/[slug]/page.tsx | 19 - components/AnalyticsProvider.tsx | 321 ---------------- lib/analytics.ts | 144 ------- lib/auth.ts | 8 +- lib/useWebVitals.ts | 355 ------------------ 17 files changed, 17 insertions(+), 1655 deletions(-) delete mode 100644 app/api/analytics/dashboard/route.ts delete mode 100644 app/api/analytics/performance/route.ts delete mode 100644 app/api/analytics/reset/route.ts delete mode 100644 app/api/analytics/route.ts delete mode 100644 app/api/analytics/track/route.ts delete mode 100644 components/AnalyticsProvider.tsx delete mode 100644 lib/analytics.ts delete mode 100644 lib/useWebVitals.ts diff --git a/app/_ui/ProjectDetailClient.tsx b/app/_ui/ProjectDetailClient.tsx index 4b206c3..d642379 100644 --- a/app/_ui/ProjectDetailClient.tsx +++ b/app/_ui/ProjectDetailClient.tsx @@ -46,13 +46,7 @@ export default function ProjectDetailClient({ setCanGoBack(true); } - try { - navigator.sendBeacon?.( - "/api/analytics/track", - new Blob([JSON.stringify({ type: "pageview", projectId: project.id.toString(), page: `/${locale}/projects/${project.slug}` })], { type: "application/json" }), - ); - } catch {} - }, [project.id, project.slug, locale]); + }, []); const handleBack = (e: React.MouseEvent) => { e.preventDefault(); diff --git a/app/api/analytics/dashboard/route.ts b/app/api/analytics/dashboard/route.ts deleted file mode 100644 index 497949e..0000000 --- a/app/api/analytics/dashboard/route.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { prisma, projectService } from '@/lib/prisma'; -import { analyticsCache } from '@/lib/redis'; -import { requireSessionAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; - -export async function GET(request: NextRequest) { - try { - // Rate limiting - more generous for admin dashboard - const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; - if (!checkRateLimit(ip, 20, 60000)) { // 20 requests per minute - return new NextResponse( - JSON.stringify({ error: 'Rate limit exceeded' }), - { - status: 429, - headers: { - 'Content-Type': 'application/json', - ...getRateLimitHeaders(ip, 20, 60000) - } - } - ); - } - - // Admin-only endpoint: require explicit admin header AND a valid signed session token - const isAdminRequest = request.headers.get('x-admin-request') === 'true'; - if (!isAdminRequest) return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); - const authError = requireSessionAuth(request); - if (authError) return authError; - - // 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 - const projectsResult = await projectService.getAllProjects(); - const projects = projectsResult.projects || projectsResult; - const performanceStats = await projectService.getPerformanceStats(); - - const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); - - // Use DB aggregation instead of loading every PageView row into memory - const [totalViews, sessionsByIp, viewsByProjectRows] = await Promise.all([ - prisma.pageView.count({ where: { timestamp: { gte: since } } }), - prisma.pageView.groupBy({ - by: ['ip'], - where: { - timestamp: { gte: since }, - ip: { not: null }, - }, - _count: { _all: true }, - _min: { timestamp: true }, - _max: { timestamp: true }, - }), - prisma.pageView.groupBy({ - by: ['projectId'], - where: { - timestamp: { gte: since }, - projectId: { not: null }, - }, - _count: { _all: true }, - }), - ]); - - const totalSessions = sessionsByIp.length; - const bouncedSessions = sessionsByIp.filter(s => (s as unknown as { _count?: { _all?: number } })._count?._all === 1).length; - const bounceRate = totalSessions > 0 ? Math.round((bouncedSessions / totalSessions) * 100) : 0; - - const sessionDurationsMs = sessionsByIp - .map(s => { - const count = (s as unknown as { _count?: { _all?: number } })._count?._all ?? 0; - if (count < 2) return 0; - const minTs = (s as unknown as { _min?: { timestamp?: Date | null } })._min?.timestamp; - const maxTs = (s as unknown as { _max?: { timestamp?: Date | null } })._max?.timestamp; - if (!minTs || !maxTs) return 0; - return maxTs.getTime() - minTs.getTime(); - }) - .filter(ms => ms > 0); - - const avgSessionDuration = sessionDurationsMs.length > 0 - ? Math.round(sessionDurationsMs.reduce((a, b) => a + b, 0) / sessionDurationsMs.length / 1000) - : 0; - - const totalUsers = totalSessions; - - const viewsByProject = viewsByProjectRows.reduce((acc, row) => { - const projectId = row.projectId as number | null; - if (projectId != null) { - acc[projectId] = (row as unknown as { _count?: { _all?: number } })._count?._all ?? 0; - } - return acc; - }, {} as Record); - - // Calculate analytics metrics - const analytics = { - overview: { - totalProjects: projects.length, - publishedProjects: projects.filter(p => p.published).length, - featuredProjects: projects.filter(p => p.featured).length, - totalViews, // 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) || {}; - 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)?.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: 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) || {}; - 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, - updatedAt: project.updatedAt - })), - categories: performanceStats.byCategory, - difficulties: performanceStats.byDifficulty, - performance: { - avgLighthouse: (() => { - const projectsWithPerf = projects.filter(p => { - const perf = (p.performance as Record) || {}; - return (perf.lighthouse as number || 0) > 0; - }); - return projectsWithPerf.length > 0 - ? Math.round(projectsWithPerf.reduce((sum, p) => sum + ((p.performance as Record)?.lighthouse as number || 0), 0) / projectsWithPerf.length) - : 0; - })(), - totalViews, // Real total views - totalLikes: 0, - totalShares: 0 - }, - metrics: { - bounceRate, - avgSessionDuration, - pagesPerSession: totalSessions > 0 ? (totalViews / totalSessions).toFixed(1) : '0', - newUsers: totalUsers, - totalUsers - } - }; - - // Cache the results - await analyticsCache.setOverallStats(analytics); - - return NextResponse.json(analytics); - } catch (error) { - console.error('Analytics dashboard error:', error); - return NextResponse.json( - { error: 'Failed to fetch analytics data' }, - { status: 500 } - ); - } -} diff --git a/app/api/analytics/performance/route.ts b/app/api/analytics/performance/route.ts deleted file mode 100644 index 697bf1c..0000000 --- a/app/api/analytics/performance/route.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { prisma } from '@/lib/prisma'; -import { requireSessionAuth } from '@/lib/auth'; - -export async function GET(request: NextRequest) { - try { - // Admin-only endpoint - const isAdminRequest = request.headers.get('x-admin-request') === 'true'; - if (!isAdminRequest) return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); - const authError = requireSessionAuth(request); - if (authError) return authError; - - // Get performance data from database - const pageViews = await prisma.pageView.findMany({ - orderBy: { timestamp: 'desc' }, - take: 1000 // Last 1000 page views - }); - - const userInteractions = await prisma.userInteraction.findMany({ - orderBy: { timestamp: 'desc' }, - 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)?.lighthouse as number) || 0, - loadTime: ((p.performance as Record)?.loadTime as number) || 0, - fcp: ((p.performance as Record)?.firstContentfulPaint as number) || 0, - lcp: ((p.performance as Record)?.coreWebVitals as Record)?.lcp as number || 0, - cls: ((p.performance as Record)?.coreWebVitals as Record)?.cls as number || 0 - })); - - // Calculate average lighthouse score (currently unused but kept for future use) - 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); - - 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) || {}; - return (perf.lighthouse as number || 0) > 0; - }); - return projectsWithPerf.length > 0 - ? Math.round(projectsWithPerf.reduce((sum, p) => { - const perf = (p.performance as Record) || {}; - 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 => { - const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); - return new Date(pv.timestamp) > dayAgo; - }).length, - last7d: pageViews.filter(pv => { - const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); - return new Date(pv.timestamp) > weekAgo; - }).length, - last30d: pageViews.filter(pv => { - const monthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); - return new Date(pv.timestamp) > monthAgo; - }).length - }, - interactions: { - total: userInteractions.length, - last24h: userInteractions.filter(ui => { - const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); - return new Date(ui.timestamp) > dayAgo; - }).length, - last7d: userInteractions.filter(ui => { - const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); - return new Date(ui.timestamp) > weekAgo; - }).length, - last30d: userInteractions.filter(ui => { - const monthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); - return new Date(ui.timestamp) > monthAgo; - }).length - }, - topPages: pageViews.reduce((acc, pv) => { - acc[pv.page] = (acc[pv.page] || 0) + 1; - return acc; - }, {} as Record), - topInteractions: userInteractions.reduce((acc, ui) => { - acc[ui.type] = (acc[ui.type] || 0) + 1; - return acc; - }, {} as Record) - }; - - return NextResponse.json(performance); - } catch (error) { - console.error('Performance analytics error:', error); - return NextResponse.json( - { error: 'Failed to fetch performance data' }, - { status: 500 } - ); - } -} diff --git a/app/api/analytics/reset/route.ts b/app/api/analytics/reset/route.ts deleted file mode 100644 index f99e428..0000000 --- a/app/api/analytics/reset/route.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { prisma } from '@/lib/prisma'; -import { analyticsCache } from '@/lib/redis'; -import { requireSessionAuth, 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, 3, 300000)) { // 3 requests per 5 minutes - more restrictive for reset - return new NextResponse( - JSON.stringify({ error: 'Rate limit exceeded' }), - { - status: 429, - headers: { - 'Content-Type': 'application/json', - ...getRateLimitHeaders(ip, 3, 300000) - } - } - ); - } - - // Check admin authentication - const isAdminRequest = request.headers.get('x-admin-request') === 'true'; - if (!isAdminRequest) return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); - const authError = requireSessionAuth(request); - if (authError) return authError; - - const { type } = await request.json(); - - switch (type) { - case 'analytics': - // 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) || {}; - await 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() - } - } - }); - } - 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) || {}; - await prisma.project.update({ - where: { id: project.id }, - data: { - 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() - } - } - }); - } - 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) || {}; - const perf = (project.performance as Record) || {}; - 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({}), - prisma.userInteraction.deleteMany({}) - ]); - break; - - default: - return NextResponse.json( - { error: 'Invalid reset type. Use: analytics, pageviews, interactions, performance, or all' }, - { status: 400 } - ); - } - - // Clear cache - await analyticsCache.clearAll(); - - return NextResponse.json({ - success: true, - message: `Successfully reset ${type} data`, - timestamp: new Date().toISOString() - }); - - } catch (error) { - console.error('Analytics reset error:', error); - return NextResponse.json( - { error: 'Failed to reset analytics data' }, - { status: 500 } - ); - } -} diff --git a/app/api/analytics/route.ts b/app/api/analytics/route.ts deleted file mode 100644 index 650f4a6..0000000 --- a/app/api/analytics/route.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; - -export async function POST(request: NextRequest) { - try { - // Rate limiting for POST requests - const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; - if (!checkRateLimit(ip, 30, 60000)) { // 30 requests per minute for analytics - return new NextResponse( - JSON.stringify({ error: 'Rate limit exceeded' }), - { - status: 429, - headers: { - 'Content-Type': 'application/json', - ...getRateLimitHeaders(ip, 30, 60000) - } - } - ); - } - - const body = await request.json(); - - // Log performance metrics (you can extend this to store in database) - if (process.env.NODE_ENV === 'development') { - console.log('Performance Metric:', { - timestamp: new Date().toISOString(), - ...body, - }); - } - - // You could store this in a database or send to external service - // For now, we'll just log it since Umami handles the main analytics - - return NextResponse.json({ success: true }); - } catch (error) { - if (process.env.NODE_ENV === 'development') { - console.error('Analytics API Error:', error); - } - return NextResponse.json( - { error: 'Failed to process analytics data' }, - { status: 500 } - ); - } -} - -export async function GET() { - return NextResponse.json({ - message: 'Analytics API is running', - timestamp: new Date().toISOString(), - }); -} diff --git a/app/api/analytics/track/route.ts b/app/api/analytics/track/route.ts deleted file mode 100644 index 2291145..0000000 --- a/app/api/analytics/track/route.ts +++ /dev/null @@ -1,187 +0,0 @@ -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) || {}; - 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) || {}; - 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 } - ); - } -} diff --git a/app/api/n8n/hardcover/currently-reading/route.ts b/app/api/n8n/hardcover/currently-reading/route.ts index 0d4d949..5bec57f 100644 --- a/app/api/n8n/hardcover/currently-reading/route.ts +++ b/app/api/n8n/hardcover/currently-reading/route.ts @@ -21,7 +21,7 @@ export async function GET(request: NextRequest) { : ip; const maxPerMinute = process.env.NODE_ENV === "development" ? 60 : 10; - if (!checkRateLimit(rateKey, maxPerMinute, 60000)) { // requests per minute + if (!checkRateLimit(rateKey, maxPerMinute, 60000, 'n8n-reading')) { // requests per minute return NextResponse.json( { error: 'Rate limit exceeded. Please try again later.' }, { status: 429 } diff --git a/app/api/n8n/status/route.ts b/app/api/n8n/status/route.ts index 83b1466..2734cb6 100644 --- a/app/api/n8n/status/route.ts +++ b/app/api/n8n/status/route.ts @@ -20,7 +20,7 @@ export async function GET(request: NextRequest) { : ip; const maxPerMinute = process.env.NODE_ENV === "development" ? 300 : 30; - if (!checkRateLimit(rateKey, maxPerMinute, 60000)) { // requests per minute + if (!checkRateLimit(rateKey, maxPerMinute, 60000, 'n8n-status')) { // requests per minute return NextResponse.json( { error: 'Rate limit exceeded. Please try again later.' }, { status: 429 } diff --git a/app/components/ClientProviders.tsx b/app/components/ClientProviders.tsx index c8031aa..06efe5a 100644 --- a/app/components/ClientProviders.tsx +++ b/app/components/ClientProviders.tsx @@ -5,8 +5,7 @@ import { usePathname } from "next/navigation"; import dynamic from "next/dynamic"; import { ToastProvider } from "@/components/Toast"; import ErrorBoundary from "@/components/ErrorBoundary"; -import { AnalyticsProvider } from "@/components/AnalyticsProvider"; -import { ConsentProvider, useConsent } from "./ConsentProvider"; +import { ConsentProvider } from "./ConsentProvider"; import { ThemeProvider } from "./ThemeProvider"; import { motion, AnimatePresence } from "framer-motion"; @@ -97,12 +96,7 @@ function GatedProviders({ mounted: boolean; is404Page: boolean; }) { - const { consent } = useConsent(); - - // If consent is not decided yet, treat optional features as off - const analyticsEnabled = !!consent?.analytics; - - const content = ( + return ( {mounted && } @@ -110,6 +104,4 @@ function GatedProviders({ ); - - return analyticsEnabled ? {content} : content; } diff --git a/app/components/ConsentBanner.tsx b/app/components/ConsentBanner.tsx index 8f5f56b..22f09dd 100644 --- a/app/components/ConsentBanner.tsx +++ b/app/components/ConsentBanner.tsx @@ -6,7 +6,7 @@ import { useTranslations } from "next-intl"; export default function ConsentBanner() { const { consent, ready, setConsent } = useConsent(); - const [draft, setDraft] = useState({ analytics: false, chat: false }); + const [draft, setDraft] = useState({ chat: false }); const [minimized, setMinimized] = useState(false); const t = useTranslations("consent"); @@ -19,7 +19,6 @@ export default function ConsentBanner() { title: t("title"), description: t("description"), essential: t("essential"), - analytics: t("analytics"), chat: t("chat"), alwaysOn: t("alwaysOn"), acceptAll: t("acceptAll"), @@ -68,16 +67,6 @@ export default function ConsentBanner() {
{s.alwaysOn}
- -