/** * Analytics Middleware for Next.js * * Tracks page requests at the edge before they reach your application. * This provides accurate page view counts even for cached pages. */ import { NextResponse, type NextRequest } from 'next/server'; // ───────────────────────────────────────────────────────────────────────────── // Configuration // ───────────────────────────────────────────────────────────────────────────── const COLLECTOR_URL = process.env.ANALYTICS_COLLECTOR_URL || 'http://localhost:4001'; const APP_NAME = process.env.ANALYTICS_APP_NAME || 'nextjs-app'; const ENABLED = process.env.NODE_ENV !== 'test'; // ───────────────────────────────────────────────────────────────────────────── // Middleware // ───────────────────────────────────────────────────────────────────────────── export async function middleware(request: NextRequest): Promise { const response = NextResponse.next(); if (!ENABLED) return response; // Generate or extract session ID const existingSessionId = request.cookies.get('analytics_session')?.value; const sessionId = existingSessionId || `edge_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; // Set session cookie if new if (!existingSessionId) { response.cookies.set('analytics_session', sessionId, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 30 * 60, // 30 minutes }); } // Track asynchronously (don't block the response) trackEdgePageView(request, sessionId).catch(() => { // Silent failure }); // Add session ID header for downstream use response.headers.set('x-session-id', sessionId); return response; } // ───────────────────────────────────────────────────────────────────────────── // Tracking // ───────────────────────────────────────────────────────────────────────────── async function trackEdgePageView(request: NextRequest, sessionId: string): Promise { const payload = { sessionId, type: 'navigation', action: 'edge_page_view', timestamp: new Date().toISOString(), metadata: { path: request.nextUrl.pathname, search: request.nextUrl.search, referrer: request.headers.get('referer'), userAgent: request.headers.get('user-agent'), ip: request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('x-real-ip') || request.ip, country: request.geo?.country, region: request.geo?.region, city: request.geo?.city, _edge: true, }, source: { app: APP_NAME, environment: process.env.NODE_ENV || 'development', }, }; await fetch(`${COLLECTOR_URL}/collect/engagement`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); } // ───────────────────────────────────────────────────────────────────────────── // Route Matcher // ───────────────────────────────────────────────────────────────────────────── /** * Configure which routes the middleware runs on. * * Excludes: * - API routes (tracked separately) * - Static files * - Next.js internals */ export const config = { matcher: [ /* * Match all request paths except: * - api routes (they have their own tracking) * - _next (Next.js internals) * - static files (images, fonts, etc.) */ '/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)', ], };