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
JavaScript
;
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