Initial commit: Complete Shopify Price Updater implementation
- Full Node.js application with Shopify GraphQL API integration - Compare At price support for promotional pricing - Comprehensive error handling and retry logic - Progress tracking with markdown logging - Complete test suite with unit and integration tests - Production-ready with proper exit codes and signal handling
This commit is contained in:
386
src/utils/logger.js
Normal file
386
src/utils/logger.js
Normal file
@@ -0,0 +1,386 @@
|
||||
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
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async logOperationStart(config) {
|
||||
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
|
||||
await this.progressService.logOperationStart(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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 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
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async logProductError(entry) {
|
||||
const variantInfo = entry.variantId ? ` (Variant: ${entry.variantId})` : "";
|
||||
const message = `${this.colors.red}❌${this.colors.reset} Failed to update "${entry.productTitle}"${variantInfo}: ${entry.errorMessage}`;
|
||||
console.error(message);
|
||||
|
||||
// Also log to progress file
|
||||
await this.progressService.logError(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 comprehensive error analysis and recommendations
|
||||
* @param {Array} errors - Array of error objects
|
||||
* @param {Object} summary - Operation summary statistics
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async logErrorAnalysis(errors, summary) {
|
||||
if (!errors || errors.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.info("=".repeat(50));
|
||||
await this.info("ERROR ANALYSIS");
|
||||
await this.info("=".repeat(50));
|
||||
|
||||
// Categorize errors
|
||||
const categories = {};
|
||||
errors.forEach((error) => {
|
||||
const category = this.categorizeError(
|
||||
error.errorMessage || error.error || "Unknown"
|
||||
);
|
||||
if (!categories[category]) {
|
||||
categories[category] = [];
|
||||
}
|
||||
categories[category].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}%)`
|
||||
);
|
||||
});
|
||||
|
||||
// Provide recommendations based on error patterns
|
||||
await this.info("\nRecommendations:");
|
||||
await this.provideErrorRecommendations(categories, summary);
|
||||
|
||||
// Log to progress file as well
|
||||
await this.progressService.logErrorAnalysis(errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async provideErrorRecommendations(categories, summary) {
|
||||
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 (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"]) {
|
||||
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");
|
||||
}
|
||||
|
||||
// Success rate analysis
|
||||
const successRate =
|
||||
summary.totalVariants > 0
|
||||
? ((summary.successfulUpdates / summary.totalVariants) * 100).toFixed(1)
|
||||
: 0;
|
||||
|
||||
if (successRate < 50) {
|
||||
await this.warning(
|
||||
" • Low success rate detected - consider reviewing configuration"
|
||||
);
|
||||
} else if (successRate < 90) {
|
||||
await this.info(
|
||||
" • Moderate success rate - some optimization may be beneficial"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Logger;
|
||||
Reference in New Issue
Block a user