UNPKG

@bschauer/webtools-mcp-server

Version:

MCP server providing web analysis tools including screenshot, debug, performance, security, accessibility, SEO, and asset optimization capabilities

331 lines (295 loc) 10.4 kB
/** * Coverage Analysis Tool * Uses Chrome DevTools Protocol Coverage API to analyze unused JavaScript and CSS */ import { logInfo, logError } from "../../../utils/logging.js"; import { BROWSER_HEADERS } from "../../../config/constants.js"; import { checkSiteAvailability } from "../../../utils/html.js"; import { fetchWithRetry } from "../../../utils/fetch.js"; import { getDeviceConfig } from "../../../config/devices.js"; import { getNetworkCondition, applyNetworkConditions } from "../../../config/network_conditions.js"; import { enableRequiredDomains, startCoverageCollection, stopCoverageCollection, configureCdpSession } from "../../../utils/cdp_helpers.js"; import { analyzeJSCoverage } from "./js_analyzer.js"; import { analyzeCSSCoverage } from "./css_analyzer.js"; /** * Run coverage analysis on a webpage * @param {Object} args - The tool arguments * @returns {Promise<Object>} Coverage analysis results */ export async function runCoverageAnalysis(args) { const { url, timeoutMs = 15000, waitAfterLoadMs = 2000, includeThirdParty = true, useProxy = false, ignoreSSLErrors = false, disableCache = true, networkConditionName, networkCondition, deviceName, deviceConfig: deviceConfigArg } = args; let puppeteer; let browser; try { // Dynamically import puppeteer try { puppeteer = await import("puppeteer"); } catch (e) { return { content: [ { type: "text", text: JSON.stringify( { error: "Coverage analysis functionality not available", details: "Puppeteer is not installed", recommendation: "Please install Puppeteer to use coverage analysis functionality", retryable: false, url, }, null, 2 ), }, ], }; } // Check site availability first const availability = await checkSiteAvailability(url, { ignoreSSLErrors }, fetchWithRetry); if (!availability.available) { return { content: [ { type: "text", text: JSON.stringify( { error: "Site unavailable", details: availability.error, recommendation: availability.recommendation, retryable: true, url, }, null, 2 ), }, ], }; } // Get device configuration const deviceConfig = getDeviceConfig({ deviceName, deviceConfig: deviceConfigArg }); // Get network condition if specified const networkConditionConfig = getNetworkCondition({ networkConditionName, networkCondition }); logInfo("coverage_analysis", "Starting coverage analysis", { url, deviceConfig: deviceConfig.name, networkCondition: networkConditionConfig.name, }); // Launch browser with appropriate settings browser = await puppeteer.launch({ headless: "new", args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu", useProxy ? `--proxy-server=${process.env.PROXY_URL || ""}` : "", "--ignore-certificate-errors"].filter(Boolean), ignoreHTTPSErrors: ignoreSSLErrors, }); const page = await browser.newPage(); // Set HTTP headers await page.setExtraHTTPHeaders(BROWSER_HEADERS); // Set viewport based on device config await page.setViewport({ width: deviceConfig.width, height: deviceConfig.height, deviceScaleFactor: deviceConfig.deviceScaleFactor, isMobile: deviceConfig.isMobile, hasTouch: deviceConfig.hasTouch, isLandscape: deviceConfig.isLandscape, }); // Set custom user agent if provided if (deviceConfig.userAgent) { await page.setUserAgent(deviceConfig.userAgent); } // Create CDP session const client = await page.target().createCDPSession(); // Configure CDP session with network conditions and other settings await configureCdpSession(client, { networkCondition: networkConditionConfig, disableCache, }); // Start coverage collection const coverageStarted = await startCoverageCollection(client); if (!coverageStarted) { throw new Error("Failed to start coverage collection"); } // Navigate to the page with timeout try { await page.goto(url, { waitUntil: "networkidle0", timeout: timeoutMs, }); } catch (navError) { // Handle navigation timeout or other navigation errors logError("coverage_analysis", "Navigation failed", navError, { url }); return { content: [ { type: "text", text: JSON.stringify( { error: "Navigation failed", details: navError.message, recommendation: "Try increasing the timeout or check if the site is responsive", retryable: true, url, errorType: navError.name, }, null, 2 ), }, ], }; } // Wait for a moment to capture post-load activity await new Promise((resolve) => setTimeout(resolve, waitAfterLoadMs)); // Interact with the page to trigger more JavaScript execution await autoInteractWithPage(page); // Stop coverage collection const coverageData = await stopCoverageCollection(client); if (!coverageData) { throw new Error("Failed to collect coverage data"); } // Analyze JavaScript coverage const jsAnalysis = await analyzeJSCoverage(coverageData.jsCoverage, { url, includeThirdParty, }); // Analyze CSS coverage const cssAnalysis = await analyzeCSSCoverage(coverageData.cssCoverage, { url, includeThirdParty, }); // Get page resources for reference const resources = await page.evaluate(() => { return performance.getEntriesByType("resource").map((resource) => ({ name: resource.name, type: resource.initiatorType, size: resource.transferSize, duration: resource.duration, })); }); // Prepare the final results const results = { url, deviceConfig: { name: deviceConfig.name, width: deviceConfig.width, height: deviceConfig.height, userAgent: await page.evaluate(() => navigator.userAgent), }, networkCondition: { name: networkConditionConfig.name, description: networkConditionConfig.description, }, summary: { totalJsBytes: jsAnalysis.totalBytes, unusedJsBytes: jsAnalysis.unusedBytes, unusedJsPercentage: jsAnalysis.unusedPercentage, totalCssBytes: cssAnalysis.totalBytes, unusedCssBytes: cssAnalysis.unusedBytes, unusedCssPercentage: cssAnalysis.unusedPercentage, potentialSavings: jsAnalysis.unusedBytes + cssAnalysis.unusedBytes, }, javascript: { files: jsAnalysis.files, unusedByFile: jsAnalysis.unusedByFile, thirdPartyAnalysis: jsAnalysis.thirdPartyAnalysis, recommendations: jsAnalysis.recommendations, }, css: { files: cssAnalysis.files, unusedByFile: cssAnalysis.unusedByFile, thirdPartyAnalysis: cssAnalysis.thirdPartyAnalysis, recommendations: cssAnalysis.recommendations, }, resources, }; logInfo("coverage_analysis", "Coverage analysis completed successfully", { url }); return { content: [ { type: "text", text: JSON.stringify(results, null, 2), }, ], }; } catch (error) { logError("coverage_analysis", "Coverage analysis failed", error, { url }); return { content: [ { type: "text", text: JSON.stringify( { error: "Coverage analysis failed", details: error.message, recommendation: "Please try again with different settings", retryable: true, url, errorType: error.name, }, null, 2 ), }, ], }; } finally { if (browser) { try { await browser.close(); } catch (closeError) { logError("coverage_analysis", "Failed to close browser", closeError, { url }); } } } } /** * Automatically interact with the page to trigger more JavaScript execution * @param {Object} page - Puppeteer page * @returns {Promise<void>} */ async function autoInteractWithPage(page) { try { // Scroll down the page in increments const pageHeight = await page.evaluate(() => document.body.scrollHeight); const viewportHeight = await page.evaluate(() => window.innerHeight); const scrollSteps = Math.ceil(pageHeight / viewportHeight); for (let i = 1; i <= scrollSteps; i++) { await page.evaluate( (step, vh) => { window.scrollTo(0, step * vh); }, i, viewportHeight ); await page.waitForTimeout(100); } // Scroll back to top await page.evaluate(() => { window.scrollTo(0, 0); }); // Try to click on some interactive elements const clickableElements = await page.$$('button, a, [role="button"], .btn, input[type="submit"]'); // Limit to first 5 elements to avoid too much interaction const elementsToClick = clickableElements.slice(0, 5); for (const element of elementsToClick) { try { // Check if element is visible const isVisible = await page.evaluate((el) => { const style = window.getComputedStyle(el); return style && style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0"; }, element); if (isVisible) { await element.click({ delay: 100 }).catch(() => {}); await page.waitForTimeout(200); } } catch (error) { // Ignore errors from clicking } } // Wait a bit for any triggered JavaScript to execute await page.waitForTimeout(500); } catch (error) { // Ignore errors from auto-interaction logInfo("coverage_analysis", "Auto-interaction with page completed with some errors", { error: error.message }); } }