799 lines
25 KiB
JavaScript
799 lines
25 KiB
JavaScript
const ProgressService = require("../services/progress");
|
|
|
|
class Logger {
|
|
constructor(progressService = null) {
|
|
this.progressService = progressService || new ProgressService();
|
|
this.colors = {
|
|
reset: "\x1b[0m",
|
|
bright: "\x1b[1m",
|
|
red: "\x1b[31m",
|
|
green: "\x1b[32m",
|
|
yellow: "\x1b[33m",
|
|
blue: "\x1b[34m",
|
|
cyan: "\x1b[36m",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Formats a timestamp for console display
|
|
* @param {Date} date - The date to format
|
|
* @returns {string} Formatted timestamp string
|
|
*/
|
|
formatTimestamp(date = new Date()) {
|
|
return date
|
|
.toISOString()
|
|
.replace("T", " ")
|
|
.replace(/\.\d{3}Z$/, "");
|
|
}
|
|
|
|
/**
|
|
* Formats a console message with color and timestamp
|
|
* @param {string} level - Log level (INFO, WARN, ERROR)
|
|
* @param {string} message - Message to log
|
|
* @param {string} color - ANSI color code
|
|
* @returns {string} Formatted message
|
|
*/
|
|
formatConsoleMessage(level, message, color) {
|
|
const timestamp = this.formatTimestamp();
|
|
return `${color}[${timestamp}] ${level}:${this.colors.reset} ${message}`;
|
|
}
|
|
|
|
/**
|
|
* Logs an info message to console
|
|
* @param {string} message - Message to log
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async info(message) {
|
|
const formattedMessage = this.formatConsoleMessage(
|
|
"INFO",
|
|
message,
|
|
this.colors.cyan
|
|
);
|
|
console.log(formattedMessage);
|
|
}
|
|
|
|
/**
|
|
* Logs a warning message to console
|
|
* @param {string} message - Message to log
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async warning(message) {
|
|
const formattedMessage = this.formatConsoleMessage(
|
|
"WARN",
|
|
message,
|
|
this.colors.yellow
|
|
);
|
|
console.warn(formattedMessage);
|
|
}
|
|
|
|
/**
|
|
* Logs an error message to console
|
|
* @param {string} message - Message to log
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async error(message) {
|
|
const formattedMessage = this.formatConsoleMessage(
|
|
"ERROR",
|
|
message,
|
|
this.colors.red
|
|
);
|
|
console.error(formattedMessage);
|
|
}
|
|
|
|
/**
|
|
* Logs operation start with configuration details (Requirement 3.1)
|
|
* @param {Object} config - Configuration object
|
|
* @param {Object} schedulingContext - Optional scheduling context
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logOperationStart(config, schedulingContext = null) {
|
|
if (schedulingContext && schedulingContext.isScheduled) {
|
|
await this.info(
|
|
`Starting scheduled price update operation with configuration:`
|
|
);
|
|
await this.info(
|
|
` Scheduled Time: ${schedulingContext.scheduledTime.toLocaleString()}`
|
|
);
|
|
await this.info(
|
|
` Original Schedule Input: ${schedulingContext.originalInput}`
|
|
);
|
|
} else {
|
|
await this.info(`Starting price update operation with configuration:`);
|
|
}
|
|
|
|
await this.info(` Target Tag: ${config.targetTag}`);
|
|
await this.info(` Price Adjustment: ${config.priceAdjustmentPercentage}%`);
|
|
await this.info(` Shop Domain: ${config.shopDomain}`);
|
|
|
|
// Also log to progress file with scheduling context
|
|
await this.progressService.logOperationStart(config, schedulingContext);
|
|
}
|
|
|
|
/**
|
|
* Logs rollback operation start with configuration details (Requirements 3.1, 7.1, 8.3)
|
|
* @param {Object} config - Configuration object
|
|
* @param {Object} schedulingContext - Optional scheduling context
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logRollbackStart(config, schedulingContext = null) {
|
|
if (schedulingContext && schedulingContext.isScheduled) {
|
|
await this.info(
|
|
`Starting scheduled price rollback operation with configuration:`
|
|
);
|
|
await this.info(
|
|
` Scheduled Time: ${schedulingContext.scheduledTime.toLocaleString()}`
|
|
);
|
|
await this.info(
|
|
` Original Schedule Input: ${schedulingContext.originalInput}`
|
|
);
|
|
} else {
|
|
await this.info(`Starting price rollback operation with configuration:`);
|
|
}
|
|
|
|
await this.info(` Target Tag: ${config.targetTag}`);
|
|
await this.info(` Operation Mode: rollback`);
|
|
await this.info(` Shop Domain: ${config.shopDomain}`);
|
|
|
|
// Also log to progress file with rollback-specific format and scheduling context
|
|
try {
|
|
await this.progressService.logRollbackStart(config, schedulingContext);
|
|
} catch (error) {
|
|
// Progress logging should not block main operations
|
|
console.warn(`Warning: Failed to log to progress file: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Logs product count information (Requirement 3.2)
|
|
* @param {number} count - Number of matching products found
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logProductCount(count) {
|
|
const message = `Found ${count} product${
|
|
count !== 1 ? "s" : ""
|
|
} matching the specified tag`;
|
|
await this.info(message);
|
|
}
|
|
|
|
/**
|
|
* Logs individual product update details (Requirement 3.3)
|
|
* @param {Object} entry - Product update entry
|
|
* @param {string} entry.productTitle - Product title
|
|
* @param {string} entry.productId - Product ID
|
|
* @param {string} entry.variantId - Variant ID
|
|
* @param {number} entry.oldPrice - Original price
|
|
* @param {number} entry.newPrice - Updated price
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logProductUpdate(entry) {
|
|
const compareAtInfo = entry.compareAtPrice
|
|
? ` (Compare At: $${entry.compareAtPrice})`
|
|
: "";
|
|
const message = `${this.colors.green}✅${this.colors.reset} Updated "${entry.productTitle}" - Price: $${entry.oldPrice} → $${entry.newPrice}${compareAtInfo}`;
|
|
console.log(message);
|
|
|
|
// Also log to progress file
|
|
await this.progressService.logProductUpdate(entry);
|
|
}
|
|
|
|
/**
|
|
* Logs successful rollback operations (Requirements 3.3, 7.2, 8.3)
|
|
* @param {Object} entry - Rollback update entry
|
|
* @param {string} entry.productTitle - Product title
|
|
* @param {string} entry.productId - Product ID
|
|
* @param {string} entry.variantId - Variant ID
|
|
* @param {number} entry.oldPrice - Original price before rollback
|
|
* @param {number} entry.compareAtPrice - Compare-at price being used as new price
|
|
* @param {number} entry.newPrice - New price (same as compare-at price)
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logRollbackUpdate(entry) {
|
|
const message = `${this.colors.green}🔄${this.colors.reset} Rolled back "${entry.productTitle}" - Price: ${entry.oldPrice} → ${entry.newPrice} (from Compare At: ${entry.compareAtPrice})`;
|
|
console.log(message);
|
|
|
|
// Also log to progress file with rollback-specific format
|
|
try {
|
|
await this.progressService.logRollbackUpdate(entry);
|
|
} catch (error) {
|
|
// Progress logging should not block main operations
|
|
console.warn(`Warning: Failed to log to progress file: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Logs completion summary (Requirement 3.4)
|
|
* @param {Object} summary - Summary statistics
|
|
* @param {number} summary.totalProducts - Total products processed
|
|
* @param {number} summary.successfulUpdates - Successful updates
|
|
* @param {number} summary.failedUpdates - Failed updates
|
|
* @param {Date} summary.startTime - Operation start time
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logCompletionSummary(summary) {
|
|
await this.info("=".repeat(50));
|
|
await this.info("OPERATION COMPLETE");
|
|
await this.info("=".repeat(50));
|
|
await this.info(`Total Products Processed: ${summary.totalProducts}`);
|
|
await this.info(
|
|
`Successful Updates: ${this.colors.green}${summary.successfulUpdates}${this.colors.reset}`
|
|
);
|
|
|
|
if (summary.failedUpdates > 0) {
|
|
await this.info(
|
|
`Failed Updates: ${this.colors.red}${summary.failedUpdates}${this.colors.reset}`
|
|
);
|
|
} else {
|
|
await this.info(`Failed Updates: ${summary.failedUpdates}`);
|
|
}
|
|
|
|
if (summary.startTime) {
|
|
const duration = Math.round((new Date() - summary.startTime) / 1000);
|
|
await this.info(`Duration: ${duration} seconds`);
|
|
}
|
|
|
|
// Also log to progress file
|
|
await this.progressService.logCompletionSummary(summary);
|
|
}
|
|
|
|
/**
|
|
* Logs rollback completion summary (Requirements 3.5, 7.3, 8.3)
|
|
* @param {Object} summary - Rollback summary statistics
|
|
* @param {number} summary.totalProducts - Total products processed
|
|
* @param {number} summary.totalVariants - Total variants processed
|
|
* @param {number} summary.eligibleVariants - Variants eligible for rollback
|
|
* @param {number} summary.successfulRollbacks - Successful rollback operations
|
|
* @param {number} summary.failedRollbacks - Failed rollback operations
|
|
* @param {number} summary.skippedVariants - Variants skipped (no compare-at price)
|
|
* @param {Date} summary.startTime - Operation start time
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logRollbackSummary(summary) {
|
|
await this.info("=".repeat(50));
|
|
await this.info("ROLLBACK OPERATION COMPLETE");
|
|
await this.info("=".repeat(50));
|
|
await this.info(`Total Products Processed: ${summary.totalProducts}`);
|
|
await this.info(`Total Variants Processed: ${summary.totalVariants}`);
|
|
await this.info(`Eligible Variants: ${summary.eligibleVariants}`);
|
|
await this.info(
|
|
`Successful Rollbacks: ${this.colors.green}${summary.successfulRollbacks}${this.colors.reset}`
|
|
);
|
|
|
|
if (summary.failedRollbacks > 0) {
|
|
await this.info(
|
|
`Failed Rollbacks: ${this.colors.red}${summary.failedRollbacks}${this.colors.reset}`
|
|
);
|
|
} else {
|
|
await this.info(`Failed Rollbacks: ${summary.failedRollbacks}`);
|
|
}
|
|
|
|
if (summary.skippedVariants > 0) {
|
|
await this.info(
|
|
`Skipped Variants: ${this.colors.yellow}${summary.skippedVariants}${this.colors.reset} (no compare-at price)`
|
|
);
|
|
} else {
|
|
await this.info(`Skipped Variants: ${summary.skippedVariants}`);
|
|
}
|
|
|
|
if (summary.startTime) {
|
|
const duration = Math.round((new Date() - summary.startTime) / 1000);
|
|
await this.info(`Duration: ${duration} seconds`);
|
|
}
|
|
|
|
// Also log to progress file with rollback-specific format
|
|
try {
|
|
await this.progressService.logRollbackSummary(summary);
|
|
} catch (error) {
|
|
// Progress logging should not block main operations
|
|
console.warn(`Warning: Failed to log to progress file: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Logs error details and continues processing (Requirement 3.5)
|
|
* @param {Object} entry - Error entry
|
|
* @param {string} entry.productTitle - Product title
|
|
* @param {string} entry.productId - Product ID
|
|
* @param {string} entry.variantId - Variant ID (optional)
|
|
* @param {string} entry.errorMessage - Error message
|
|
* @param {Object} schedulingContext - Optional scheduling context for error logging (Requirements 5.3, 5.4)
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logProductError(entry, schedulingContext = null) {
|
|
const variantInfo = entry.variantId ? ` (Variant: ${entry.variantId})` : "";
|
|
const schedulingInfo =
|
|
schedulingContext && schedulingContext.isScheduled
|
|
? ` [Scheduled Operation]`
|
|
: "";
|
|
const message = `${this.colors.red}❌${this.colors.reset} Failed to update "${entry.productTitle}"${variantInfo}: ${entry.errorMessage}${schedulingInfo}`;
|
|
console.error(message);
|
|
|
|
// Also log to progress file with scheduling context
|
|
await this.progressService.logError(entry, schedulingContext);
|
|
}
|
|
|
|
/**
|
|
* Logs API rate limiting information
|
|
* @param {number} retryAfter - Seconds to wait before retry
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logRateLimit(retryAfter) {
|
|
await this.warning(
|
|
`Rate limit encountered. Waiting ${retryAfter} seconds before retry...`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Logs network retry attempts
|
|
* @param {number} attempt - Current attempt number
|
|
* @param {number} maxAttempts - Maximum attempts
|
|
* @param {string} error - Error message
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logRetryAttempt(attempt, maxAttempts, error) {
|
|
await this.warning(
|
|
`Network error (attempt ${attempt}/${maxAttempts}): ${error}. Retrying...`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Logs when a product is skipped due to invalid data
|
|
* @param {string} productTitle - Product title
|
|
* @param {string} reason - Reason for skipping
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logSkippedProduct(productTitle, reason) {
|
|
await this.warning(`Skipped "${productTitle}": ${reason}`);
|
|
}
|
|
|
|
/**
|
|
* Logs scheduling confirmation with operation details (Requirements 2.1, 2.3)
|
|
* @param {Object} schedulingInfo - Scheduling information
|
|
* @param {Date} schedulingInfo.scheduledTime - Target execution time
|
|
* @param {string} schedulingInfo.originalInput - Original datetime input
|
|
* @param {string} schedulingInfo.operationType - Type of operation (update/rollback)
|
|
* @param {Object} schedulingInfo.config - Operation configuration
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logSchedulingConfirmation(schedulingInfo) {
|
|
const { scheduledTime, originalInput, operationType, config } =
|
|
schedulingInfo;
|
|
|
|
await this.info("=".repeat(50));
|
|
await this.info("SCHEDULED OPERATION CONFIRMED");
|
|
await this.info("=".repeat(50));
|
|
await this.info(`Operation Type: ${operationType}`);
|
|
await this.info(`Scheduled Time: ${scheduledTime.toLocaleString()}`);
|
|
await this.info(`Original Input: ${originalInput}`);
|
|
await this.info(`Target Tag: ${config.targetTag}`);
|
|
|
|
if (operationType === "update") {
|
|
await this.info(`Price Adjustment: ${config.priceAdjustmentPercentage}%`);
|
|
}
|
|
|
|
await this.info(`Shop Domain: ${config.shopDomain}`);
|
|
|
|
const delay = scheduledTime.getTime() - new Date().getTime();
|
|
const timeRemaining = this.formatTimeRemaining(delay);
|
|
await this.info(`Time Remaining: ${timeRemaining}`);
|
|
await this.info("Press Ctrl+C to cancel the scheduled operation");
|
|
await this.info("=".repeat(50));
|
|
|
|
// Also log to progress file
|
|
await this.progressService.logSchedulingConfirmation(schedulingInfo);
|
|
}
|
|
|
|
/**
|
|
* Logs countdown updates during scheduled wait period (Requirements 2.2, 2.3)
|
|
* @param {Object} countdownInfo - Countdown information
|
|
* @param {Date} countdownInfo.scheduledTime - Target execution time
|
|
* @param {number} countdownInfo.remainingMs - Milliseconds remaining
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logCountdownUpdate(countdownInfo) {
|
|
const { scheduledTime, remainingMs } = countdownInfo;
|
|
const timeRemaining = this.formatTimeRemaining(remainingMs);
|
|
|
|
await this.info(
|
|
`Scheduled execution in: ${timeRemaining} (at ${scheduledTime.toLocaleString()})`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Logs the start of scheduled operation execution (Requirements 2.3, 5.4)
|
|
* @param {Object} executionInfo - Execution information
|
|
* @param {Date} executionInfo.scheduledTime - Original scheduled time
|
|
* @param {Date} executionInfo.actualTime - Actual execution time
|
|
* @param {string} executionInfo.operationType - Type of operation
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logScheduledExecutionStart(executionInfo) {
|
|
const { scheduledTime, actualTime, operationType } = executionInfo;
|
|
const delay = actualTime.getTime() - scheduledTime.getTime();
|
|
const delayText =
|
|
Math.abs(delay) < 1000
|
|
? "on time"
|
|
: delay > 0
|
|
? `${Math.round(delay / 1000)}s late`
|
|
: `${Math.round(Math.abs(delay) / 1000)}s early`;
|
|
|
|
await this.info("=".repeat(50));
|
|
await this.info("SCHEDULED OPERATION STARTING");
|
|
await this.info("=".repeat(50));
|
|
await this.info(`Operation Type: ${operationType}`);
|
|
await this.info(`Scheduled Time: ${scheduledTime.toLocaleString()}`);
|
|
await this.info(`Actual Start Time: ${actualTime.toLocaleString()}`);
|
|
await this.info(`Timing: ${delayText}`);
|
|
await this.info("=".repeat(50));
|
|
|
|
// Also log to progress file
|
|
await this.progressService.logScheduledExecutionStart(executionInfo);
|
|
}
|
|
|
|
/**
|
|
* Logs scheduled operation cancellation (Requirements 3.1, 3.2)
|
|
* @param {Object} cancellationInfo - Cancellation information
|
|
* @param {Date} cancellationInfo.scheduledTime - Original scheduled time
|
|
* @param {Date} cancellationInfo.cancelledTime - Time when cancelled
|
|
* @param {string} cancellationInfo.operationType - Type of operation
|
|
* @param {string} cancellationInfo.reason - Cancellation reason
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logScheduledOperationCancellation(cancellationInfo) {
|
|
const { scheduledTime, cancelledTime, operationType, reason } =
|
|
cancellationInfo;
|
|
const timeRemaining = scheduledTime.getTime() - cancelledTime.getTime();
|
|
const remainingText = this.formatTimeRemaining(timeRemaining);
|
|
|
|
await this.info("=".repeat(50));
|
|
await this.info("SCHEDULED OPERATION CANCELLED");
|
|
await this.info("=".repeat(50));
|
|
await this.info(`Operation Type: ${operationType}`);
|
|
await this.info(`Scheduled Time: ${scheduledTime.toLocaleString()}`);
|
|
await this.info(`Cancelled Time: ${cancelledTime.toLocaleString()}`);
|
|
await this.info(`Time Remaining: ${remainingText}`);
|
|
await this.info(`Reason: ${reason}`);
|
|
await this.info("=".repeat(50));
|
|
|
|
// Also log to progress file
|
|
await this.progressService.logScheduledOperationCancellation(
|
|
cancellationInfo
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Format time remaining into human-readable string
|
|
* @param {number} milliseconds - Time remaining in milliseconds
|
|
* @returns {string} Formatted time string (e.g., "2h 30m 15s")
|
|
*/
|
|
formatTimeRemaining(milliseconds) {
|
|
if (milliseconds <= 0) {
|
|
return "0s";
|
|
}
|
|
|
|
const seconds = Math.floor(milliseconds / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const hours = Math.floor(minutes / 60);
|
|
const days = Math.floor(hours / 24);
|
|
|
|
const remainingHours = hours % 24;
|
|
const remainingMinutes = minutes % 60;
|
|
const remainingSeconds = seconds % 60;
|
|
|
|
const parts = [];
|
|
|
|
if (days > 0) parts.push(`${days}d`);
|
|
if (remainingHours > 0) parts.push(`${remainingHours}h`);
|
|
if (remainingMinutes > 0) parts.push(`${remainingMinutes}m`);
|
|
if (remainingSeconds > 0 || parts.length === 0)
|
|
parts.push(`${remainingSeconds}s`);
|
|
|
|
return parts.join(" ");
|
|
}
|
|
|
|
/**
|
|
* Logs comprehensive error analysis and recommendations
|
|
* @param {Array} errors - Array of error objects
|
|
* @param {Object} summary - Operation summary statistics
|
|
* @param {Object} schedulingContext - Optional scheduling context for error analysis (Requirements 5.3, 5.4)
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async logErrorAnalysis(errors, summary, schedulingContext = null) {
|
|
if (!errors || errors.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const operationType =
|
|
summary.successfulRollbacks !== undefined ? "ROLLBACK" : "UPDATE";
|
|
const schedulingPrefix =
|
|
schedulingContext && schedulingContext.isScheduled ? "SCHEDULED " : "";
|
|
|
|
await this.info("=".repeat(50));
|
|
await this.info(`${schedulingPrefix}${operationType} ERROR ANALYSIS`);
|
|
await this.info("=".repeat(50));
|
|
|
|
if (schedulingContext && schedulingContext.isScheduled) {
|
|
await this.info(
|
|
`Scheduled Time: ${schedulingContext.scheduledTime.toLocaleString()}`
|
|
);
|
|
await this.info(
|
|
`Original Schedule Input: ${schedulingContext.originalInput}`
|
|
);
|
|
await this.info("=".repeat(50));
|
|
}
|
|
|
|
// Enhanced categorization for rollback operations
|
|
const categories = {};
|
|
const retryableErrors = [];
|
|
const nonRetryableErrors = [];
|
|
|
|
errors.forEach((error) => {
|
|
const category = this.categorizeError(
|
|
error.errorMessage || error.error || "Unknown"
|
|
);
|
|
if (!categories[category]) {
|
|
categories[category] = [];
|
|
}
|
|
categories[category].push(error);
|
|
|
|
// Track retryable vs non-retryable errors for rollback analysis
|
|
if (error.retryable === true) {
|
|
retryableErrors.push(error);
|
|
} else if (error.retryable === false) {
|
|
nonRetryableErrors.push(error);
|
|
}
|
|
});
|
|
|
|
// Display category breakdown
|
|
await this.info("Error Categories:");
|
|
Object.entries(categories).forEach(([category, categoryErrors]) => {
|
|
const percentage = (
|
|
(categoryErrors.length / errors.length) *
|
|
100
|
|
).toFixed(1);
|
|
this.info(
|
|
` ${category}: ${categoryErrors.length} errors (${percentage}%)`
|
|
);
|
|
});
|
|
|
|
// Rollback-specific error analysis (Requirements 4.3, 4.5)
|
|
if (operationType === "ROLLBACK") {
|
|
await this.info("\nRollback Error Analysis:");
|
|
|
|
if (retryableErrors.length > 0) {
|
|
await this.info(
|
|
` Retryable Errors: ${retryableErrors.length} (${(
|
|
(retryableErrors.length / errors.length) *
|
|
100
|
|
).toFixed(1)}%)`
|
|
);
|
|
}
|
|
|
|
if (nonRetryableErrors.length > 0) {
|
|
await this.info(
|
|
` Non-Retryable Errors: ${nonRetryableErrors.length} (${(
|
|
(nonRetryableErrors.length / errors.length) *
|
|
100
|
|
).toFixed(1)}%)`
|
|
);
|
|
}
|
|
|
|
// Analyze rollback-specific error patterns
|
|
const validationErrors = errors.filter(
|
|
(e) =>
|
|
e.errorType === "validation_error" || e.errorType === "validation"
|
|
);
|
|
if (validationErrors.length > 0) {
|
|
await this.info(
|
|
` Products without compare-at prices: ${validationErrors.length}`
|
|
);
|
|
}
|
|
|
|
const networkErrors = errors.filter(
|
|
(e) => e.errorType === "network_error"
|
|
);
|
|
if (networkErrors.length > 0) {
|
|
await this.info(
|
|
` Network-related failures: ${networkErrors.length} (consider retry)`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Provide recommendations based on error patterns
|
|
await this.info("\nRecommendations:");
|
|
await this.provideErrorRecommendations(categories, summary, operationType);
|
|
|
|
// Log to progress file as well with scheduling context
|
|
await this.progressService.logErrorAnalysis(errors, schedulingContext);
|
|
}
|
|
|
|
/**
|
|
* Categorize error for analysis (same logic as progress service)
|
|
* @param {string} errorMessage - Error message to categorize
|
|
* @returns {string} Error category
|
|
*/
|
|
categorizeError(errorMessage) {
|
|
const message = errorMessage.toLowerCase();
|
|
|
|
if (
|
|
message.includes("rate limit") ||
|
|
message.includes("429") ||
|
|
message.includes("throttled")
|
|
) {
|
|
return "Rate Limiting";
|
|
}
|
|
if (
|
|
message.includes("network") ||
|
|
message.includes("connection") ||
|
|
message.includes("timeout")
|
|
) {
|
|
return "Network Issues";
|
|
}
|
|
if (
|
|
message.includes("authentication") ||
|
|
message.includes("unauthorized") ||
|
|
message.includes("401")
|
|
) {
|
|
return "Authentication";
|
|
}
|
|
if (
|
|
message.includes("permission") ||
|
|
message.includes("forbidden") ||
|
|
message.includes("403")
|
|
) {
|
|
return "Permissions";
|
|
}
|
|
if (message.includes("not found") || message.includes("404")) {
|
|
return "Resource Not Found";
|
|
}
|
|
if (
|
|
message.includes("validation") ||
|
|
message.includes("invalid") ||
|
|
message.includes("price")
|
|
) {
|
|
return "Data Validation";
|
|
}
|
|
if (
|
|
message.includes("server error") ||
|
|
message.includes("500") ||
|
|
message.includes("502") ||
|
|
message.includes("503")
|
|
) {
|
|
return "Server Errors";
|
|
}
|
|
if (message.includes("shopify") && message.includes("api")) {
|
|
return "Shopify API";
|
|
}
|
|
|
|
return "Other";
|
|
}
|
|
|
|
/**
|
|
* Provide recommendations based on error patterns
|
|
* @param {Object} categories - Categorized errors
|
|
* @param {Object} summary - Operation summary
|
|
* @param {string} operationType - Type of operation ('UPDATE' or 'ROLLBACK')
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async provideErrorRecommendations(
|
|
categories,
|
|
summary,
|
|
operationType = "UPDATE"
|
|
) {
|
|
if (categories["Rate Limiting"]) {
|
|
await this.info(
|
|
" • Consider reducing batch size or adding delays between requests"
|
|
);
|
|
await this.info(
|
|
" • Check if your API plan supports the current request volume"
|
|
);
|
|
}
|
|
|
|
if (categories["Network Issues"]) {
|
|
await this.info(" • Check your internet connection stability");
|
|
await this.info(" • Consider running the script during off-peak hours");
|
|
if (operationType === "ROLLBACK") {
|
|
await this.info(
|
|
" • Network errors during rollback are retryable - consider re-running"
|
|
);
|
|
}
|
|
}
|
|
|
|
if (categories["Authentication"]) {
|
|
await this.info(
|
|
" • Verify your Shopify access token is valid and not expired"
|
|
);
|
|
await this.info(" • Check that your app has the required permissions");
|
|
}
|
|
|
|
if (categories["Data Validation"]) {
|
|
if (operationType === "ROLLBACK") {
|
|
await this.info(
|
|
" • Products without compare-at prices cannot be rolled back"
|
|
);
|
|
await this.info(
|
|
" • Consider filtering products to only include those with compare-at prices"
|
|
);
|
|
await this.info(
|
|
" • Review which products were updated in the original price adjustment"
|
|
);
|
|
} else {
|
|
await this.info(
|
|
" • Review product data for invalid prices or missing information"
|
|
);
|
|
await this.info(
|
|
" • Consider adding more robust data validation before updates"
|
|
);
|
|
}
|
|
}
|
|
|
|
if (categories["Server Errors"]) {
|
|
await this.info(" • Shopify may be experiencing temporary issues");
|
|
await this.info(" • Try running the script again later");
|
|
if (operationType === "ROLLBACK") {
|
|
await this.info(" • Server errors during rollback are retryable");
|
|
}
|
|
}
|
|
|
|
// Rollback-specific recommendations (Requirement 4.5)
|
|
if (operationType === "ROLLBACK") {
|
|
if (categories["Resource Not Found"]) {
|
|
await this.info(
|
|
" • Some products or variants may have been deleted since the original update"
|
|
);
|
|
await this.info(
|
|
" • Consider checking product existence before rollback operations"
|
|
);
|
|
}
|
|
|
|
if (categories["Permissions"]) {
|
|
await this.info(
|
|
" • Ensure your API credentials have product update permissions"
|
|
);
|
|
await this.info(
|
|
" • Rollback operations require the same permissions as price updates"
|
|
);
|
|
}
|
|
}
|
|
|
|
// Success rate analysis with operation-specific metrics
|
|
let successRate;
|
|
if (operationType === "ROLLBACK") {
|
|
successRate =
|
|
summary.eligibleVariants > 0
|
|
? (
|
|
(summary.successfulRollbacks / summary.eligibleVariants) *
|
|
100
|
|
).toFixed(1)
|
|
: 0;
|
|
} else {
|
|
successRate =
|
|
summary.totalVariants > 0
|
|
? ((summary.successfulUpdates / summary.totalVariants) * 100).toFixed(
|
|
1
|
|
)
|
|
: 0;
|
|
}
|
|
|
|
if (successRate < 50) {
|
|
await this.warning(
|
|
` • Low success rate detected (${successRate}%) - consider reviewing configuration`
|
|
);
|
|
if (operationType === "ROLLBACK") {
|
|
await this.warning(
|
|
" • Many products may not have valid compare-at prices for rollback"
|
|
);
|
|
}
|
|
} else if (successRate < 90) {
|
|
await this.info(
|
|
` • Moderate success rate (${successRate}%) - some optimization may be beneficial`
|
|
);
|
|
} else {
|
|
await this.info(
|
|
` • Good success rate (${successRate}%) - most operations completed successfully`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = Logger;
|