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

225 lines
6.9 KiB
TypeScript

/**
* 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<string, unknown>;
}
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<ServerAnalyticsConfig>): 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<void> {
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 <Product id={params.id} />;
* }
*/
export async function trackServerPageView(options: {
path: string;
title?: string;
userId?: string;
metadata?: Record<string, unknown>;
}): Promise<void> {
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<string, unknown>;
}): Promise<void> {
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<string, unknown>;
}): Promise<void> {
await trackServerEvent({
type: 'server_action',
action: options.action,
userId: options.userId,
metadata: {
success: options.success,
durationMs: options.durationMs,
error: options.error,
...options.metadata,
},
});
}