113 lines
4.6 KiB
TypeScript
113 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).*)',
|
||
|
|
],
|
||
|
|
};
|