UNPKG

supapup

Version:

⚡ Lightning-fast MCP browser dev tool. Navigate → Get instant structured data. No screenshots needed! Puppeteer: 📸 → CSS selectors → JS eval. Supapup: semantic IDs ready to use. 10x faster, 90% fewer tokens.

470 lines (469 loc) 19.6 kB
export class DevToolsMonitor { networkLogs = []; consoleLogs = []; page; requestTimings = new Map(); requestDetails = new Map(); cdpSession; consoleHandler = () => { }; requestHandler = () => { }; responseHandler = () => { }; pageErrorHandler = () => { }; requestFailedHandler = () => { }; cdpRequestWillBeSentHandler = () => { }; cdpResponseReceivedHandler = () => { }; cdpLoadingFinishedHandler = () => { }; constructor(page) { this.page = page; this.setupListeners(); // Delay CDP setup more to ensure browser is fully ready setTimeout(() => { this.setupCDPListeners().catch(err => { // console.error('[DevTools] CDP setup error (non-fatal):', err.message); }); }, 2000); } setupListeners() { // Store handlers for cleanup this.consoleHandler = (msg) => { const location = msg.location(); this.consoleLogs.push({ timestamp: new Date(), type: msg.type(), text: msg.text(), location: location.url ? `${location.url}:${location.lineNumber}:${location.columnNumber}` : undefined, stackTrace: msg.stackTrace() }); }; this.requestHandler = (request) => { this.requestTimings.set(request.url(), Date.now()); }; this.responseHandler = (response) => { const request = response.request(); const startTime = this.requestTimings.get(request.url()); const duration = startTime ? Date.now() - startTime : undefined; this.networkLogs.push({ timestamp: new Date(), method: request.method(), url: request.url(), status: response.status(), type: request.resourceType(), size: response.headers()['content-length'] ? parseInt(response.headers()['content-length']) : undefined, duration }); this.requestTimings.delete(request.url()); }; this.pageErrorHandler = (error) => { this.consoleLogs.push({ timestamp: new Date(), type: 'error', text: error.message, stackTrace: error.stack }); }; this.requestFailedHandler = (request) => { this.networkLogs.push({ timestamp: new Date(), method: request.method(), url: request.url(), status: 0, type: request.resourceType() }); }; // Console logging this.page.on('console', this.consoleHandler); // Network monitoring this.page.on('request', this.requestHandler); this.page.on('response', this.responseHandler); // Page errors this.page.on('pageerror', this.pageErrorHandler); // Request failures this.page.on('requestfailed', this.requestFailedHandler); } // Get logs getConsoleLogs(filter) { let logs = [...this.consoleLogs]; if (filter?.type) { logs = logs.filter(log => log.type === filter.type); } if (filter?.since) { logs = logs.filter(log => log.timestamp > filter.since); } return logs; } getNetworkLogs(filter) { let logs = [...this.networkLogs]; if (filter?.status) { logs = logs.filter(log => log.status === filter.status); } if (filter?.method) { logs = logs.filter(log => log.method === filter.method); } if (filter?.since) { logs = logs.filter(log => log.timestamp > filter.since); } return logs; } // Clear logs clearLogs() { this.networkLogs = []; this.consoleLogs = []; this.requestTimings.clear(); } // Get performance metrics async getPerformanceMetrics() { // Get basic metrics from Puppeteer const metrics = await this.page.metrics(); // Get detailed performance data from the browser const performanceData = await this.page.evaluate(() => { const perf = window.performance; const navigation = perf.getEntriesByType('navigation')[0]; const resources = perf.getEntriesByType('resource'); // Calculate key metrics const timingMetrics = navigation ? { // Time to first byte ttfb: navigation.responseStart - navigation.requestStart, // DOM Content Loaded domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart, // Load Complete loadComplete: navigation.loadEventEnd - navigation.loadEventStart, // Total page load time totalLoadTime: navigation.loadEventEnd - navigation.fetchStart, // DNS lookup dnsLookup: navigation.domainLookupEnd - navigation.domainLookupStart, // TCP connection tcpConnection: navigation.connectEnd - navigation.connectStart, // Request duration requestDuration: navigation.responseEnd - navigation.requestStart, // Response duration responseDuration: navigation.responseEnd - navigation.responseStart, // DOM processing domProcessing: navigation.domComplete - navigation.domInteractive, // Resource fetch time resourceFetchTime: navigation.loadEventStart - navigation.domContentLoadedEventEnd } : {}; // Resource breakdown const resourceBreakdown = { scripts: resources.filter(r => r.initiatorType === 'script').length, stylesheets: resources.filter(r => r.initiatorType === 'link' || r.initiatorType === 'css').length, images: resources.filter(r => r.initiatorType === 'img').length, fonts: resources.filter(r => r.name.includes('.woff') || r.name.includes('.ttf')).length, xhr: resources.filter(r => r.initiatorType === 'xmlhttprequest' || r.initiatorType === 'fetch').length, other: resources.filter(r => !['script', 'link', 'css', 'img', 'xmlhttprequest', 'fetch'].includes(r.initiatorType)).length }; // Largest resources const largestResources = resources .filter(r => r.transferSize > 0) .sort((a, b) => b.transferSize - a.transferSize) .slice(0, 5) .map(r => ({ url: r.name, size: r.transferSize, duration: Math.round(r.duration), type: r.initiatorType })); // Slowest resources const slowestResources = resources .filter(r => r.duration > 0) .sort((a, b) => b.duration - a.duration) .slice(0, 5) .map(r => ({ url: r.name, duration: Math.round(r.duration), size: r.transferSize, type: r.initiatorType })); // Paint metrics const paintMetrics = {}; const paintEntries = perf.getEntriesByType('paint'); paintEntries.forEach((entry) => { paintMetrics[entry.name] = Math.round(entry.startTime); }); // Memory info if available const memoryInfo = 'memory' in performance ? performance.memory : null; return { navigation: timingMetrics, resources: { total: resources.length, breakdown: resourceBreakdown, totalSize: resources.reduce((sum, r) => sum + (r.transferSize || 0), 0), totalDuration: Math.max(...resources.map(r => r.responseEnd || 0)) }, largestResources, slowestResources, paintMetrics, memoryInfo }; }); return { ...metrics, detailed: performanceData }; } // Get coverage async startCoverage() { await Promise.all([ this.page.coverage.startJSCoverage(), this.page.coverage.startCSSCoverage() ]); } async stopCoverage() { const [jsCoverage, cssCoverage] = await Promise.all([ this.page.coverage.stopJSCoverage(), this.page.coverage.stopCSSCoverage() ]); return { jsCoverage, cssCoverage }; } // DOM and CSS inspection async inspectElement(selector) { const element = await this.page.$(selector); if (!element) { return null; } const properties = await element.evaluate((el) => { const styles = window.getComputedStyle(el); const rect = el.getBoundingClientRect(); return { tagName: el.tagName, id: el.id, className: el.className, attributes: Array.from(el.attributes).map(attr => ({ name: attr.name, value: attr.value })), boundingBox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, computedStyles: { display: styles.display, position: styles.position, color: styles.color, backgroundColor: styles.backgroundColor, fontSize: styles.fontSize, fontFamily: styles.fontFamily, margin: styles.margin, padding: styles.padding, border: styles.border, width: styles.width, height: styles.height, zIndex: styles.zIndex, opacity: styles.opacity, visibility: styles.visibility }, innerHTML: el.innerHTML.substring(0, 200) + (el.innerHTML.length > 200 ? '...' : ''), innerText: el.innerText?.substring(0, 200) + (el.innerText?.length > 200 ? '...' : '') }; }); return properties; } // Get page resources async getPageResources() { return await this.page.evaluate(() => { const resources = { scripts: Array.from(document.scripts).map(script => ({ src: script.src, async: script.async, defer: script.defer, type: script.type, inline: !script.src })), stylesheets: Array.from(document.styleSheets).map(sheet => ({ href: sheet.href, disabled: sheet.disabled, media: sheet.media.mediaText, rules: sheet.cssRules ? sheet.cssRules.length : 0 })), images: Array.from(document.images).map(img => ({ src: img.src, alt: img.alt, width: img.width, height: img.height, loaded: img.complete })), links: Array.from(document.links).map(link => ({ href: link.href, text: link.textContent, target: link.target, rel: link.rel })) }; return resources; }); } // Accessibility tree async getAccessibilityTree() { return await this.page.accessibility.snapshot(); } // Memory info async getMemoryInfo() { return await this.page.evaluate(() => { if ('memory' in performance) { return performance.memory; } return null; }); } // Setup Chrome DevTools Protocol listeners for enhanced network monitoring async setupCDPListeners() { try { // Wait for the page to be fully initialized await new Promise(resolve => setTimeout(resolve, 100)); this.cdpSession = await this.page.target().createCDPSession(); // Check if CDP session is valid if (!this.cdpSession || typeof this.cdpSession.on !== 'function') { // console.error('[DevTools] Invalid CDP session, skipping advanced network monitoring'); return; } // Enable network domain for detailed request/response data await this.cdpSession.send('Network.enable'); // Listen for request will be sent - captures headers and initiator this.cdpRequestWillBeSentHandler = (params) => { const { requestId, request, initiator, type } = params; // Store request details for later this.requestDetails.set(requestId, { url: request.url, method: request.method, headers: request.headers, postData: request.postData, initiator: initiator, type: type, timestamp: Date.now() }); }; this.cdpSession.on('Network.requestWillBeSent', this.cdpRequestWillBeSentHandler); // Listen for response received - captures response headers this.cdpResponseReceivedHandler = (params) => { const { requestId, response } = params; const requestDetail = this.requestDetails.get(requestId); if (requestDetail) { requestDetail.responseHeaders = response.headers; requestDetail.status = response.status; requestDetail.statusText = response.statusText; requestDetail.mimeType = response.mimeType; } }; this.cdpSession.on('Network.responseReceived', this.cdpResponseReceivedHandler); // Listen for loading finished to get response body this.cdpLoadingFinishedHandler = async (params) => { const { requestId } = params; const requestDetail = this.requestDetails.get(requestId); if (requestDetail && this.isAPIRequest(requestDetail)) { try { // Get response body for API requests const bodyResponse = await this.cdpSession.send('Network.getResponseBody', { requestId }); requestDetail.responseBody = bodyResponse.body; // Create enhanced network log entry const duration = Date.now() - requestDetail.timestamp; this.networkLogs.push({ timestamp: new Date(requestDetail.timestamp), method: requestDetail.method, url: requestDetail.url, status: requestDetail.status, type: requestDetail.type, duration: duration, requestHeaders: requestDetail.headers, requestPayload: requestDetail.postData ? this.parsePayload(requestDetail.postData) : undefined, responseHeaders: requestDetail.responseHeaders, responseBody: requestDetail.responseBody, initiator: requestDetail.initiator, isAPI: true }); } catch (err) { // Response body might not be available for some requests } // Clean up this.requestDetails.delete(requestId); } }; this.cdpSession.on('Network.loadingFinished', this.cdpLoadingFinishedHandler); } catch (err) { // console.error('Failed to setup CDP listeners:', err); } } // Helper to identify API requests isAPIRequest(requestDetail) { const url = requestDetail.url.toLowerCase(); const type = requestDetail.type?.toLowerCase(); // Check if it's XHR or Fetch if (type === 'xhr' || type === 'fetch') { return true; } // Check common API patterns if (url.includes('/api/') || url.includes('.json') || url.includes('/graphql') || url.includes('/rest/')) { return true; } // Check content type const contentType = requestDetail.responseHeaders?.['content-type'] || ''; if (contentType.includes('application/json') || contentType.includes('application/xml')) { return true; } return false; } // Helper to parse request payload parsePayload(postData) { try { return JSON.parse(postData); } catch { // Not JSON, return as-is return postData; } } // Get API logs with enhanced filtering getAPILogs(filter) { let logs = this.networkLogs.filter(log => log.isAPI); if (filter?.method) { logs = logs.filter(log => log.method === filter.method); } if (filter?.urlPattern) { const pattern = new RegExp(filter.urlPattern); logs = logs.filter(log => pattern.test(log.url)); } if (filter?.status) { logs = logs.filter(log => log.status === filter.status); } if (filter?.since) { logs = logs.filter(log => log.timestamp > filter.since); } return logs; } // Cleanup method to remove all event listeners async cleanup() { try { // Remove page event listeners using stored handlers if (this.page) { this.page.off('console', this.consoleHandler); this.page.off('request', this.requestHandler); this.page.off('response', this.responseHandler); this.page.off('pageerror', this.pageErrorHandler); this.page.off('requestfailed', this.requestFailedHandler); } // Remove CDP session listeners if (this.cdpSession) { this.cdpSession.off('Network.requestWillBeSent', this.cdpRequestWillBeSentHandler); this.cdpSession.off('Network.responseReceived', this.cdpResponseReceivedHandler); this.cdpSession.off('Network.loadingFinished', this.cdpLoadingFinishedHandler); // Detach the CDP session this.cdpSession.detach().catch(() => { // Ignore errors during detach }); } // Clear data this.networkLogs = []; this.consoleLogs = []; this.requestTimings.clear(); this.requestDetails.clear(); this.cdpSession = null; } catch (error) { // Ignore cleanup errors } } }