/** * ErrorHandlingService.js - Centralized error handling for the feed block */ class ErrorHandler { constructor(options = {}) { this.options = { apiUrl: '', logToServer: true, displayNotifications: true, notificationDuration: 5000, retryEnabled: true, maxRetries: 3, ...options }; this.retryCount = 0; } /** * Handle API errors * @param {Error} error - The error object * @param {Object} context - Additional context information * @param {Function} retryCallback - Function to retry the operation * @returns {Promise} - Result of error handling */ async log(error, context = {}, retryCallback = null) { // Log error to console console.error('API Error:', error, context); // Determine error type and message const errorType = this.getErrorType(error); const errorMessage = this.getErrorMessage(error, errorType); // Log to server if enabled if (this.options.logToServer) { await this.logErrorToServer(errorType, errorMessage, context); } // Handle specific error types switch (errorType) { case 'network': // Check if we should retry if (this.options.retryEnabled && this.retryCount < this.options.maxRetries && retryCallback) { this.retryCount++; return this.retryWithBackoff(retryCallback); } break; case 'auth': // Handle authentication errors - possibly redirect to login this.handleAuthError(); break; case 'rate_limit': // Handle rate limiting return this.handleRateLimitError(retryCallback); case 'server': // Server errors may be temporary if (this.options.retryEnabled && this.retryCount < this.options.maxRetries && retryCallback) { this.retryCount++; return this.retryWithBackoff(retryCallback); } break; } // Display error notification if enabled if (this.options.displayNotifications) { this.displayErrorNotification(errorMessage, errorType, retryCallback); } // Reset retry count if we're not retrying if (!retryCallback || !this.options.retryEnabled) { this.retryCount = 0; } // Return standardized error object return { success: false, error: errorType, message: errorMessage, context }; } /** * Get error type based on error object */ getErrorType(error) { if (error.name === 'AbortError') { return 'timeout'; } if (!navigator.onLine) { return 'offline'; } if (error.response) { const status = error.response.status; if (status >= 400 && status < 500) { if (status === 401 || status === 403) { return 'auth'; } if (status === 429) { return 'rate_limit'; } return 'client'; } if (status >= 500) { return 'server'; } } return 'network'; } /** * Get user-friendly error message */ getErrorMessage(error, type) { const defaultMessages = { network: "We couldn't connect to the server. Please check your connection and try again.", timeout: "The request took too long to complete. Please try again.", offline: "You appear to be offline. Please check your internet connection.", auth: "Your session may have expired. Please log in again.", rate_limit: "You've made too many requests. Please wait a moment and try again.", server: "We're experiencing technical difficulties. Please try again later.", client: "Something went wrong with your request. Please try again.", unknown: "An unexpected error occurred. Please try again." }; // Try to get message from error object if (error.response && error.response.data && error.response.data.message) { return error.response.data.message; } if (error.message) { return error.message; } // Fall back to default message return defaultMessages[type] || defaultMessages.unknown; } /** * Log error to server with enhanced context */ async logErrorToServer(type, message, context) { try { if (!this.options.apiUrl) return; // Enhanced context with component tracking const enhancedContext = { ...context, url: window.location.href, pathname: window.location.pathname, userAgent: navigator.userAgent, timestamp: new Date().toISOString(), viewport: `${window.innerWidth}x${window.innerHeight}`, component: context.component || this.extractComponentFromStack(context.stack), method: context.method || this.extractMethodFromStack(context.stack), stack: context.stack || (context.error?.stack), isLoggedIn: window.auth.isAuthenticated(), source: 'frontend' }; const data = new FormData(); data.append('error_type', type); data.append('message', message); data.append('context', JSON.stringify(enhancedContext)); await fetch(`${this.options.apiUrl}errors/log`, { method: 'POST', headers: { 'X-WP-Nonce': window.auth.getNonce() }, body: data }); } catch (e) { console.warn('Failed to log error to server', e); } } /** * Extract component name from error stack */ extractComponentFromStack(stack) { if (!stack) return 'Unknown'; // Try to extract class/component name from stack trace const match = stack.match(/at\s+(\w+)\./); return match ? match[1] : 'Unknown'; } /** * Extract method name from error stack */ extractMethodFromStack(stack) { if (!stack) return null; // Try to extract method name const match = stack.match(/at\s+\w+\.(\w+)\s+/); return match ? match[1] : null; } /** * Display error notification */ displayErrorNotification(message, type, retryCallback) { // Use WordPress notification system if available if (window.jvbNotifications) { const actions = []; // Add retry action if callback provided if (retryCallback) { actions.push({ label: 'Try Again', icon: 'refresh', action: retryCallback }); } window.jvbNotifications.queuePopupNotification({ type: 'error', message: message, icon: 'alert', priority: 'high', displayDuration: this.options.notificationDuration, actions: actions }); return; } // Fallback to basic alert if notification system not available alert(message); } /** * Handle authentication errors */ handleAuthError() { // Redirect to login page if user isn't logged in if (window.jvbSettings && window.jvbSettings.loginUrl) { window.location.href = window.jvbSettings.loginUrl; return; } // Or reload the page to refresh session window.location.reload(); } /** * Handle rate limit errors */ async handleRateLimitError(retryCallback) { // Wait for escalating periods before retrying const waitTime = 2000 * (this.retryCount + 1); await new Promise(resolve => setTimeout(resolve, waitTime)); if (retryCallback) { this.retryCount++; return retryCallback(); } } /** * Retry with exponential backoff */ async retryWithBackoff(callback) { const backoffTime = Math.min(1000 * Math.pow(2, this.retryCount), 10000); // Wait before retrying await new Promise(resolve => setTimeout(resolve, backoffTime)); // Try again return callback(); } /** * Reset retry counter */ resetRetryCount() { this.retryCount = 0; } } document.addEventListener('DOMContentLoaded', async function () { window.auth.subscribe((event) => { if (event === 'auth-loaded') { window.jvbError = new ErrorHandler({ api: jvbSettings.api, logToServer: true, displayNotifications: true, notificationDuration: 5000, retryEnabled: true, maxRetries: 3 }); } }); });