/** * 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 */ async logErrorToServer(type, message, context) { try { if (!this.options.apiUrl) return; const data = new FormData(); data.append('error_type', type); data.append('message', message); data.append('context', JSON.stringify({ ...context, url: window.location.href, userAgent: navigator.userAgent, timestamp: new Date().toISOString() })); // Use fetch with no-cors to ensure this always succeeds // even if there are CORS issues await fetch(`${this.options.apiUrl}errors/log`, { method: 'POST', headers: { 'X-WP-Nonce': window.feedSettings?.nonce || '' }, body: data }); } catch (e) { // Silently fail - we don't want errors in error reporting console.warn('Failed to log error to server', e); } } /** * 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.feedSettings && window.feedSettings.loginUrl) { window.location.href = window.feedSettings.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); // Display retry notification if (this.options.displayNotifications) { this.displayRetryNotification(backoffTime); } // Wait before retrying await new Promise(resolve => setTimeout(resolve, backoffTime)); // Try again return callback(); } /** * Display retry notification */ displayRetryNotification(backoffTime) { if (window.jvbNotifications) { window.jvbNotifications.queuePopupNotification({ type: 'info', message: `Retrying in ${backoffTime/1000} seconds...`, icon: 'refresh', priority: 'medium', displayDuration: backoffTime }); } } /** * Reset retry counter */ resetRetryCount() { this.retryCount = 0; } /** * Handle user feedback for errors */ collectUserFeedback(errorInfo) { // Create a modal for collecting feedback const modal = document.createElement('dialog'); modal.className = 'error-feedback-modal'; modal.innerHTML = `

Help Us Improve

We encountered an error. Would you like to tell us what happened?

`; document.body.appendChild(modal); return new Promise((resolve) => { modal.addEventListener('close', () => { const feedback = modal.returnValue === 'submit' ? modal.querySelector('textarea').value : null; document.body.removeChild(modal); resolve(feedback); }); modal.showModal(); }); } /** * Handle global errors */ setupGlobalErrorHandling() { // Handle uncaught errors window.addEventListener('error', event => { this.log( event.error || new Error(event.message), { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, type: 'global_error' } ); // Don't prevent default - let browser show its own error if needed }); // Handle unhandled promise rejections window.addEventListener('unhandledrejection', event => { this.log( event.reason, { type: 'unhandled_promise', message: event.reason?.message || 'Unhandled promise rejection' } ); }); } } document.addEventListener('DOMContentLoaded', function () { window.jvbError = new ErrorHandler({ api: jvbSettings.api, logToServer: true, displayNotifications: true, notificationDuration: 5000, retryEnabled: true, maxRetries: 3 }); });