Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
223
resources/js/core/ModuleErrorBoundary.js
Normal file
223
resources/js/core/ModuleErrorBoundary.js
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Error Boundary System für Module
|
||||
* Bietet Schutz vor Module-Crashes und Recovery-Mechanismen
|
||||
*/
|
||||
import { Logger } from './logger.js';
|
||||
|
||||
export class ModuleErrorBoundary {
|
||||
constructor() {
|
||||
this.crashedModules = new Set();
|
||||
this.recoveryAttempts = new Map();
|
||||
this.maxRecoveryAttempts = 3;
|
||||
this.recoveryDelay = 1000; // 1 second
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a module with error handling
|
||||
* @param {Object} module - The module to wrap
|
||||
* @param {string} moduleName - Name of the module for logging
|
||||
* @returns {Proxy} - Protected module instance
|
||||
*/
|
||||
wrapModule(module, moduleName) {
|
||||
if (!module || typeof module !== 'object') {
|
||||
Logger.warn(`[ErrorBoundary] Cannot wrap non-object module: ${moduleName}`);
|
||||
return module;
|
||||
}
|
||||
|
||||
return new Proxy(module, {
|
||||
get: (target, prop, receiver) => {
|
||||
const originalValue = target[prop];
|
||||
|
||||
// If property is not a function, return as-is
|
||||
if (typeof originalValue !== 'function') {
|
||||
return originalValue;
|
||||
}
|
||||
|
||||
// Check if property is non-configurable - return original if so
|
||||
const descriptor = Object.getOwnPropertyDescriptor(target, prop);
|
||||
if (descriptor && !descriptor.configurable) {
|
||||
return originalValue;
|
||||
}
|
||||
|
||||
// Return wrapped function with error handling
|
||||
return (...args) => {
|
||||
try {
|
||||
const result = originalValue.apply(target, args);
|
||||
|
||||
// Handle async functions
|
||||
if (result && typeof result.catch === 'function') {
|
||||
return result.catch(error => {
|
||||
this.handleModuleError(error, moduleName, prop, args);
|
||||
return this.getRecoveryValue(moduleName, prop);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.handleModuleError(error, moduleName, prop, args);
|
||||
return this.getRecoveryValue(moduleName, prop);
|
||||
}
|
||||
};
|
||||
},
|
||||
getOwnPropertyDescriptor: (target, prop) => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(target, prop);
|
||||
|
||||
// For non-configurable properties, return original descriptor
|
||||
if (descriptor && !descriptor.configurable) {
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
return descriptor;
|
||||
},
|
||||
has: (target, prop) => {
|
||||
return prop in target;
|
||||
},
|
||||
ownKeys: (target) => {
|
||||
return Object.getOwnPropertyNames(target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles module errors with recovery logic
|
||||
* @param {Error} error - The error that occurred
|
||||
* @param {string} moduleName - Name of the failing module
|
||||
* @param {string} method - Method that failed
|
||||
* @param {Array} args - Arguments passed to the method
|
||||
*/
|
||||
handleModuleError(error, moduleName, method, args) {
|
||||
const errorKey = `${moduleName}.${method}`;
|
||||
|
||||
Logger.error(`[ErrorBoundary] Module ${moduleName} crashed in ${method}():`, error);
|
||||
|
||||
// Track crashed modules
|
||||
this.crashedModules.add(moduleName);
|
||||
|
||||
// Track recovery attempts
|
||||
const attempts = this.recoveryAttempts.get(errorKey) || 0;
|
||||
this.recoveryAttempts.set(errorKey, attempts + 1);
|
||||
|
||||
// Emit custom event for external handling
|
||||
window.dispatchEvent(new CustomEvent('module-error', {
|
||||
detail: {
|
||||
moduleName,
|
||||
method,
|
||||
error: error.message,
|
||||
args,
|
||||
attempts: attempts + 1
|
||||
}
|
||||
}));
|
||||
|
||||
// Attempt recovery if under limit
|
||||
if (attempts < this.maxRecoveryAttempts) {
|
||||
this.scheduleRecovery(moduleName, method, args);
|
||||
} else {
|
||||
Logger.error(`[ErrorBoundary] Module ${moduleName} exceeded recovery attempts. Marking as permanently failed.`);
|
||||
this.markModuleAsPermanentlyFailed(moduleName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a recovery attempt for a failed module method
|
||||
* @param {string} moduleName - Name of the module
|
||||
* @param {string} method - Method to retry
|
||||
* @param {Array} args - Original arguments
|
||||
*/
|
||||
scheduleRecovery(moduleName, method, args) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
Logger.info(`[ErrorBoundary] Attempting recovery for ${moduleName}.${method}()`);
|
||||
// This would need module registry integration
|
||||
// For now, just log the attempt
|
||||
} catch (recoveryError) {
|
||||
Logger.error(`[ErrorBoundary] Recovery failed for ${moduleName}.${method}():`, recoveryError);
|
||||
}
|
||||
}, this.recoveryDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a safe fallback value for failed module methods
|
||||
* @param {string} moduleName - Name of the module
|
||||
* @param {string} method - Method that failed
|
||||
* @returns {*} - Safe fallback value
|
||||
*/
|
||||
getRecoveryValue(moduleName, method) {
|
||||
// Return safe defaults based on common method names
|
||||
switch (method) {
|
||||
case 'init':
|
||||
case 'destroy':
|
||||
case 'update':
|
||||
case 'render':
|
||||
return Promise.resolve();
|
||||
case 'getData':
|
||||
case 'getConfig':
|
||||
return {};
|
||||
case 'isEnabled':
|
||||
case 'isActive':
|
||||
return false;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a module as permanently failed
|
||||
* @param {string} moduleName - Name of the module
|
||||
*/
|
||||
markModuleAsPermanentlyFailed(moduleName) {
|
||||
window.dispatchEvent(new CustomEvent('module-permanent-failure', {
|
||||
detail: { moduleName }
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets health status of all modules
|
||||
* @returns {Object} - Health status report
|
||||
*/
|
||||
getHealthStatus() {
|
||||
return {
|
||||
totalCrashedModules: this.crashedModules.size,
|
||||
crashedModules: Array.from(this.crashedModules),
|
||||
recoveryAttempts: Object.fromEntries(this.recoveryAttempts),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets error tracking for a module (useful for hot reload)
|
||||
* @param {string} moduleName - Name of the module to reset
|
||||
*/
|
||||
resetModule(moduleName) {
|
||||
this.crashedModules.delete(moduleName);
|
||||
|
||||
// Remove all recovery attempts for this module
|
||||
for (const [key] of this.recoveryAttempts) {
|
||||
if (key.startsWith(`${moduleName}.`)) {
|
||||
this.recoveryAttempts.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info(`[ErrorBoundary] Reset error tracking for module: ${moduleName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all error tracking
|
||||
*/
|
||||
reset() {
|
||||
this.crashedModules.clear();
|
||||
this.recoveryAttempts.clear();
|
||||
Logger.info('[ErrorBoundary] Reset all error tracking');
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
export const moduleErrorBoundary = new ModuleErrorBoundary();
|
||||
|
||||
// Global error handlers
|
||||
window.addEventListener('error', (event) => {
|
||||
Logger.error('[Global] Unhandled error:', event.error || event.message);
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
Logger.error('[Global] Unhandled promise rejection:', event.reason);
|
||||
});
|
||||
Reference in New Issue
Block a user