UNPKG

n8n-nodes-google-pagespeed

Version:

n8n community node for Google PageSpeed Insights API with comprehensive performance, accessibility, and SEO analysis

247 lines 9.55 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.makePageSpeedRequest = makePageSpeedRequest; exports.batchProcessUrls = batchProcessUrls; exports.validateApiKey = validateApiKey; exports.estimateQuotaUsage = estimateQuotaUsage; const config_1 = require("../config"); const urlUtils_1 = require("./urlUtils"); /** * Create delay for retry logic with exponential backoff * @param attempt - Current attempt number * @param baseDelay - Base delay in milliseconds * @returns Promise that resolves after delay */ function createDelay(attempt, baseDelay = config_1.PAGESPEED_CONFIG.RETRY_DELAY_BASE) { const delay = Math.min(baseDelay * Math.pow(2, attempt), config_1.PAGESPEED_CONFIG.RETRY_DELAY_MAX); return new Promise(resolve => setTimeout(resolve, delay)); } /** * Classify error type based on error message and status code * @param error - The error object * @returns Classified error type */ function classifyError(error) { const message = error?.message?.toLowerCase() || ''; const statusCode = error?.response?.status || error?.statusCode; if (statusCode === 401 || message.includes('authentication') || message.includes('api key')) { return config_1.ERROR_TYPES.AUTHENTICATION_ERROR; } if (statusCode === 429 || message.includes('rate limit') || message.includes('quota')) { return config_1.ERROR_TYPES.RATE_LIMITED; } if (statusCode === 403 && (message.includes('quota') || message.includes('billing'))) { return config_1.ERROR_TYPES.QUOTA_EXCEEDED; } if (message.includes('timeout') || message.includes('ETIMEDOUT')) { return config_1.ERROR_TYPES.TIMEOUT; } if (message.includes('not_html') || message.includes('NOT_HTML')) { return config_1.ERROR_TYPES.NOT_HTML; } if (message.includes('network') || message.includes('ECONNREFUSED') || message.includes('ENOTFOUND')) { return config_1.ERROR_TYPES.NETWORK_ERROR; } if (statusCode >= 400 && statusCode < 500) { return config_1.ERROR_TYPES.API_ERROR; } return config_1.ERROR_TYPES.UNKNOWN; } /** * Check if error is retryable * @param errorType - Classified error type * @returns True if error should be retried */ function isRetryableError(errorType) { const retryableErrors = [ config_1.ERROR_TYPES.TIMEOUT, config_1.ERROR_TYPES.NETWORK_ERROR, config_1.ERROR_TYPES.RATE_LIMITED, config_1.ERROR_TYPES.UNKNOWN ]; return retryableErrors.includes(errorType); } /** * Build PageSpeed API URL with parameters * @param config - API request configuration * @param apiKey - Google API key * @returns Complete API URL */ function buildApiUrl(config, apiKey) { const params = new URLSearchParams({ url: config.url, strategy: config.strategy, key: apiKey, }); // Add categories - Fixed: Added proper typing config.categories.forEach((category) => { params.append('category', category); }); // Add optional parameters if (config.locale) { params.set('locale', config.locale); } if (config.screenshot) { params.set('screenshot', 'true'); } return `${config_1.PAGESPEED_CONFIG.API_BASE_URL}?${params.toString()}`; } /** * Make PageSpeed API request with enhanced error handling and retry logic * @param context - n8n execution context * @param apiKey - Google API key * @param config - API request configuration * @param additionalFields - Additional request options * @returns PageSpeed API response or error response */ async function makePageSpeedRequest(context, apiKey, config, additionalFields = {}) { const maxRetries = additionalFields.retryAttempts || config_1.PAGESPEED_CONFIG.DEFAULT_RETRY_ATTEMPTS; const timeout = additionalFields.customTimeout ? additionalFields.customTimeout * 1000 : config_1.PAGESPEED_CONFIG.DEFAULT_TIMEOUT; const skipValidation = additionalFields.skipContentValidation || false; // Pre-validate URL content type if not skipped if (!skipValidation) { const validation = await (0, urlUtils_1.validateUrlContentType)(context, config.url); if (!validation.isValid) { return { error: true, errorType: config_1.ERROR_TYPES.INVALID_CONTENT_TYPE, errorMessage: validation.error || `URL returns ${validation.contentType} instead of HTML`, url: config.url, strategy: config.strategy, contentType: validation.contentType, analysisTime: new Date().toISOString(), canRetry: false, }; } } // Build API URL const apiUrl = buildApiUrl(config, apiKey); // Attempt request with retry logic for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const options = { method: 'GET', url: apiUrl, json: true, timeout: timeout, headers: { 'User-Agent': 'n8n-google-pagespeed/1.0', }, }; const response = await context.helpers.request(options); // Validate response structure if (!response.lighthouseResult) { throw new Error('Invalid API response: missing lighthouse results'); } return response; } catch (error) { const errorType = classifyError(error); const isLastAttempt = attempt === maxRetries; const canRetry = isRetryableError(errorType) && !isLastAttempt; // Log the attempt for debugging console.log(`PageSpeed API attempt ${attempt + 1}/${maxRetries + 1} failed for ${config.url}: ${error?.message || 'Unknown error'}`); if (!canRetry) { return { error: true, errorType, errorMessage: config_1.USER_FRIENDLY_ERROR_MESSAGES[errorType] || error?.message || 'Unknown error', url: config.url, strategy: config.strategy, analysisTime: new Date().toISOString(), retryCount: attempt, canRetry: false, }; } // Wait before retrying (except for the last attempt) if (attempt < maxRetries) { await createDelay(attempt); } } } // This shouldn't be reached, but provide fallback return { error: true, errorType: config_1.ERROR_TYPES.UNKNOWN, errorMessage: 'All retry attempts failed', url: config.url, strategy: config.strategy, analysisTime: new Date().toISOString(), retryCount: maxRetries, canRetry: false, }; } /** * Process batch of URLs with controlled concurrency and rate limiting * @param context - n8n execution context * @param apiKey - Google API key * @param configs - Array of API request configurations * @param additionalFields - Additional request options * @param onProgress - Optional progress callback * @returns Array of PageSpeed results */ async function batchProcessUrls(context, apiKey, configs, additionalFields = {}, onProgress) { const results = []; const maxConcurrent = config_1.PAGESPEED_CONFIG.MAX_CONCURRENT_REQUESTS; for (let i = 0; i < configs.length; i += maxConcurrent) { const batch = configs.slice(i, i + maxConcurrent); const batchPromises = batch.map(async (config, batchIndex) => { const result = await makePageSpeedRequest(context, apiKey, config, additionalFields); // Call progress callback if provided if (onProgress) { onProgress(i + batchIndex + 1, configs.length); } return result; }); const batchResults = await Promise.all(batchPromises); results.push(...batchResults); // Add delay between batches to respect rate limits if (i + maxConcurrent < configs.length) { await createDelay(0, config_1.PAGESPEED_CONFIG.BATCH_DELAY_MS); } } return results; } /** * Validate API key by making a test request * @param context - n8n execution context * @param apiKey - Google API key to validate * @returns True if API key is valid */ async function validateApiKey(context, apiKey) { try { const testConfig = { url: 'https://www.google.com', strategy: 'mobile', categories: ['performance'], timeout: 10000, }; const result = await makePageSpeedRequest(context, apiKey, testConfig, { retryAttempts: 0, skipContentValidation: true, }); return !('error' in result) || result.errorType !== config_1.ERROR_TYPES.AUTHENTICATION_ERROR; } catch (error) { return false; } } /** * Estimate API quota usage for a batch of URLs * @param urlCount - Number of URLs to analyze * @param strategies - Array of strategies ('desktop', 'mobile', or 'both') * @returns Estimated quota usage */ function estimateQuotaUsage(urlCount, strategies) { let requestsPerUrl = 0; strategies.forEach(strategy => { if (strategy === 'both') { requestsPerUrl += 2; // Desktop + Mobile } else { requestsPerUrl += 1; } }); return urlCount * requestsPerUrl; } //# sourceMappingURL=apiUtils.js.map