From b5f7c85723fcdeefd5fe7ed7c5de5d7358d5d809 Mon Sep 17 00:00:00 2001 From: Amruta Khamkar Date: Fri, 28 Mar 2025 09:14:59 +0000 Subject: [PATCH] code update recorded at: 28/03/25 09:14:59 --- handler.js | 198 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 19 +++++ 2 files changed, 217 insertions(+) create mode 100644 handler.js create mode 100644 package.json diff --git a/handler.js b/handler.js new file mode 100644 index 0000000..e6b8155 --- /dev/null +++ b/handler.js @@ -0,0 +1,198 @@ +import lighthouse from 'lighthouse'; +import { chromium } from "@playwright/test" +import lighthouseDesktopConfig from "lighthouse/core/config/desktop-config.js" +import fs from 'fs'; + +// Log Level +// Possible values: silent, error, warn, info, verbose +const LOG_LEVEL = process.env.LOG_LEVEL || "error"; +// Maximum allowed connections in the connection pool +const BROWSER_PORT = parseInt(process.env.BROWSER_PORT) || 9000; +// Save reports to disk +const SAVE_REPORTS_TO_DISK = process.env.SAVE_REPORTS_TO_DISK === 'true'; +// Default timeout for Lighthouse +const DEFAULT_TIMEOUT_IN_SECONDS = parseInt(process.env.DEFAULT_TIMEOUT_IN_SECONDS) || 60; +// Evaluation Categories +const CATEGORIES = [ + "accessibility", + "best-practices", + "performance", + "pwa", + "seo", +] +// Device Types +const DEVICE_TYPES = { + DESKTOP: "desktop", + MOBILE: "mobile", +} +/** + * Validates the request body for required fields and structure. + * @param {Object} body - Request body containing the required fields. + * @throws {Error} If validation fails. + */ +const validateRequestBody = ({ url, timeout, categories, device }) => { + const errors = []; + + // Ensure the query string is provided + if (!url) { + errors.push('URL is required.'); + } + + // Ensure the timeout is a positive integer + if (timeout && (isNaN(timeout) || timeout <= 0)) { + errors.push('Timeout must be a positive integer.'); + } + + // Ensure the categories selected is from the allowed values + if (categories && categories.length > 0) { + for (const category of categories) { + if (!CATEGORIES.includes(category)) { + errors.push(`Category ${category} is not valid.`); + } + } + } + + // Ensure the device is from the allowed values + if (device && !Object.values(DEVICE_TYPES).includes(device)) { + errors.push(`Device ${device} is not valid.`); + } + + // Throw error if any validation fails + if (errors.length) { + var msg = `Validation errors: ${errors.join(' ')}` + console.log(msg) + throw new Error(msg); + } +}; + +async function runLighthouse({ url, timeout = DEFAULT_TIMEOUT_IN_SECONDS, device = DEVICE_TYPES.DESKTOP, categories = CATEGORIES }) { + let browser; + let page; + try { + browser = await chromium.launch({ + args: [`--remote-debugging-port=${BROWSER_PORT}`], + }); + + const options = { + logLevel: LOG_LEVEL, + output: 'html', + port: BROWSER_PORT, + ignoreStatusCode: false, + maxWaitForLoad: timeout * 1000, + ...(device === DEVICE_TYPES.DESKTOP ? lighthouseDesktopConfig.settings : {}), + emulatedUserAgent: `Boltic Serverless v1.0 with mode ${device}`, + onlyCategories: categories, + }; + + console.log("Running Lighthouse with options:", JSON.stringify(options)) + + const runnerResult = await lighthouse(url, options); + const reportHtml = runnerResult.report; + + // Extract performance metrics from the lighthouse results + const metrics = { + firstContentfulPaint: runnerResult.lhr.audits['first-contentful-paint'].numericValue, + largestContentfulPaint: runnerResult.lhr.audits['largest-contentful-paint'].numericValue, + speedIndex: runnerResult.lhr.audits['speed-index'].numericValue, + totalBlockingTime: runnerResult.lhr.audits['total-blocking-time'].numericValue, + cumulativeLayoutShift: runnerResult.lhr.audits['cumulative-layout-shift'].numericValue, + // Convert milliseconds to seconds for better readability + timeToInteractive: runnerResult.lhr.audits['interactive'].numericValue, + performanceScore: runnerResult.lhr.categories.performance.score * 100 + }; + + // Generate PDF directly without saving HTML first + page = await browser.newPage(); + await page.setContent(reportHtml, { + waitUntil: 'load', + }); + const pdfBuffer = await page.pdf({ + format: 'A4', + printBackground: true, + margin: { + top: '10px', + right: '10px', + bottom: '10px', + left: '10px' + } + }); + + if (SAVE_REPORTS_TO_DISK) { + fs.writeFileSync('lighthouse-report.html', reportHtml); + fs.writeFileSync('lighthouse-report.pdf', pdfBuffer); + } + + // Check report size + const reportSizeInMB = Buffer.byteLength(reportHtml) / (1024 * 1024); + if (reportSizeInMB > 4) { + console.warn(`Warning: Report size (${reportSizeInMB.toFixed(2)} MB) exceeds 4MB`); + // throw new Error(`Report size (${reportSizeInMB.toFixed(2)} MB) exceeds 4MB`) // Uncomment if needed + } else { + console.log(`Report size: ${reportSizeInMB.toFixed(2)} MB`); + } + + return { + reports: { + // Uncomment if needed + // html: reportHtml.toString('base64'), + pdf: pdfBuffer.toString('base64'), + }, + metrics: { + firstContentfulPaint: Number((metrics.firstContentfulPaint / 1000).toFixed(2)), + largestContentfulPaint: Number((metrics.largestContentfulPaint / 1000).toFixed(2)), + speedIndex: Number((metrics.speedIndex / 1000).toFixed(2)), + totalBlockingTime: Number(metrics.totalBlockingTime.toFixed(2)), + cumulativeLayoutShift: Number(metrics.cumulativeLayoutShift.toFixed(3)), + timeToInteractive: Number((metrics.timeToInteractive / 1000).toFixed(2)), + performanceScore: Number(metrics.performanceScore.toFixed(0)), + } + }; + + } catch (error) { + console.error('Error generating lighthouse report:', error); + throw error; + } finally { + if (page) await page.close(); + if (browser) await browser.close(); + } +} + +/** + * Express.js handler for incoming requests. + * Run Lighthouse and return the result. + * @param {Object} req - Express request object. + * @param {Object} res - Express response object. + */ +export const handler = async (req, res) => { + try { + console.log('Received request:', JSON.stringify(req.body)); + // Ensure the request method is POST + if (req.method !== 'POST') { + return res.status(400).json({ error: 'Invalid method, only POST allowed.' }); + } + + // Extract and validate request body + validateRequestBody(req.body); + + // Initialize startTime + const startTime = new Date(); + + // Run Lighthouse + const result = await runLighthouse(req.body); + // Calculate elapsed time in seconds + const elapsedTime = (new Date() - startTime) / 1000; + + // Log the elapsed time + console.log(`Lighthouse execution time: ${elapsedTime.toFixed(3)} seconds`); + + // Send the result + res.setHeader('Content-Type', 'application/json'); + res.status(200).json({ + ...result, + executionTime: Number(elapsedTime.toFixed(3)) + }); + } catch (error) { + console.error('Error handling request:', error.message); + res.status(500).json({ error: error.message }); + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..46aedec --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "boltic-lighthouse-report", + "version": "1.0.0", + "description": "Analyze web apps and web pages using Lighthouse to collect modern performance metrics and gain insights into developer best practices.", + "main": "handler.js", + "scripts": { + "start": "node handler.js" + }, + "dependencies": { + "@playwright/test": "^1.51.1", + "lighthouse": "^12.5.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "author": "Boltic", + "license": "MIT", + "type": "module" +} \ No newline at end of file