/**
|
* 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 = `
|
<h2>Help Us Improve</h2>
|
<p>We encountered an error. Would you like to tell us what happened?</p>
|
<form method="dialog" data-save="error">
|
<textarea placeholder="What were you trying to do when this error occurred?"></textarea>
|
<div class="actions">
|
<button value="cancel">Skip</button>
|
<button value="submit" class="primary">Send Feedback</button>
|
</div>
|
</form>
|
`;
|
|
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
|
});
|
});
|