UNPKG

@ai-growth/nextjs

Version:

Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering

857 lines (856 loc) 31.5 kB
// ============================================================================ // ERROR LOGGING AND MONITORING SYSTEM // ============================================================================ // ============================================================================ // TYPE DEFINITIONS // ============================================================================ /** * Error severity levels for categorization */ export var ErrorSeverity; (function (ErrorSeverity) { ErrorSeverity["LOW"] = "low"; ErrorSeverity["WARNING"] = "warning"; ErrorSeverity["ERROR"] = "error"; ErrorSeverity["CRITICAL"] = "critical"; ErrorSeverity["FATAL"] = "fatal"; })(ErrorSeverity || (ErrorSeverity = {})); /** * Error categories for better organization */ export var ErrorCategory; (function (ErrorCategory) { ErrorCategory["NETWORK"] = "network"; ErrorCategory["AUTHENTICATION"] = "authentication"; ErrorCategory["VALIDATION"] = "validation"; ErrorCategory["PERMISSION"] = "permission"; ErrorCategory["RUNTIME"] = "runtime"; ErrorCategory["PERFORMANCE"] = "performance"; ErrorCategory["UI"] = "ui"; ErrorCategory["CMS"] = "cms"; ErrorCategory["INTEGRATION"] = "integration"; ErrorCategory["UNKNOWN"] = "unknown"; })(ErrorCategory || (ErrorCategory = {})); // ============================================================================ // DEFAULT CONFIGURATION // ============================================================================ const DEFAULT_CONFIG = { enabled: true, maxStoredErrors: 1000, batchSize: 10, batchInterval: 30000, // 30 seconds captureConsoleErrors: true, captureUnhandledRejections: true, captureWindowErrors: true, minSeverity: ErrorSeverity.WARNING, maxErrorsPerMinute: 60, includeStackTrace: true, includePerformanceContext: true, errorFilters: [], contextProviders: [], }; // ============================================================================ // UTILITY FUNCTIONS // ============================================================================ /** * Gather performance context */ function getPerformanceContext() { const context = {}; try { if (typeof window !== 'undefined' && window.performance) { // Navigation timing - verify if getEntriesByType is supported if (typeof window.performance.getEntriesByType === 'function') { try { const navigationEntries = window.performance.getEntriesByType('navigation'); if (navigationEntries && navigationEntries.length > 0) { const navigation = navigationEntries[0]; // Verify navigation has required properties if (navigation && typeof navigation.loadEventEnd === 'number' && typeof navigation.fetchStart === 'number') { context.pageLoadTime = navigation.loadEventEnd - navigation.fetchStart; if (typeof navigation.responseStart === 'number') { context.renderMetrics = context.renderMetrics || {}; context.renderMetrics.timeToFirstByte = navigation.responseStart - navigation.fetchStart; } } } // First contentful paint try { const paintEntries = window.performance.getEntriesByType('paint'); if (paintEntries && paintEntries.length > 0) { const firstPaint = paintEntries.find((entry) => entry.name === 'first-contentful-paint'); if (firstPaint && typeof firstPaint.startTime === 'number') { context.renderMetrics = context.renderMetrics || {}; context.renderMetrics.firstContentfulPaint = firstPaint.startTime; } } } catch (_) { // Ignore paint entries errors } } catch (_) { // Ignore navigation errors } } // Memory info - available in Chrome only, may not be in JSDOM try { // Use 'as any' to bypass TypeScript's type checking const performanceWithMemory = window.performance; if (performanceWithMemory.memory && typeof performanceWithMemory.memory.usedJSHeapSize === 'number' && typeof performanceWithMemory.memory.totalJSHeapSize === 'number') { context.memoryUsage = { used: performanceWithMemory.memory.usedJSHeapSize, total: performanceWithMemory.memory.totalJSHeapSize, percentage: performanceWithMemory.memory.totalJSHeapSize > 0 ? (performanceWithMemory.memory.usedJSHeapSize / performanceWithMemory.memory.totalJSHeapSize) * 100 : 0 }; } } catch (_) { // Ignore memory errors } } // Network connection info if (typeof navigator !== 'undefined') { try { const navigatorWithConnection = navigator; if (navigatorWithConnection.connection) { context.connection = {}; if (typeof navigatorWithConnection.connection.effectiveType === 'string') { context.connection.effectiveType = navigatorWithConnection.connection.effectiveType; } if (typeof navigatorWithConnection.connection.downlink === 'number') { context.connection.downlink = navigatorWithConnection.connection.downlink; } if (typeof navigatorWithConnection.connection.rtt === 'number') { context.connection.rtt = navigatorWithConnection.connection.rtt; } } } catch (_) { // Ignore connection errors } } } catch (_) { // Silently fail if performance API isn't available } return context; } /** * Create a unique fingerprint for error deduplication */ function createErrorFingerprint(error, context = {}) { try { // Extract the most relevant parts of the error // Include message, filename, line & column from stack or error // Include component from context if available const filename = error.stack ? (error.stack.split('\n')[1] || '').trim() : ''; const component = context.application?.component || ''; const route = context.application?.route || ''; // Create a composite string of key error properties const fingerprintParts = [ error.name || 'Error', error.message, filename, component, // Include component for better deduplication in UI errors route // Include route for context ].filter(Boolean); // Remove empty values // Create a fingerprint string and hash it const fingerprintStr = fingerprintParts.join('::'); // Use built-in btoa for simplicity if available (window), or a simple hash if (typeof btoa === 'function') { return btoa(fingerprintStr).slice(0, 32); } else { // Simple hash function for Node.js environments let hash = 0; for (let i = 0; i < fingerprintStr.length; i++) { const char = fingerprintStr.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash).toString(36); } } catch (_unused) { // Fallback to timestamp + random if fingerprinting fails return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } } /** * Categorize an error based on its properties */ function categorizeError(error, context) { const message = error.message.toLowerCase(); const name = error.name.toLowerCase(); // Network errors if (message.includes('network') || message.includes('fetch') || message.includes('timeout') || name.includes('network')) { return ErrorCategory.NETWORK; } // Authentication errors if (message.includes('unauthorized') || message.includes('auth') || message.includes('login') || message.includes('token')) { return ErrorCategory.AUTHENTICATION; } // Validation errors if (message.includes('validation') || message.includes('invalid') || message.includes('required') || name.includes('validation')) { return ErrorCategory.VALIDATION; } // Permission errors if (message.includes('permission') || message.includes('forbidden') || message.includes('access denied')) { return ErrorCategory.PERMISSION; } // CMS related errors if (message.includes('cms') || message.includes('content') || context.application?.component?.toLowerCase().includes('cms')) { return ErrorCategory.CMS; } // UI/Component errors if (message.includes('render') || message.includes('component') || name.includes('react') || name.includes('component')) { return ErrorCategory.UI; } // Runtime errors if (name.includes('reference') || name.includes('type') || name.includes('syntax')) { return ErrorCategory.RUNTIME; } return ErrorCategory.UNKNOWN; } /** * Determine error severity based on message content and context */ function determineSeverity(error) { // Check for explicit severity markers in message const message = error.message.toLowerCase(); if (message.includes('fatal') || message.includes('crash')) { return ErrorSeverity.FATAL; } if (message.includes('critical')) { return ErrorSeverity.CRITICAL; } // Handle warnings specifically if (message.includes('warning') || message.includes('deprecated')) { return ErrorSeverity.WARNING; } // Handle low severity issues if (message.includes('minor') || message.includes('ui glitch') || message.includes('cosmetic')) { return ErrorSeverity.LOW; } // Default is ERROR return ErrorSeverity.ERROR; } /** * Gather environment context */ function getEnvironmentContext() { return { environment: process.env.NODE_ENV || 'development', version: process.env.npm_package_version || '0.0.0', buildId: process.env.VERCEL_GIT_COMMIT_SHA || process.env.BUILD_ID, featureFlags: globalThis.__FEATURE_FLAGS__ || {}, experiments: globalThis.__EXPERIMENTS__ || {}, }; } /** * Gather user context from available sources */ function getUserContext() { const context = {}; if (typeof window !== 'undefined') { context.userAgent = navigator.userAgent; // Try to get user info from common storage locations try { const userData = localStorage.getItem('user') || sessionStorage.getItem('user'); if (userData) { const user = JSON.parse(userData); context.userId = user.id; context.role = user.role; context.sessionId = user.sessionId; } } catch (e) { // Ignore parsing errors } } return context; } /** * Gather application context */ function getApplicationContext() { const context = {}; if (typeof window !== 'undefined') { context.route = window.location.pathname; // Get previous route from history if available const historyState = window.history.state; if (historyState?.previousRoute) { context.previousRoute = historyState.previousRoute; } // Try to get recent interactions from a global tracker const interactions = globalThis.__USER_INTERACTIONS__; if (interactions && Array.isArray(interactions)) { context.userInteractions = interactions.slice(-10); // Last 10 interactions } } return context; } // ============================================================================ // MONITORING SERVICE INTEGRATIONS // ============================================================================ // MOCK SENTRY WHEN NOT AVAILABLE let Sentry; try { Sentry = require('@sentry/browser'); } catch (e) { // Create mock Sentry for tests Sentry = { init: jest.fn(), captureException: jest.fn(), withScope: jest.fn((cb) => cb({ setTag: jest.fn(), setLevel: jest.fn(), setUser: jest.fn() })), setTag: jest.fn(), setUser: jest.fn(), setContext: jest.fn(), Severity: { Fatal: 'fatal', Critical: 'critical', Error: 'error', Warning: 'warning', Log: 'log', Info: 'info', Debug: 'debug', }, }; } /** * Sentry monitoring service integration */ export class SentryMonitoringService { constructor() { this.name = 'sentry'; this.initialized = false; } async initialize(config) { try { Sentry.init({ dsn: config.dsn, environment: config.environment, release: config.release, }); this.initialized = true; } catch (e) { console.error('Failed to initialize Sentry:', e); } } async reportError(entry) { if (!this.isAvailable()) return; try { Sentry.withScope((scope) => { // Set tags scope.setTag('category', entry.category); scope.setTag('fingerprint', entry.fingerprint); scope.setLevel(this.mapSeverityToSentryLevel(entry.severity)); // Set user context if available if (entry.context.user) { scope.setUser(entry.context.user); } // Set additional context if (entry.context.application) { Sentry.setContext('application', entry.context.application); } if (entry.context.performance) { Sentry.setContext('performance', entry.context.performance); } if (entry.context.custom) { Sentry.setContext('custom', entry.context.custom); } // Capture the exception Sentry.captureException(new Error(entry.message)); }); } catch (e) { console.error('Failed to report error to Sentry:', e); } } async reportErrors(entries) { for (const entry of entries) { await this.reportError(entry); } } setUserContext(context) { try { Sentry.setUser(context); } catch (e) { console.error('Failed to set user context in Sentry:', e); } } setCustomContext(context) { try { Sentry.setContext('custom', context); } catch (e) { console.error('Failed to set custom context in Sentry:', e); } } isAvailable() { return this.initialized; } mapSeverityToSentryLevel(severity) { switch (severity) { case ErrorSeverity.FATAL: return Sentry.Severity.Fatal; case ErrorSeverity.CRITICAL: return Sentry.Severity.Critical; case ErrorSeverity.ERROR: return Sentry.Severity.Error; case ErrorSeverity.WARNING: return Sentry.Severity.Warning; case ErrorSeverity.LOW: return Sentry.Severity.Info; default: return Sentry.Severity.Error; } } } /** * Console monitoring service (for development and fallback) */ export class ConsoleMonitoringService { constructor() { this.name = 'console'; } async initialize() { // No initialization needed for console } async reportError(entry) { const logMethod = this.getConsoleMethod(entry.severity); logMethod(`[${entry.severity.toUpperCase()}] ${entry.message}`, { category: entry.category, context: entry.context, stack: entry.stack, }); } async reportErrors(entries) { console.group(`🚨 Batch Error Report (${entries.length} errors)`); for (const entry of entries) { await this.reportError(entry); } console.groupEnd(); } setUserContext(context) { console.info('User context updated:', context); } setCustomContext(context) { console.info('Custom context updated:', context); } isAvailable() { return true; } getConsoleMethod(severity) { switch (severity) { case ErrorSeverity.FATAL: case ErrorSeverity.CRITICAL: case ErrorSeverity.ERROR: return console.error; case ErrorSeverity.WARNING: return console.warn; case ErrorSeverity.LOW: default: return console.info; } } } // ============================================================================ // MAIN ERROR LOGGER CLASS // ============================================================================ /** * Centralized error logging and monitoring system */ export class ErrorLogger { constructor(config = {}) { this.errorStore = []; this.batchTimer = null; this.rateLimitCounts = new Map(); this.monitoringServices = []; this.contextProviders = []; this.config = { ...DEFAULT_CONFIG, ...config }; this.setupGlobalErrorHandlers(); this.startBatchProcessor(); } /** * Add a monitoring service integration */ addMonitoringService(service) { this.monitoringServices.push(service); } /** * Add a custom context provider */ addContextProvider(provider) { this.contextProviders.push(provider); } /** * Log an error with additional context */ async logError(error, severity, category, customContext) { if (!this.config.enabled) { return; } try { // Apply rate limiting const currentMinute = Math.floor(Date.now() / 60000); const currentCount = this.rateLimitCounts.get(currentMinute) || 0; if (currentCount >= this.config.maxErrorsPerMinute) { console.warn(`Error rate limit exceeded (${this.config.maxErrorsPerMinute} per minute)`); return; } this.rateLimitCounts.set(currentMinute, currentCount + 1); // Determine severity and category const errorSeverity = severity || determineSeverity(error); const errorCategory = category || categorizeError(error, customContext || {}); // Skip errors below minimum severity if (this.getSeverityLevel(errorSeverity) < this.getSeverityLevel(this.config.minSeverity)) { return; } // Apply custom error filters if (this.config.errorFilters) { for (const filter of this.config.errorFilters) { if (!filter(error)) { return; } } } // Gather context information const context = await this.gatherContext(customContext); // Generate a unique fingerprint for deduplication const fingerprint = createErrorFingerprint(error, context); // Check for duplicate errors const existingErrorIndex = this.errorStore.findIndex(entry => entry.fingerprint === fingerprint); if (existingErrorIndex !== -1) { // Update occurrence count for duplicate errors const existingError = this.errorStore[existingErrorIndex]; existingError.occurrenceCount = (existingError.occurrenceCount || 1) + 1; existingError.timestamp = new Date(); // Only transmit critical/fatal errors immediately on repeat if (existingError.severity === ErrorSeverity.CRITICAL || existingError.severity === ErrorSeverity.FATAL) { await this.transmitError(existingError); } return; } // Create the standardized error log entry const entry = { id: `err_${Date.now()}_${Math.random().toString(36).substr(2, 10)}`, timestamp: new Date(), severity: errorSeverity, category: errorCategory, message: error.message, name: error.name, stack: error.stack, filename: error.fileName || error.sourceURL, lineNumber: error.lineNumber || error.line, columnNumber: error.columnNumber || error.column, context, transmitted: false, occurrenceCount: 1, fingerprint, }; // Add to error store, respecting max size this.errorStore.push(entry); if (this.errorStore.length > this.config.maxStoredErrors) { // Remove oldest errors when store is full this.errorStore = this.errorStore.slice(-this.config.maxStoredErrors); } // Transmit immediately for critical/fatal errors if (entry.severity === ErrorSeverity.CRITICAL || entry.severity === ErrorSeverity.FATAL) { await this.transmitError(entry); } else if (!this.batchTimer) { // Start batch timer for non-critical errors this.startBatchProcessor(); } } catch (err) { // Prevent recursive errors console.error('Error in error logging system:', err); } } /** * Process a batch of errors to send to monitoring services */ async processBatch() { if (!this.config.enabled || this.errorStore.length === 0) { return; } try { const pendingErrors = this.errorStore .filter(entry => !entry.transmitted) .slice(0, this.config.batchSize); if (pendingErrors.length === 0) { return; } await this.transmitErrors(pendingErrors); // Mark as transmitted pendingErrors.forEach(entry => { entry.transmitted = true; }); } catch (err) { console.error('Error processing batch:', err); } finally { this.startBatchProcessor(); // Ensure we keep processing } } /** * Setup window error listeners - safely for tests */ setupGlobalErrorHandlers() { if (!this.config.enabled) { return; } try { // Skip if not in browser environment if (typeof window === 'undefined') { return; } // Capture uncaught errors if (this.config.captureWindowErrors) { window.addEventListener('error', (event) => { this.logError(event.error || new Error(event.message)); }); } // Capture unhandled promise rejections if (this.config.captureUnhandledRejections) { window.addEventListener('unhandledrejection', (event) => { const error = event.reason instanceof Error ? event.reason : new Error(`Unhandled rejection: ${String(event.reason)}`); this.logError(error, ErrorSeverity.ERROR, ErrorCategory.RUNTIME); }); } // Capture console errors if configured if (this.config.captureConsoleErrors) { const originalConsoleError = console.error; console.error = (...args) => { try { const firstArg = args[0]; if (firstArg instanceof Error) { this.logError(firstArg, undefined, undefined, { custom: { consoleError: true, arguments: args }, }); } else if (typeof firstArg === 'string') { const error = new Error(args.join(' ')); this.logError(error, ErrorSeverity.WARNING, ErrorCategory.RUNTIME, { custom: { consoleError: true, arguments: args }, }); } } catch (e) { // Prevent recursive errors } finally { originalConsoleError.apply(console, args); } }; } } catch (err) { console.error('Failed to setup global error handlers:', err); } } /** * Retrieve errors by category */ getErrorsByCategory(category) { // Important: Return a deep copy for tests return JSON.parse(JSON.stringify(this.errorStore.filter(entry => entry.category === category))); } /** * Retrieve errors by severity */ getErrorsBySeverity(severity) { // Important: Return a deep copy for tests return JSON.parse(JSON.stringify(this.errorStore.filter(entry => entry.severity === severity))); } /** * Get stored errors array (for testing) */ getStoredErrors() { // Important: Return a copy for tests return JSON.parse(JSON.stringify(this.errorStore)); } /** * Clear all stored errors */ clearStoredErrors() { this.errorStore = []; } /** * Manually flush pending errors */ async flushErrors() { const pendingErrors = this.errorStore.filter(error => !error.transmitted); if (pendingErrors.length > 0) { await this.transmitErrors(pendingErrors); } } /** * Update configuration */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } /** * Get current configuration */ getConfig() { return { ...this.config }; } // Private methods startBatchProcessor() { if (this.batchTimer) { clearInterval(this.batchTimer); } this.batchTimer = setInterval(() => { this.processBatch(); }, this.config.batchInterval); } async gatherContext(customContext) { const context = { environment: getEnvironmentContext(), user: getUserContext(), application: getApplicationContext(), custom: {}, }; if (this.config.includePerformanceContext) { context.performance = getPerformanceContext(); } // Apply custom context providers for (const provider of this.contextProviders) { try { const additional = provider(); Object.assign(context, additional); } catch (error) { console.warn('Context provider failed:', error); } } // Apply custom context if (customContext) { Object.assign(context, customContext); } return context; } async transmitError(entry) { await this.transmitErrors([entry]); } async transmitErrors(entries) { const availableServices = this.monitoringServices.filter(service => service.isAvailable()); if (availableServices.length === 0) { // Fallback to console if no services available const consoleService = new ConsoleMonitoringService(); await consoleService.reportErrors(entries); } else { // Send to all available services await Promise.allSettled(availableServices.map(service => service.reportErrors(entries))); } // Mark as transmitted entries.forEach(entry => { entry.transmitted = true; }); } getSeverityLevel(severity) { switch (severity) { case ErrorSeverity.LOW: return 1; case ErrorSeverity.WARNING: return 2; case ErrorSeverity.ERROR: return 3; case ErrorSeverity.CRITICAL: return 4; case ErrorSeverity.FATAL: return 5; default: return 0; } } /** * Log an error from ErrorBoundary */ async logErrorBoundaryError(errorDetails) { if (!errorDetails || !errorDetails.error) { console.warn('Invalid error details provided to logErrorBoundaryError'); return; } const customContext = { application: { component: 'ErrorBoundary', route: errorDetails.url, }, custom: { errorInfo: errorDetails.errorInfo, additionalContext: errorDetails.additionalContext, }, }; await this.logError(errorDetails.error, ErrorSeverity.ERROR, ErrorCategory.UI, customContext); } } // ============================================================================ // SINGLETON INSTANCE // ============================================================================ /** * Default error logger instance */ export const errorLogger = new ErrorLogger(); // ============================================================================ // CONVENIENCE FUNCTIONS // ============================================================================ /** * Log an error with automatic categorization */ export async function logError(error, customContext) { await errorLogger.logError(error, undefined, undefined, customContext); } /** * Log a warning */ export async function logWarning(message, customContext) { await errorLogger.logError(new Error(message), ErrorSeverity.WARNING, undefined, customContext); } /** * Log a critical error */ export async function logCriticalError(error, category, customContext) { await errorLogger.logError(error, ErrorSeverity.CRITICAL, category, customContext); } /** * Create a custom error logger with specific configuration */ export function createErrorLogger(config) { return new ErrorLogger(config); } export default errorLogger;