From 589b55a87e272902a0e58a6ac351c7f4716e66f6 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Tue, 6 Jan 2026 13:21:55 +0100 Subject: [PATCH] logging --- apps/website/lib/api/base/ApiError.ts | 3 + apps/website/lib/api/base/BaseApiClient.ts | 50 +- .../lib/infrastructure/ApiRequestLogger.ts | 81 +-- .../infrastructure/ConsoleErrorReporter.ts | 8 + .../lib/infrastructure/GlobalErrorHandler.ts | 134 ++--- .../infrastructure/logging/ConsoleLogger.ts | 86 ++- plans/UNIFIED_LOGGING_PLAN.md | 526 ++++++++++++++++++ 7 files changed, 763 insertions(+), 125 deletions(-) create mode 100644 apps/website/lib/infrastructure/ConsoleErrorReporter.ts create mode 100644 plans/UNIFIED_LOGGING_PLAN.md diff --git a/apps/website/lib/api/base/ApiError.ts b/apps/website/lib/api/base/ApiError.ts index 3dd931bef..1afadc0a2 100644 --- a/apps/website/lib/api/base/ApiError.ts +++ b/apps/website/lib/api/base/ApiError.ts @@ -25,6 +25,9 @@ export interface ApiErrorContext { troubleshooting?: string; source?: string; componentStack?: string; + isRetryable?: boolean; + isConnectivity?: boolean; + developerHint?: string; } export class ApiError extends Error { diff --git a/apps/website/lib/api/base/BaseApiClient.ts b/apps/website/lib/api/base/BaseApiClient.ts index 212a46d52..5bc57df26 100644 --- a/apps/website/lib/api/base/BaseApiClient.ts +++ b/apps/website/lib/api/base/BaseApiClient.ts @@ -143,11 +143,43 @@ export class BaseApiClient { retryCount, // Add helpful context for developers troubleshooting: this.getTroubleshootingContext(error, path), + isRetryable: this.isRetryableError(errorType), + isConnectivity: errorType === 'NETWORK_ERROR' || errorType === 'TIMEOUT_ERROR', + developerHint: this.getDeveloperHint(error, path, method), }, error ); } + /** + * Check if error type is retryable + */ + private isRetryableError(errorType: ApiErrorType): boolean { + const retryableTypes: ApiErrorType[] = [ + 'NETWORK_ERROR', + 'SERVER_ERROR', + 'RATE_LIMIT_ERROR', + 'TIMEOUT_ERROR', + ]; + return retryableTypes.includes(errorType); + } + + /** + * Get developer-friendly hint for troubleshooting + */ + private getDeveloperHint(error: Error, path: string, method: string): string { + if (error.message.includes('fetch failed') || error.message.includes('Failed to fetch')) { + return 'Check if API server is running and CORS is configured correctly'; + } + if (error.message.includes('timeout')) { + return 'Request timed out - consider increasing timeout or checking network'; + } + if (error.message.includes('ECONNREFUSED')) { + return 'Connection refused - verify API server address and port'; + } + return 'Review network connection and API endpoint configuration'; + } + /** * Get troubleshooting context for network errors */ @@ -359,17 +391,25 @@ export class BaseApiClient { const severity = error.getSeverity(); const message = error.getDeveloperMessage(); - // Log based on severity + // Enhanced context for better debugging + const enhancedContext = { + ...error.context, + severity, + isRetryable: error.isRetryable(), + isConnectivity: error.isConnectivityIssue(), + }; + + // Use appropriate log level if (severity === 'error') { - this.logger.error(message, error, error.context); + this.logger.error(message, error, enhancedContext); } else if (severity === 'warn') { - this.logger.warn(message, error.context); + this.logger.warn(message, enhancedContext); } else { - this.logger.info(message, error.context); + this.logger.info(message, enhancedContext); } // Report to error tracking - this.errorReporter.report(error, error.context); + this.errorReporter.report(error, enhancedContext); } protected get(path: string, options?: BaseApiClientOptions): Promise { diff --git a/apps/website/lib/infrastructure/ApiRequestLogger.ts b/apps/website/lib/infrastructure/ApiRequestLogger.ts index 75a0da623..8133e0487 100644 --- a/apps/website/lib/infrastructure/ApiRequestLogger.ts +++ b/apps/website/lib/infrastructure/ApiRequestLogger.ts @@ -177,12 +177,13 @@ export class ApiRequestLogger { this.activeRequests.set(id, log); if (this.options.logToConsole) { - console.groupCollapsed(`%c[API REQUEST] ${method} ${url}`, 'color: #00aaff; font-weight: bold;'); - console.log('Request ID:', id); - console.log('Timestamp:', timestamp); - if (headers) console.log('Headers:', log.headers); - if (body && this.options.logBodies) console.log('Body:', log.body); - console.groupEnd(); + // Use enhanced logger for beautiful output + this.logger.debug(`API Request: ${method} ${url}`, { + requestId: id, + timestamp, + headers: this.options.logBodies ? log.headers : '[headers hidden]', + body: this.options.logBodies ? log.body : '[body hidden]', + }); } return id; @@ -209,15 +210,19 @@ export class ApiRequestLogger { this.addToHistory(log); if (this.options.logToConsole) { - const statusColor = response.ok ? '#00ff88' : '#ff4444'; - console.groupCollapsed(`%c[API RESPONSE] ${log.method} ${log.url} - ${response.status}`, `color: ${statusColor}; font-weight: bold;`); - console.log('Request ID:', id); - console.log('Duration:', `${duration.toFixed(2)}ms`); - console.log('Status:', `${response.status} ${response.statusText}`); - if (this.options.logResponses) { - console.log('Response Body:', log.response.body); + const isSuccess = response.ok; + const context = { + requestId: id, + duration: `${duration.toFixed(2)}ms`, + status: `${response.status} ${response.statusText}`, + ...(this.options.logResponses && { body: log.response.body }), + }; + + if (isSuccess) { + this.logger.info(`API Response: ${log.method} ${log.url}`, context); + } else { + this.logger.warn(`API Response: ${log.method} ${log.url}`, context); } - console.groupEnd(); } } @@ -240,26 +245,40 @@ export class ApiRequestLogger { this.addToHistory(log); if (this.options.logToConsole) { - console.groupCollapsed(`%c[API ERROR] ${log.method} ${log.url}`, 'color: #ff4444; font-weight: bold;'); - console.log('Request ID:', id); - console.log('Duration:', `${duration.toFixed(2)}ms`); - console.log('Error:', error.message); - console.log('Type:', error.name); - if (error.stack) { - console.log('Stack:', error.stack); + const isNetworkError = error.message.includes('fetch') || + error.message.includes('Failed to fetch') || + error.message.includes('NetworkError'); + + const context = { + requestId: id, + duration: `${duration.toFixed(2)}ms`, + errorType: error.name, + ...(process.env.NODE_ENV === 'development' && error.stack ? { stack: error.stack } : {}), + }; + + // Use warn level for network errors (expected), error level for others + if (isNetworkError) { + this.logger.warn(`API Network Error: ${log.method} ${log.url}`, context); + } else { + this.logger.error(`API Error: ${log.method} ${log.url}`, error, context); } - console.groupEnd(); } - // Report to global error handler - const globalHandler = getGlobalErrorHandler(); - globalHandler.report(error, { - source: 'api_request', - url: log.url, - method: log.method, - duration, - requestId: id, - }); + // Don't report network errors to external services (they're expected) + const isNetworkError = error.message.includes('fetch') || + error.message.includes('Failed to fetch') || + error.message.includes('NetworkError'); + + if (!isNetworkError) { + const globalHandler = getGlobalErrorHandler(); + globalHandler.report(error, { + source: 'api_request', + url: log.url, + method: log.method, + duration, + requestId: id, + }); + } } /** diff --git a/apps/website/lib/infrastructure/ConsoleErrorReporter.ts b/apps/website/lib/infrastructure/ConsoleErrorReporter.ts new file mode 100644 index 000000000..e3527b68d --- /dev/null +++ b/apps/website/lib/infrastructure/ConsoleErrorReporter.ts @@ -0,0 +1,8 @@ +import { ErrorReporter } from '../interfaces/ErrorReporter'; + +export class ConsoleErrorReporter implements ErrorReporter { + report(error: Error, context?: unknown): void { + const timestamp = new Date().toISOString(); + console.error(`[${timestamp}] Error reported:`, error.message, { error, context }); + } +} \ No newline at end of file diff --git a/apps/website/lib/infrastructure/GlobalErrorHandler.ts b/apps/website/lib/infrastructure/GlobalErrorHandler.ts index 2daa00501..9f666bdce 100644 --- a/apps/website/lib/infrastructure/GlobalErrorHandler.ts +++ b/apps/website/lib/infrastructure/GlobalErrorHandler.ts @@ -112,6 +112,18 @@ export class GlobalErrorHandler { return; } + // Check if this is a network/CORS error (expected in some cases) + if (error instanceof TypeError && error.message.includes('fetch')) { + this.logger.warn('Network error detected', { + type: 'network_error', + message: error.message, + url: event.filename, + line: event.lineno, + col: event.colno, + }); + return; // Don't prevent default for network errors + } + const enhancedContext = this.captureEnhancedContext('window_error', { filename: event.filename, lineno: event.lineno, @@ -119,14 +131,13 @@ export class GlobalErrorHandler { message: event.message, }); - // Log with maximum detail + // Log with appropriate detail this.logErrorWithMaximumDetail(error, enhancedContext); // Store in history this.addToHistory(error, enhancedContext); - - // Report to external if enabled + // Report to external if enabled (but not for network errors) if (this.options.reportToExternal) { this.reportToExternal(error, enhancedContext); } @@ -154,19 +165,27 @@ export class GlobalErrorHandler { return; } + // Check if this is a network error (expected) + if (error instanceof TypeError && error.message.includes('fetch')) { + this.logger.warn('Unhandled promise rejection - network error', { + type: 'network_error', + message: error.message, + }); + return; + } + const enhancedContext = this.captureEnhancedContext('unhandled_promise', { promise: event.promise, reason: typeof error === 'string' ? error : error?.message || 'Unknown promise rejection', }); - // Log with maximum detail + // Log with appropriate detail this.logErrorWithMaximumDetail(error, enhancedContext); // Store in history this.addToHistory(error, enhancedContext); - - // Report to external if enabled + // Report to external if enabled (but not for network errors) if (this.options.reportToExternal) { this.reportToExternal(error, enhancedContext); } @@ -284,74 +303,44 @@ export class GlobalErrorHandler { } /** - * Log error with maximum detail + * Log error with appropriate detail */ private logErrorWithMaximumDetail(error: Error | ApiError, context: Record): void { if (!this.options.verboseLogging) return; const isApiError = error instanceof ApiError; - - // Group all related information - console.groupCollapsed(`%c[GLOBAL ERROR] ${error.name || 'Error'}: ${error.message}`, - 'color: #ff4444; font-weight: bold; font-size: 14px;' - ); + const isWarning = isApiError && error.getSeverity() === 'warn'; - // Error details - console.log('Error Details:', { - name: error.name, - message: error.message, - stack: error.stack, - type: isApiError ? error.type : 'N/A', - severity: isApiError ? error.getSeverity() : 'error', - retryable: isApiError ? error.isRetryable() : 'N/A', - connectivity: isApiError ? error.isConnectivityIssue() : 'N/A', - }); - - // Context information - console.log('Context:', context); - - // Enhanced stack trace - if (context.enhancedStack) { - console.log('Enhanced Stack Trace:\n' + context.enhancedStack); + if (isWarning) { + // Simplified warning output + this.logger.warn(error.message, context); + return; } - // API-specific information - if (isApiError && error.context) { - console.log('API Context:', error.context); - } - - // Environment information - console.log('Environment:', { - mode: process.env.NODE_ENV, - nextPublicMode: process.env.NEXT_PUBLIC_GRIDPILOT_MODE, - version: process.env.NEXT_PUBLIC_APP_VERSION, - buildTime: process.env.NEXT_PUBLIC_BUILD_TIME, - }); - - // Performance metrics - if (context.memory && typeof context.memory === 'object' && - 'usedJSHeapSize' in context.memory && 'totalJSHeapSize' in context.memory) { - const memory = context.memory as { usedJSHeapSize: number; totalJSHeapSize: number }; - console.log('Memory Usage:', { - used: `${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`, - total: `${(memory.totalJSHeapSize / 1024 / 1024).toFixed(2)} MB`, - }); - } - - // Network information - if (context.connection) { - console.log('Network:', context.connection); - } - - // Error history (last 5 errors) - if (this.errorHistory.length > 0) { - console.log('Recent Error History:', this.errorHistory.slice(-5)); - } - - console.groupEnd(); - - // Also log to our logger + // Full error details for actual errors this.logger.error(error.message, error, context); + + // In development, show additional details + if (process.env.NODE_ENV === 'development') { + console.groupCollapsed(`%c[ERROR DETAIL] ${error.name || 'Error'}`, 'color: #ff4444; font-weight: bold; font-size: 14px;'); + + console.log('%cError Details:', 'color: #666; font-weight: bold;', { + name: error.name, + message: error.message, + type: isApiError ? error.type : 'N/A', + severity: isApiError ? error.getSeverity() : 'error', + retryable: isApiError ? error.isRetryable() : 'N/A', + connectivity: isApiError ? error.isConnectivityIssue() : 'N/A', + }); + + console.log('%cContext:', 'color: #666; font-weight: bold;', context); + + if (isApiError && error.context?.developerHint) { + console.log('%cπŸ’‘ Developer Hint:', 'color: #00aaff; font-weight: bold;', error.context.developerHint); + } + + console.groupEnd(); + } } @@ -444,7 +433,17 @@ export class GlobalErrorHandler { report(error: Error | ApiError, additionalContext: Record = {}): void { const context = this.captureEnhancedContext('manual_report', additionalContext); - this.logErrorWithMaximumDetail(error, context); + // Check if this is a network error (don't report to external services) + const isNetworkError = error.message.includes('fetch') || + error.message.includes('Failed to fetch') || + error.message.includes('NetworkError'); + + if (isNetworkError) { + this.logger.warn(`Manual error report: ${error.message}`, context); + } else { + this.logErrorWithMaximumDetail(error, context); + } + this.addToHistory(error, context); // Auto-capture for replay in development @@ -453,7 +452,8 @@ export class GlobalErrorHandler { replaySystem.autoCapture(error, context); } - if (this.options.reportToExternal) { + // Only report non-network errors to external services + if (this.options.reportToExternal && !isNetworkError) { this.reportToExternal(error, context); } } diff --git a/apps/website/lib/infrastructure/logging/ConsoleLogger.ts b/apps/website/lib/infrastructure/logging/ConsoleLogger.ts index ed536dd3d..9e657207f 100644 --- a/apps/website/lib/infrastructure/logging/ConsoleLogger.ts +++ b/apps/website/lib/infrastructure/logging/ConsoleLogger.ts @@ -1,39 +1,81 @@ import { Logger } from '../../interfaces/Logger'; +type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + export class ConsoleLogger implements Logger { - private formatMessage(level: string, message: string, context?: unknown): string { - const timestamp = new Date().toISOString(); - const contextStr = context ? ` | ${JSON.stringify(context)}` : ''; - return `[${timestamp}] ${level.toUpperCase()}: ${message}${contextStr}`; + private readonly COLORS: Record = { + debug: '#888888', + info: '#00aaff', + warn: '#ffaa00', + error: '#ff4444', + }; + + private readonly EMOJIS: Record = { + debug: 'πŸ›', + info: 'ℹ️', + warn: '⚠️', + error: '❌', + }; + + private readonly PREFIXES: Record = { + debug: 'DEBUG', + info: 'INFO', + warn: 'WARN', + error: 'ERROR', + }; + + private shouldLog(level: LogLevel): boolean { + if (process.env.NODE_ENV === 'test') return level === 'error'; + if (process.env.NODE_ENV === 'production') return level !== 'debug'; + return true; + } + + private formatOutput(level: LogLevel, source: string, message: string, context?: unknown, error?: Error): void { + const color = this.COLORS[level]; + const emoji = this.EMOJIS[level]; + const prefix = this.PREFIXES[level]; + + console.groupCollapsed(`%c${emoji} [${source.toUpperCase()}] ${prefix}: ${message}`, `color: ${color}; font-weight: bold;`); + + console.log(`%cTimestamp:`, 'color: #666; font-weight: bold;', new Date().toISOString()); + console.log(`%cSource:`, 'color: #666; font-weight: bold;', source); + + if (context) { + console.log(`%cContext:`, 'color: #666; font-weight: bold;'); + console.dir(context, { depth: 3, colors: true }); + } + + if (error) { + console.log(`%cError Details:`, 'color: #666; font-weight: bold;'); + console.log(`%cType:`, 'color: #ff4444; font-weight: bold;', error.name); + console.log(`%cMessage:`, 'color: #ff4444; font-weight: bold;', error.message); + + if (process.env.NODE_ENV === 'development' && error.stack) { + console.log(`%cStack Trace:`, 'color: #666; font-weight: bold;'); + console.log(error.stack); + } + } + + console.groupEnd(); } debug(message: string, context?: unknown): void { - // Always log debug in development and test environments - if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { - console.debug(this.formatMessage('debug', message, context)); - } + if (!this.shouldLog('debug')) return; + this.formatOutput('debug', 'website', message, context); } info(message: string, context?: unknown): void { - // Always log info - we need transparency in all environments - console.info(this.formatMessage('info', message, context)); + if (!this.shouldLog('info')) return; + this.formatOutput('info', 'website', message, context); } warn(message: string, context?: unknown): void { - console.warn(this.formatMessage('warn', message, context)); + if (!this.shouldLog('warn')) return; + this.formatOutput('warn', 'website', message, context); } error(message: string, error?: Error, context?: unknown): void { - const errorStr = error ? ` | Error: ${error.message}` : ''; - console.error(this.formatMessage('error', message, context) + errorStr); - - // In development, also show enhanced error info - if (process.env.NODE_ENV === 'development' && error) { - console.groupCollapsed(`%c[ERROR DETAIL] ${message}`, 'color: #ff4444; font-weight: bold;'); - console.log('Error Object:', error); - console.log('Stack Trace:', error.stack); - console.log('Context:', context); - console.groupEnd(); - } + if (!this.shouldLog('error')) return; + this.formatOutput('error', 'website', message, context, error); } } \ No newline at end of file diff --git a/plans/UNIFIED_LOGGING_PLAN.md b/plans/UNIFIED_LOGGING_PLAN.md new file mode 100644 index 000000000..49fd1d13f --- /dev/null +++ b/plans/UNIFIED_LOGGING_PLAN.md @@ -0,0 +1,526 @@ +# Unified Logging Plan - Professional & Developer Friendly + +## Problem Summary + +**Current Issues:** +- Website logs are overly aggressive and verbose +- Network errors show full stack traces (looks like syntax errors) +- Multiple error formats for same issue +- Not machine-readable +- Different patterns than apps/api + +**Goal:** Create unified, professional logging that's both machine-readable AND beautiful for developers. + +## Solution Overview + +### 1. Unified Logger Interface (No Core Imports) +```typescript +// apps/website/lib/interfaces/Logger.ts +export interface Logger { + debug(message: string, context?: unknown): void; + info(message: string, context?: unknown): void; + warn(message: string, context?: unknown): void; + error(message: string, error?: Error, context?: unknown): void; +} +``` + +### 2. How Website Logging Aligns with apps/api + +**apps/api ConsoleLogger (Simple & Clean):** +```typescript +// adapters/logging/ConsoleLogger.ts +formatMessage(level: string, message: string, context?: unknown): string { + const timestamp = new Date().toISOString(); + const contextStr = context ? ` | ${JSON.stringify(context)}` : ''; + return `[${timestamp}] ${level.toUpperCase()}: ${message}${contextStr}`; +} + +// Output: [2026-01-06T12:00:00.000Z] WARN: Network error, retrying... | {"endpoint":"/auth/session"} +``` + +**apps/website ConsoleLogger (Enhanced & Developer-Friendly):** +```typescript +// apps/website/lib/infrastructure/logging/ConsoleLogger.ts +formatOutput(level: string, source: string, message: string, context?: unknown, error?: Error): void { + const color = this.COLORS[level]; + const emoji = this.EMOJIS[level]; + const prefix = this.PREFIXES[level]; + + // Same core format as apps/api, but enhanced with colors/emojis + console.groupCollapsed(`%c${emoji} [${source.toUpperCase()}] ${prefix}: ${message}`, `color: ${color}; font-weight: bold;`); + console.log(`%cTimestamp:`, 'color: #666; font-weight: bold;', new Date().toISOString()); + console.log(`%cContext:`, 'color: #666; font-weight: bold;'); + console.dir(context, { depth: 3, colors: true }); + console.groupEnd(); +} + +// Output: ⚠️ [API] NETWORK WARN: Network error, retrying... +// β”œβ”€ Timestamp: 2026-01-06T12:00:00.000Z +// β”œβ”€ Context: { endpoint: "/auth/session", ... } +``` + +**Alignment:** +- βœ… Same timestamp format (ISO 8601) +- βœ… Same log levels (debug, info, warn, error) +- βœ… Same context structure +- βœ… Same message patterns +- βœ… Website adds colors/emojis for better UX + +### 3. Unified API Client Logging Strategy +**Both apps/api and apps/website use the same patterns:** + +```typescript +// In BaseApiClient (shared logic): +private handleError(error: ApiError): void { + const severity = error.getSeverity(); + const message = error.getDeveloperMessage(); + + // Same logic for both: + if (error.context.isRetryable && error.context.retryCount > 0) { + // Network errors during retry = warn (not error) + this.logger.warn(message, error.context); + } else if (severity === 'error') { + // Final failure = error + this.logger.error(message, error, error.context); + } else { + // Other errors = warn + this.logger.warn(message, error.context); + } +} +``` + +### 4. Unified Error Classification +**Both environments use the same severity levels:** +- **error**: Critical failures (server down, auth failures, data corruption) +- **warn**: Expected errors (network timeouts, CORS, validation failures) +- **info**: Normal operations (successful retries, connectivity恒倍) +- **debug**: Detailed info (development only) + +### 5. Example: Same Error, Different Output + +**Scenario: Server down, retrying connection** + +**apps/api output:** +``` +[2026-01-06T12:00:00.000Z] WARN: [NETWORK_ERROR] GET /auth/session retry:1 | {"endpoint":"/auth/session","method":"GET","retryCount":1} +``` + +**apps/website output:** +``` +⚠️ [API] NETWORK WARN: GET /auth/session - retry:1 +β”œβ”€ Timestamp: 2026-01-06T12:00:00.000Z +β”œβ”€ Endpoint: /auth/session +β”œβ”€ Method: GET +β”œβ”€ Retry Count: 1 +└─ Hint: Check if API server is running and CORS is configured +``` + +**Key Alignment Points:** +1. **Same log level**: `warn` (not `error`) +2. **Same context**: `{endpoint, method, retryCount}` +3. **Same message pattern**: Includes retry count +4. **Same timestamp format**: ISO 8601 +5. **Website just adds**: Colors, emojis, and developer hints + +This creates a **unified logging ecosystem** where: +- Logs can be parsed the same way +- Severity levels mean the same thing +- Context structures are identical +- Website enhances for developer experience +- apps/api keeps it simple for server logs + +## Implementation Files + +### File 1: Logger Interface +**Path:** `apps/website/lib/interfaces/Logger.ts` + +```typescript +export interface Logger { + debug(message: string, context?: unknown): void; + info(message: string, context?: unknown): void; + warn(message: string, context?: unknown): void; + error(message: string, error?: Error, context?: unknown): void; +} +``` + +### File 2: ErrorReporter Interface +**Path:** `apps/website/lib/interfaces/ErrorReporter.ts` + +```typescript +export interface ErrorReporter { + report(error: Error, context?: unknown): void; +} +``` + +### File 3: Enhanced ConsoleLogger (Human-Readable Only) +**Path:** `apps/website/lib/infrastructure/logging/ConsoleLogger.ts` + +```typescript +import { Logger } from '../../interfaces/Logger'; + +export class ConsoleLogger implements Logger { + private readonly COLORS = { debug: '#888888', info: '#00aaff', warn: '#ffaa00', error: '#ff4444' }; + private readonly EMOJIS = { debug: 'πŸ›', info: 'ℹ️', warn: '⚠️', error: '❌' }; + private readonly PREFIXES = { debug: 'DEBUG', info: 'INFO', warn: 'WARN', error: 'ERROR' }; + + private shouldLog(level: string): boolean { + if (process.env.NODE_ENV === 'test') return level === 'error'; + if (process.env.NODE_ENV === 'production') return level !== 'debug'; + return true; + } + + private formatOutput(level: string, source: string, message: string, context?: unknown, error?: Error): void { + const color = this.COLORS[level]; + const emoji = this.EMOJIS[level]; + const prefix = this.PREFIXES[level]; + + console.groupCollapsed(`%c${emoji} [${source.toUpperCase()}] ${prefix}: ${message}`, `color: ${color}; font-weight: bold;`); + + console.log(`%cTimestamp:`, 'color: #666; font-weight: bold;', new Date().toISOString()); + console.log(`%cSource:`, 'color: #666; font-weight: bold;', source); + + if (context) { + console.log(`%cContext:`, 'color: #666; font-weight: bold;'); + console.dir(context, { depth: 3, colors: true }); + } + + if (error) { + console.log(`%cError Details:`, 'color: #666; font-weight: bold;'); + console.log(`%cType:`, 'color: #ff4444; font-weight: bold;', error.name); + console.log(`%cMessage:`, 'color: #ff4444; font-weight: bold;', error.message); + + if (process.env.NODE_ENV === 'development' && error.stack) { + console.log(`%cStack Trace:`, 'color: #666; font-weight: bold;'); + console.log(error.stack); + } + } + + console.groupEnd(); + } + + debug(message: string, context?: unknown): void { + if (!this.shouldLog('debug')) return; + this.formatOutput('debug', 'website', message, context); + } + + info(message: string, context?: unknown): void { + if (!this.shouldLog('info')) return; + this.formatOutput('info', 'website', message, context); + } + + warn(message: string, context?: unknown): void { + if (!this.shouldLog('warn')) return; + this.formatOutput('warn', 'website', message, context); + } + + error(message: string, error?: Error, context?: unknown): void { + if (!this.shouldLog('error')) return; + this.formatOutput('error', 'website', message, context, error); + } +} +``` + +### File 4: ConsoleErrorReporter +**Path:** `apps/website/lib/infrastructure/ConsoleErrorReporter.ts` + +```typescript +import { ErrorReporter } from '../../interfaces/ErrorReporter'; + +export class ConsoleErrorReporter implements ErrorReporter { + report(error: Error, context?: unknown): void { + const timestamp = new Date().toISOString(); + console.error(`[${timestamp}] Error reported:`, error.message, { error, context }); + } +} +``` + +### File 5: Updated BaseApiClient +**Path:** `apps/website/lib/api/base/BaseApiClient.ts` + +**Key Changes:** + +1. **Update createNetworkError:** +```typescript +private createNetworkError(error: Error, method: string, path: string, retryCount: number = 0): ApiError { + // ... existing logic ... + + return new ApiError( + message, + errorType, + { + endpoint: path, + method, + timestamp: new Date().toISOString(), + retryCount, + troubleshooting: this.getTroubleshootingContext(error, path), + isRetryable: retryableTypes.includes(errorType), + isConnectivity: errorType === 'NETWORK_ERROR' || errorType === 'TIMEOUT_ERROR', + developerHint: this.getDeveloperHint(error, path, method), + }, + error + ); +} + +private getDeveloperHint(error: Error, path: string, method: string): string { + if (error.message.includes('fetch failed') || error.message.includes('Failed to fetch')) { + return 'Check if API server is running and CORS is configured correctly'; + } + if (error.message.includes('timeout')) { + return 'Request timed out - consider increasing timeout or checking network'; + } + if (error.message.includes('ECONNREFUSED')) { + return 'Connection refused - verify API server address and port'; + } + return 'Review network connection and API endpoint configuration'; +} +``` + +2. **Update handleError:** +```typescript +private handleError(error: ApiError): void { + const severity = error.getSeverity(); + const message = error.getDeveloperMessage(); + + const enhancedContext = { + ...error.context, + severity, + isRetryable: error.isRetryable(), + isConnectivity: error.isConnectivityIssue(), + }; + + if (severity === 'error') { + this.logger.error(message, error, enhancedContext); + } else if (severity === 'warn') { + this.logger.warn(message, enhancedContext); + } else { + this.logger.info(message, enhancedContext); + } + + this.errorReporter.report(error, enhancedContext); +} +``` + +3. **Update request method logging:** +```typescript +// In catch block: +} catch (error) { + const responseTime = Date.now() - startTime; + + if (error instanceof ApiError) { + // Reduce verbosity - only log final failure + if (process.env.NODE_ENV === 'development' && requestId) { + try { + const apiLogger = getGlobalApiLogger(); + // This will use warn level for retryable errors + apiLogger.logError(requestId, error, responseTime); + } catch (e) { + // Silent fail + } + } + throw error; + } + // ... rest of error handling +} +``` + +### File 6: Updated ApiRequestLogger +**Path:** `apps/website/lib/infrastructure/ApiRequestLogger.ts` + +**Key Changes:** + +```typescript +// Update logError to use warn for network errors: +logError(id: string, error: Error, duration: number): void { + // ... existing setup ... + + const isNetworkError = error.message.includes('fetch') || + error.message.includes('Failed to fetch') || + error.message.includes('NetworkError'); + + if (this.options.logToConsole) { + const emoji = isNetworkError ? '⚠️' : '❌'; + const prefix = isNetworkError ? 'NETWORK WARN' : 'ERROR'; + const color = isNetworkError ? '#ffaa00' : '#ff4444'; + + console.groupCollapsed( + `%c${emoji} [API] ${prefix}: ${log.method} ${log.url}`, + `color: ${color}; font-weight: bold; font-size: 12px;` + ); + console.log(`%cRequest ID:`, 'color: #666; font-weight: bold;', id); + console.log(`%cDuration:`, 'color: #666; font-weight: bold;', `${duration.toFixed(2)}ms`); + console.log(`%cError:`, 'color: #666; font-weight: bold;', error.message); + console.log(`%cType:`, 'color: #666; font-weight: bold;', error.name); + + if (process.env.NODE_ENV === 'development' && error.stack) { + console.log(`%cStack:`, 'color: #666; font-weight: bold;'); + console.log(error.stack); + } + console.groupEnd(); + } + + // Don't report network errors to external services + if (!isNetworkError) { + const globalHandler = getGlobalErrorHandler(); + globalHandler.report(error, { + source: 'api_request', + url: log.url, + method: log.method, + duration, + requestId: id, + }); + } +} +``` + +### File 7: Updated GlobalErrorHandler +**Path:** `apps/website/lib/infrastructure/GlobalErrorHandler.ts` + +**Key Changes:** + +```typescript +// In handleWindowError: +private handleWindowError = (event: ErrorEvent): void => { + const error = event.error; + + // Check if this is a network/CORS error (expected in some cases) + if (error instanceof TypeError && error.message.includes('fetch')) { + this.logger.warn('Network error detected', { + type: 'network_error', + message: error.message, + url: event.filename + }); + return; // Don't prevent default for network errors + } + + // ... existing logic for other errors ... +}; + +// Update logErrorWithMaximumDetail: +private logErrorWithMaximumDetail(error: Error | ApiError, context: Record): void { + if (!this.options.verboseLogging) return; + + const isApiError = error instanceof ApiError; + const isWarning = isApiError && error.getSeverity() === 'warn'; + + if (isWarning) { + console.groupCollapsed(`%c⚠️ [WARNING] ${error.message}`, 'color: #ffaa00; font-weight: bold; font-size: 14px;'); + console.log('Context:', context); + console.groupEnd(); + return; + } + + // Full error details for actual errors + console.groupCollapsed(`%c❌ [ERROR] ${error.name || 'Error'}: ${error.message}`, 'color: #ff4444; font-weight: bold; font-size: 16px;'); + + console.log('%cError Details:', 'color: #ff4444; font-weight: bold; font-size: 14px;'); + console.table({ + Name: error.name, + Message: error.message, + Type: isApiError ? error.type : 'N/A', + Severity: isApiError ? error.getSeverity() : 'error', + Retryable: isApiError ? error.isRetryable() : 'N/A', + }); + + console.log('%cContext:', 'color: #666; font-weight: bold; font-size: 14px;'); + console.dir(context, { depth: 4, colors: true }); + + if (process.env.NODE_ENV === 'development' && error.stack) { + console.log('%cStack Trace:', 'color: #666; font-weight: bold; font-size: 14px;'); + console.log(error.stack); + } + + if (isApiError && error.context?.developerHint) { + console.log('%cπŸ’‘ Developer Hint:', 'color: #00aaff; font-weight: bold; font-size: 14px;'); + console.log(error.context.developerHint); + } + + console.groupEnd(); + this.logger.error(error.message, error, context); +} +``` + +## Expected Results + +### Before (Current - Too Verbose) +``` +[API-ERROR] [NETWORK_ERROR] Unable to connect to server. Possible CORS or network issue. GET /auth/session { + error: Error [ApiError]: Unable to connect to server. Possible CORS or network issue. + at AuthApiClient.createNetworkError (lib/api/base/BaseApiClient.ts:136:12) + at executeRequest (lib/api/base/BaseApiClient.ts:314:31) + ... +} +[USER-NOTIFICATION] Unable to connect to the server. Please check your internet connection. +[API ERROR] GET http://api:3000/auth/session + Request ID: req_1767694969495_4 + Duration: 8.00ms + Error: Unable to connect to server. Possible CORS or network issue. + Type: ApiError + Stack: ApiError: Unable to connect to server... +``` + +### After (Unified - Clean & Beautiful) + +Beautiful console output: +``` +⚠️ [API] NETWORK WARN: GET /auth/session - retry:1 +β”œβ”€ Request ID: req_123 +β”œβ”€ Duration: 8.00ms +β”œβ”€ Error: fetch failed +β”œβ”€ Type: TypeError +└─ Hint: Check if API server is running and CORS is configured +``` + +And user notification (separate): +``` +[USER-NOTIFICATION] Unable to connect to the server. Please check your internet connection. +``` + +## Benefits + +βœ… **Developer-Friendly**: Beautiful colors, emojis, and formatting +βœ… **Reduced Noise**: Appropriate severity levels prevent spam +βœ… **Unified Format**: Same patterns as apps/api +βœ… **No Core Imports**: Website remains independent +βœ… **Professional**: Industry-standard logging practices +βœ… **Clean Output**: Human-readable only, no JSON clutter + +## Testing Checklist + +1. **Server Down**: Should show warn level, no stack trace, retry attempts +2. **CORS Error**: Should show warn level with troubleshooting hint +3. **Auth Error (401)**: Should show warn level, retryable +4. **Server Error (500)**: Should show error level with full details +5. **Validation Error (400)**: Should show warn level, not retryable +6. **Successful Call**: Should show info level with duration + +## Quick Implementation + +Run these commands to implement: + +```bash +# 1. Create interfaces +mkdir -p apps/website/lib/interfaces +cat > apps/website/lib/interfaces/Logger.ts << 'EOF' +export interface Logger { + debug(message: string, context?: unknown): void; + info(message: string, context?: unknown): void; + warn(message: string, context?: unknown): void; + error(message: string, error?: Error, context?: unknown): void; +} +EOF + +# 2. Create ErrorReporter interface +cat > apps/website/lib/interfaces/ErrorReporter.ts << 'EOF' +export interface ErrorReporter { + report(error: Error, context?: unknown): void; +} +EOF + +# 3. Update ConsoleLogger (use the enhanced version above) +# 4. Update ConsoleErrorReporter (use the version above) +# 5. Update BaseApiClient (use the changes above) +# 6. Update ApiRequestLogger (use the changes above) +# 7. Update GlobalErrorHandler (use the changes above) +``` + +This single plan provides everything needed to transform the logging from verbose and confusing to professional and beautiful! 🎨 \ No newline at end of file