/** * Server-side Analytics for Next.js * * Track events from Server Components, API Routes, and Server Actions. */ import { headers } from 'next/headers'; // ───────────────────────────────────────────────────────────────────────────── // Types // ───────────────────────────────────────────────────────────────────────────── interface ServerEvent { type: string; action: string; userId?: string; metadata?: Record; } interface ServerAnalyticsConfig { collectorUrl: string; appName: string; enabled?: boolean; } // ───────────────────────────────────────────────────────────────────────────── // Configuration // ───────────────────────────────────────────────────────────────────────────── let config: ServerAnalyticsConfig = { collectorUrl: process.env.ANALYTICS_COLLECTOR_URL || 'http://localhost:4001', appName: process.env.ANALYTICS_APP_NAME || 'nextjs-app', enabled: process.env.NODE_ENV !== 'test', }; export function configureServerAnalytics(newConfig: Partial): void { config = { ...config, ...newConfig }; } // ───────────────────────────────────────────────────────────────────────────── // Server Session // ───────────────────────────────────────────────────────────────────────────── /** * Generate a server-side session ID for request correlation. */ function generateServerSessionId(): string { return `srv_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; } /** * Extract request context from Next.js headers. */ async function getRequestContext(): Promise<{ sessionId: string; ip: string; userAgent: string; path: string; }> { const headersList = await headers(); return { sessionId: headersList.get('x-session-id') || generateServerSessionId(), ip: headersList.get('x-forwarded-for')?.split(',')[0]?.trim() || headersList.get('x-real-ip') || '0.0.0.0', userAgent: headersList.get('user-agent') || 'unknown', path: headersList.get('x-invoke-path') || '/', }; } // ───────────────────────────────────────────────────────────────────────────── // Tracking Functions // ───────────────────────────────────────────────────────────────────────────── /** * Track an event from server-side code. * * @example * // In a Server Component * await trackServerEvent({ * type: 'page_view', * action: 'product_viewed', * metadata: { productId: '123' }, * }); */ export async function trackServerEvent(event: ServerEvent): Promise { if (!config.enabled) return; const context = await getRequestContext(); const payload = { sessionId: context.sessionId, userId: event.userId, type: event.type, action: event.action, timestamp: new Date().toISOString(), metadata: { ...event.metadata, _server: true, _ip: context.ip, _userAgent: context.userAgent, _path: context.path, }, source: { app: config.appName, environment: process.env.NODE_ENV || 'development', }, }; try { await fetch(`${config.collectorUrl}/collect/engagement`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); } catch (error) { // Silent failure - don't break the app for analytics if (process.env.NODE_ENV === 'development') { console.warn('[Analytics] Server tracking failed:', error); } } } /** * Track a page view from a Server Component. * * @example * // In app/products/[id]/page.tsx * export default async function ProductPage({ params }) { * await trackServerPageView({ * path: `/products/${params.id}`, * metadata: { productId: params.id }, * }); * return ; * } */ export async function trackServerPageView(options: { path: string; title?: string; userId?: string; metadata?: Record; }): Promise { await trackServerEvent({ type: 'navigation', action: 'page_view', userId: options.userId, metadata: { path: options.path, title: options.title, ...options.metadata, }, }); } /** * Track API route access. * * @example * // In app/api/users/route.ts * export async function GET(request: Request) { * await trackApiCall({ route: '/api/users', method: 'GET' }); * return Response.json({ users: [] }); * } */ export async function trackApiCall(options: { route: string; method: string; userId?: string; statusCode?: number; durationMs?: number; metadata?: Record; }): Promise { await trackServerEvent({ type: 'api_call', action: `${options.method} ${options.route}`, userId: options.userId, metadata: { route: options.route, method: options.method, statusCode: options.statusCode, durationMs: options.durationMs, ...options.metadata, }, }); } /** * Track Server Action execution. * * @example * // In a Server Action * 'use server'; * export async function submitForm(data: FormData) { * const start = Date.now(); * try { * // ... action logic * await trackServerAction({ action: 'submitForm', success: true, durationMs: Date.now() - start }); * } catch (error) { * await trackServerAction({ action: 'submitForm', success: false, error: error.message }); * throw error; * } * } */ export async function trackServerAction(options: { action: string; success: boolean; userId?: string; durationMs?: number; error?: string; metadata?: Record; }): Promise { await trackServerEvent({ type: 'server_action', action: options.action, userId: options.userId, metadata: { success: options.success, durationMs: options.durationMs, error: options.error, ...options.metadata, }, }); }