UNPKG

@adopture/next

Version:

Next.js SDK for Adopture feature adoption tracking with SSR support

1 lines 25.3 kB
{"version":3,"sources":["../src/shared/utils.ts","../src/middleware.ts"],"names":["now","sanitizeMetadata","metadata","sensitiveKeys","result","key","value","lowerKey","sensitiveKey","extractRoute","pathname","shouldExcludeRoute","excludeRoutes","includeRoutes","route","pattern","matchRoute","regexPattern","createAdoptureMiddleware","config","request","startTime","NextResponse","userId","extractUserIdFromHeaders","getClientIP","response","success","error","e","routeEvent","sendTrackingEvent","err","performanceEvent","generateRequestId","type","event","payload","tRPCPayload","errorText","headers","authHeader","match","tokenParts","userIdHeader","cookieHeader","cookies","parseCookies","forwardedFor","realIP","cfConnectingIP","cookie","name","rest","simpleAdoptureMiddleware","apiKey","options","withAdoptureTracking","middleware","middlewarePresets"],"mappings":"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","file":"middleware.mjs","sourcesContent":["/**\n * Shared utilities for Next.js SDK\n */\n\n/**\n * Generate a unique ID for tracking\n */\nexport function generateId(): string {\n return Math.random().toString(36).substring(2) + Date.now().toString(36);\n}\n\n/**\n * Safely parse JSON with fallback\n */\nexport function safeJsonParse<T>(json: string, fallback: T): T {\n try {\n return JSON.parse(json);\n } catch {\n return fallback;\n }\n}\n\n/**\n * Safely stringify JSON with fallback\n */\nexport function safeJsonStringify(obj: any, fallback: string = '{}'): string {\n try {\n return JSON.stringify(obj);\n } catch {\n return fallback;\n }\n}\n\n/**\n * Deep merge two objects\n */\nexport function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {\n const result = { ...target };\n\n for (const key in source) {\n if (source.hasOwnProperty(key)) {\n const value = source[key];\n if (value !== null && typeof value === 'object' && !Array.isArray(value)) {\n result[key] = deepMerge(result[key] || {} as any, value);\n } else if (value !== undefined) {\n result[key] = value as any;\n }\n }\n }\n\n return result;\n}\n\n/**\n * Debounce function calls\n */\nexport function debounce<T extends (...args: any[]) => any>(\n func: T,\n wait: number\n): (...args: Parameters<T>) => void {\n let timeout: NodeJS.Timeout | null = null;\n \n return (...args: Parameters<T>) => {\n if (timeout) {\n clearTimeout(timeout);\n }\n \n timeout = setTimeout(() => {\n func(...args);\n }, wait);\n };\n}\n\n/**\n * Throttle function calls\n */\nexport function throttle<T extends (...args: any[]) => any>(\n func: T,\n limit: number\n): (...args: Parameters<T>) => void {\n let inThrottle = false;\n \n return (...args: Parameters<T>) => {\n if (!inThrottle) {\n func(...args);\n inThrottle = true;\n setTimeout(() => {\n inThrottle = false;\n }, limit);\n }\n };\n}\n\n/**\n * Get current timestamp in milliseconds\n */\nexport function now(): number {\n return Date.now();\n}\n\n/**\n * Format timestamp to ISO string\n */\nexport function formatTimestamp(timestamp?: number): string {\n return new Date(timestamp || now()).toISOString();\n}\n\n/**\n * Check if a value is a promise\n */\nexport function isPromise<T = any>(value: any): value is Promise<T> {\n return value != null && typeof value.then === 'function';\n}\n\n/**\n * Create a promise that resolves after a delay\n */\nexport function delay(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/**\n * Retry a function with exponential backoff\n */\nexport async function retryWithBackoff<T>(\n fn: () => Promise<T>,\n maxAttempts: number = 3,\n baseDelay: number = 1000\n): Promise<T> {\n let lastError: Error;\n \n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error as Error;\n \n if (attempt === maxAttempts) {\n throw lastError;\n }\n \n const delayMs = baseDelay * Math.pow(2, attempt - 1);\n await delay(delayMs);\n }\n }\n \n throw lastError!;\n}\n\n/**\n * Create a timeout promise that rejects after a delay\n */\nexport function timeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n return Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n setTimeout(() => {\n reject(new Error(`Operation timed out after ${ms}ms`));\n }, ms);\n }),\n ]);\n}\n\n/**\n * Sanitize metadata to remove sensitive information\n */\nexport function sanitizeMetadata(metadata: Record<string, any>): Record<string, any> {\n const sensitiveKeys = ['password', 'token', 'secret', 'key', 'auth', 'credential'];\n const result: Record<string, any> = {};\n \n for (const [key, value] of Object.entries(metadata)) {\n const lowerKey = key.toLowerCase();\n const isSensitive = sensitiveKeys.some(sensitiveKey => lowerKey.includes(sensitiveKey));\n \n if (isSensitive) {\n result[key] = '[REDACTED]';\n } else if (typeof value === 'object' && value !== null) {\n result[key] = sanitizeMetadata(value);\n } else {\n result[key] = value;\n }\n }\n \n return result;\n}\n\n/**\n * Extract route from Next.js pathname\n */\nexport function extractRoute(pathname: string): string {\n // Remove query parameters and hash\n const cleanPath = pathname.split('?')[0].split('#')[0];\n \n // Remove dynamic segments (e.g., /users/[id] becomes /users/*)\n return cleanPath.replace(/\\/\\[[^\\]]+\\]/g, '/*');\n}\n\n/**\n * Check if route should be excluded from tracking\n */\nexport function shouldExcludeRoute(\n pathname: string,\n excludeRoutes?: string[],\n includeRoutes?: string[]\n): boolean {\n if (!excludeRoutes && !includeRoutes) {\n return false;\n }\n \n const route = extractRoute(pathname);\n \n // If includeRoutes is specified, only include those routes\n if (includeRoutes && includeRoutes.length > 0) {\n return !includeRoutes.some(pattern => matchRoute(route, pattern));\n }\n \n // Otherwise, exclude routes in excludeRoutes\n if (excludeRoutes && excludeRoutes.length > 0) {\n return excludeRoutes.some(pattern => matchRoute(route, pattern));\n }\n \n return false;\n}\n\n/**\n * Match route against pattern (supports wildcards)\n */\nexport function matchRoute(route: string, pattern: string): boolean {\n // Convert pattern to regex (replace * with .*)\n const regexPattern = pattern.replace(/\\*/g, '.*').replace(/\\//g, '\\\\/');\n const regex = new RegExp(`^${regexPattern}$`);\n \n return regex.test(route);\n}\n\n/**\n * Extract user ID from various sources\n */\nexport function extractUserId(\n headers: Headers,\n cookies?: { [key: string]: string },\n customExtractor?: (headers: Headers) => string | null\n): string | null {\n // Try custom extractor first\n if (customExtractor) {\n const customId = customExtractor(headers);\n if (customId) {\n return customId;\n }\n }\n \n // Try common header patterns\n const authHeader = headers.get('authorization');\n if (authHeader) {\n // Extract from Bearer token or similar\n const match = authHeader.match(/user[_-]?id[=:]([a-zA-Z0-9-_]+)/i);\n if (match) {\n return match[1];\n }\n }\n \n // Try cookies\n if (cookies) {\n const userIdCookie = cookies.userId || cookies.user_id || cookies['user-id'];\n if (userIdCookie) {\n return userIdCookie;\n }\n }\n \n return null;\n}\n\n/**\n * Calculate size of an object in bytes (approximate)\n */\nexport function calculateObjectSize(obj: any): number {\n const jsonString = safeJsonStringify(obj);\n return new Blob([jsonString]).size;\n}\n\n/**\n * Create a cache with TTL\n */\nexport class TTLCache<K, V> {\n private cache = new Map<K, { value: V; expiry: number }>();\n private defaultTTL: number;\n\n constructor(defaultTTL: number = 300000) { // 5 minutes default\n this.defaultTTL = defaultTTL;\n }\n\n set(key: K, value: V, ttl?: number): void {\n const expiry = now() + (ttl ?? this.defaultTTL);\n this.cache.set(key, { value, expiry });\n }\n\n get(key: K): V | undefined {\n const entry = this.cache.get(key);\n if (!entry) {\n return undefined;\n }\n\n if (now() > entry.expiry) {\n this.cache.delete(key);\n return undefined;\n }\n\n return entry.value;\n }\n\n has(key: K): boolean {\n return this.get(key) !== undefined;\n }\n\n delete(key: K): boolean {\n return this.cache.delete(key);\n }\n\n clear(): void {\n this.cache.clear();\n }\n\n cleanup(): void {\n const currentTime = now();\n for (const [key, entry] of this.cache.entries()) {\n if (currentTime > entry.expiry) {\n this.cache.delete(key);\n }\n }\n }\n\n size(): number {\n this.cleanup();\n return this.cache.size;\n }\n\n forEach(callback: (value: V, key: K) => void): void {\n const currentTime = now();\n for (const [key, entry] of this.cache.entries()) {\n if (currentTime <= entry.expiry) {\n callback(entry.value, key);\n } else {\n // Clean up expired entries during iteration\n this.cache.delete(key);\n }\n }\n }\n}","/**\n * Next.js Middleware integration for Adopture\n * Provides edge runtime compatible tracking for route changes and performance metrics\n */\n\nimport { NextRequest, NextResponse } from 'next/server';\nimport type { MiddlewareConfig } from './types';\nimport { shouldExcludeRoute, extractUserId, sanitizeMetadata, now } from './shared/utils';\n\n/**\n * Adopture middleware for Next.js\n * Tracks route changes and performance metrics at the edge\n */\nexport function createAdoptureMiddleware(config: MiddlewareConfig) {\n return async function adoptureMiddleware(request: NextRequest): Promise<NextResponse> {\n const startTime = now();\n const pathname = request.nextUrl.pathname;\n \n // Check if route should be tracked\n if (shouldExcludeRoute(pathname, config.excludeRoutes, config.includeRoutes)) {\n return NextResponse.next();\n }\n\n // Extract user ID from request\n const userId = config.userIdExtractor?.(request) || extractUserIdFromHeaders(request.headers);\n \n // Extract metadata from request\n const metadata = {\n ...(config.metadataExtractor?.(request) || {}),\n pathname,\n method: request.method,\n userAgent: request.headers.get('user-agent') || '',\n referer: request.headers.get('referer') || '',\n ip: getClientIP(request),\n country: (request as any).geo?.country || '',\n city: (request as any).geo?.city || '',\n region: (request as any).geo?.region || '',\n };\n\n let response: NextResponse;\n let success = true;\n let error: string | null = null;\n\n try {\n response = NextResponse.next();\n } catch (e) {\n success = false;\n error = e instanceof Error ? e.message : 'Unknown error';\n response = NextResponse.next();\n }\n\n // Track route access if enabled\n if (config.enableRouteTracking && userId) {\n const routeEvent = {\n featureId: `route-${pathname.replace(/[^a-zA-Z0-9_.-]/g, '-')}`,\n userId,\n metadata: sanitizeMetadata({\n ...metadata,\n success,\n error,\n duration: now() - startTime,\n }),\n timestamp: startTime,\n };\n\n // Send event asynchronously to avoid blocking the response\n sendTrackingEvent(config, 'route', routeEvent).catch(err => {\n console.error('[ADOPTURE-MIDDLEWARE] Failed to send route event:', err);\n });\n }\n\n // Track performance metrics if enabled\n if (config.enablePerformanceTracking && userId) {\n const performanceEvent = {\n featureId: 'middleware:performance',\n userId,\n metadata: sanitizeMetadata({\n ...metadata,\n responseTime: now() - startTime,\n success,\n error,\n }),\n timestamp: startTime,\n };\n\n // Send event asynchronously\n sendTrackingEvent(config, 'performance', performanceEvent).catch(err => {\n console.error('[ADOPTURE-MIDDLEWARE] Failed to send performance event:', err);\n });\n }\n\n // Add tracking headers to response (for client-side correlation)\n response.headers.set('x-adopture-request-id', generateRequestId());\n response.headers.set('x-adopture-timestamp', startTime.toString());\n \n if (userId) {\n response.headers.set('x-adopture-user-id', userId);\n }\n\n return response;\n };\n}\n\n/**\n * Send tracking event to Adopture API\n */\nasync function sendTrackingEvent(\n config: MiddlewareConfig,\n type: string,\n event: any\n): Promise<void> {\n const payload = {\n type,\n event,\n timestamp: now(),\n source: 'middleware',\n };\n\n try {\n // Convert to tRPC format for middleware events\n const tRPCPayload = {\n projectId: '35207190-2644-4230-b088-9ada94638e5d',\n featureId: event.featureId || `route-${payload.type}`,\n userId: event.userId || 'anonymous',\n eventType: 'feature_exposed' as const,\n sessionId: event.sessionId || `middleware-${Date.now()}`,\n metadata: event.metadata || {}\n };\n \n const response = await fetch(`${config.apiUrl || 'http://localhost:3001'}/trpc/featureAdoption.trackEvent`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${config.apiKey}`,\n 'User-Agent': 'adopture-next-middleware/0.1.0',\n },\n body: JSON.stringify(tRPCPayload),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`HTTP ${response.status}: ${errorText}`);\n }\n } catch (error) {\n // Re-throw for caller to handle\n throw error;\n }\n}\n\n/**\n * Extract user ID from request headers\n */\nfunction extractUserIdFromHeaders(headers: Headers): string | null {\n // Try authorization header\n const authHeader = headers.get('authorization');\n if (authHeader) {\n // Extract from Bearer token\n const match = authHeader.match(/Bearer\\s+(.+)/i);\n if (match) {\n // Try to decode user ID from token (simplified)\n try {\n const tokenParts = match[1].split('.');\n if (tokenParts.length === 3) {\n // JWT-like token\n const payload = JSON.parse(atob(tokenParts[1]));\n return payload.sub || payload.userId || payload.user_id || null;\n }\n } catch {\n // Not a JWT, continue\n }\n }\n }\n\n // Try custom headers\n const userIdHeader = headers.get('x-user-id') || \n headers.get('x-user') || \n headers.get('user-id');\n if (userIdHeader) {\n return userIdHeader;\n }\n\n // Try cookie header\n const cookieHeader = headers.get('cookie');\n if (cookieHeader) {\n const cookies = parseCookies(cookieHeader);\n return cookies.userId || cookies.user_id || cookies['user-id'] || null;\n }\n\n return null;\n}\n\n/**\n * Get client IP address from request\n */\nfunction getClientIP(request: NextRequest): string {\n // Try various headers for client IP\n const forwardedFor = request.headers.get('x-forwarded-for');\n if (forwardedFor) {\n return forwardedFor.split(',')[0].trim();\n }\n\n const realIP = request.headers.get('x-real-ip');\n if (realIP) {\n return realIP;\n }\n\n const cfConnectingIP = request.headers.get('cf-connecting-ip');\n if (cfConnectingIP) {\n return cfConnectingIP;\n }\n\n return (request as any).ip || 'unknown';\n}\n\n/**\n * Parse cookie header string into object\n */\nfunction parseCookies(cookieHeader: string): { [key: string]: string } {\n const cookies: { [key: string]: string } = {};\n \n cookieHeader.split(';').forEach(cookie => {\n const [name, ...rest] = cookie.split('=');\n if (name && rest.length > 0) {\n cookies[name.trim()] = rest.join('=').trim();\n }\n });\n\n return cookies;\n}\n\n/**\n * Generate unique request ID\n */\nfunction generateRequestId(): string {\n return Math.random().toString(36).substring(2) + Date.now().toString(36);\n}\n\n/**\n * Utility function to create a basic middleware with minimal configuration\n */\nexport function simpleAdoptureMiddleware(apiKey: string, options?: {\n enableRouteTracking?: boolean;\n enablePerformanceTracking?: boolean;\n excludeRoutes?: string[];\n}) {\n return createAdoptureMiddleware({\n apiKey,\n enableRouteTracking: options?.enableRouteTracking ?? true,\n enablePerformanceTracking: options?.enablePerformanceTracking ?? false,\n excludeRoutes: options?.excludeRoutes,\n });\n}\n\n/**\n * Higher-order middleware that wraps existing middleware with Adopture tracking\n */\nexport function withAdoptureTracking<T extends NextRequest>(\n middleware: (request: T) => NextResponse | Promise<NextResponse>,\n config: MiddlewareConfig\n) {\n return async function wrappedMiddleware(request: T): Promise<NextResponse> {\n const adoptureMiddleware = createAdoptureMiddleware(config);\n \n // Run Adopture tracking first\n await adoptureMiddleware(request);\n \n // Then run the original middleware\n return middleware(request);\n };\n}\n\n/**\n * Configuration helper for common middleware patterns\n */\nexport const middlewarePresets = {\n /**\n * Basic tracking for all routes\n */\n basic: (apiKey: string): MiddlewareConfig => ({\n apiKey,\n enableRouteTracking: true,\n enablePerformanceTracking: false,\n }),\n\n /**\n * API-focused tracking\n */\n api: (apiKey: string): MiddlewareConfig => ({\n apiKey,\n enableRouteTracking: true,\n enablePerformanceTracking: true,\n includeRoutes: ['/api/*'],\n }),\n\n /**\n * Page-focused tracking (exclude API routes)\n */\n pages: (apiKey: string): MiddlewareConfig => ({\n apiKey,\n enableRouteTracking: true,\n enablePerformanceTracking: false,\n excludeRoutes: ['/api/*', '/_next/*', '/favicon.ico'],\n }),\n\n /**\n * Full tracking with performance metrics\n */\n full: (apiKey: string): MiddlewareConfig => ({\n apiKey,\n enableRouteTracking: true,\n enablePerformanceTracking: true,\n excludeRoutes: ['/_next/*', '/favicon.ico'],\n }),\n};"]}