@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
JavaScript
// ============================================================================
// 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;