analytics/examples/nextjs/middleware.ts
2026-01-29 08:20:58 -08:00

112 lines
4.6 KiB
TypeScript

/**
* 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<NextResponse> {
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<void> {
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).*)',
],
};