This commit is contained in:
2026-01-06 13:21:55 +01:00
parent c55ef731a1
commit 589b55a87e
7 changed files with 763 additions and 125 deletions

View File

@@ -25,6 +25,9 @@ export interface ApiErrorContext {
troubleshooting?: string;
source?: string;
componentStack?: string;
isRetryable?: boolean;
isConnectivity?: boolean;
developerHint?: string;
}
export class ApiError extends Error {

View File

@@ -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<T>(path: string, options?: BaseApiClientOptions): Promise<T> {

View File

@@ -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,
});
}
}
/**

View File

@@ -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 });
}
}

View File

@@ -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<string, unknown>): 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<string, unknown> = {}): 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);
}
}

View File

@@ -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<LogLevel, string> = {
debug: '#888888',
info: '#00aaff',
warn: '#ffaa00',
error: '#ff4444',
};
private readonly EMOJIS: Record<LogLevel, string> = {
debug: '🐛',
info: '',
warn: '⚠️',
error: '❌',
};
private readonly PREFIXES: Record<LogLevel, string> = {
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);
}
}