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