UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

405 lines (347 loc) • 13.5 kB
import { Page } from 'playwright'; export interface HydrationMismatch { element: string; serverHTML: string; clientHTML: string; timestamp: Date; stackTrace?: string; } export interface BundleInfo { url: string; size: number; gzipSize?: number; loadTime: number; parseTime?: number; cacheStatus: 'hit' | 'miss' | 'none'; isLazyLoaded: boolean; coverage?: number; // Percentage of code used } export interface RouteChange { from: string; to: string; duration: number; timestamp: Date; type: 'push' | 'replace' | 'pop'; dataFetched?: number; // bytes } export class ProblemDebugEngine { private hydrationMismatches: HydrationMismatch[] = []; private bundles: Map<string, BundleInfo> = new Map(); private routeChanges: RouteChange[] = []; private page?: Page; async attachToPage(page: Page): Promise<void> { this.page = page; // Inject problem detection scripts await this.injectHydrationDetection(); await this.setupBundleMonitoring(); await this.setupRouteMonitoring(); } private async injectHydrationDetection(): Promise<void> { if (!this.page) return; await this.page.addInitScript(() => { // Create hydration mismatch detector (window as any).__HYDRATION_DEBUG__ = { mismatches: [], originalError: console.error, detectMismatch: function(args: any[]) { const errorStr = args.join(' '); // Common hydration error patterns const patterns = [ /Text content does not match/i, /Hydration failed/i, /did not match/i, /Warning:.*?during hydration/i, /Prop `.*?` did not match/i, /Expected server HTML/i ]; if (patterns.some(p => p.test(errorStr))) { // Try to extract details const stack = new Error().stack; this.mismatches.push({ message: errorStr, timestamp: new Date().toISOString(), stack: stack, url: window.location.href }); } } }; // Override console.error to catch hydration warnings console.error = function(...args) { (window as any).__HYDRATION_DEBUG__.detectMismatch(args); return (window as any).__HYDRATION_DEBUG__.originalError.apply(console, args); }; // Also catch React specific hydration errors if ((window as any).React) { const originalCreateElement = (window as any).React.createElement; (window as any).React.createElement = function(...args: any[]) { try { return originalCreateElement.apply(this, args); } catch (error: any) { if (error.message?.includes('hydrat')) { (window as any).__HYDRATION_DEBUG__.mismatches.push({ message: error.message, timestamp: new Date().toISOString(), component: args[0]?.name || 'Unknown' }); } throw error; } }; } }); } private async setupBundleMonitoring(): Promise<void> { if (!this.page) return; // Monitor resource timing for bundles this.page.on('response', async (response) => { const url = response.url(); const contentType = response.headers()['content-type'] || ''; // Track JavaScript bundles if (contentType.includes('javascript') || url.endsWith('.js')) { const request = response.request(); const timing = (response as any).timing?.(); const size = parseInt(response.headers()['content-length'] || '0'); const gzipSize = response.headers()['content-encoding'] === 'gzip' ? size : undefined; const bundleInfo: BundleInfo = { url, size, gzipSize, loadTime: timing ? timing.responseEnd - timing.requestStart : 0, cacheStatus: this.getCacheStatus(response), isLazyLoaded: this.isLazyLoaded(request) }; this.bundles.set(url, bundleInfo); } }); // Inject coverage tracking await this.page.coverage.startJSCoverage(); } private getCacheStatus(response: any): 'hit' | 'miss' | 'none' { const cacheControl = response.headers()['cache-control']; const etag = response.headers()['etag']; const status = response.status(); if (status === 304) return 'hit'; if (cacheControl?.includes('no-cache')) return 'none'; if (etag) return 'miss'; // Has etag but not 304 return 'none'; } private isLazyLoaded(request: any): boolean { // Check if loaded after initial page load const timing = request.timing(); if (!timing) return false; // If request started more than 1s after navigation, likely lazy loaded return timing.requestStart > 1000; } private async setupRouteMonitoring(): Promise<void> { if (!this.page) return; await this.page.addInitScript(() => { (window as any).__ROUTE_DEBUG__ = { changes: [], lastUrl: window.location.href, startTime: Date.now(), recordChange: function(from: string, to: string, type: string) { this.changes.push({ from, to, type, timestamp: new Date().toISOString(), duration: Date.now() - this.startTime }); this.lastUrl = to; this.startTime = Date.now(); } }; // Monitor pushState/replaceState const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function(...args) { const from = window.location.href; originalPushState.apply(history, args); const to = window.location.href; (window as any).__ROUTE_DEBUG__.recordChange(from, to, 'push'); }; history.replaceState = function(...args) { const from = window.location.href; originalReplaceState.apply(history, args); const to = window.location.href; (window as any).__ROUTE_DEBUG__.recordChange(from, to, 'replace'); }; // Monitor popstate (back/forward) window.addEventListener('popstate', () => { const from = (window as any).__ROUTE_DEBUG__.lastUrl; const to = window.location.href; (window as any).__ROUTE_DEBUG__.recordChange(from, to, 'pop'); }); }); } async getHydrationIssues(): Promise<HydrationMismatch[]> { if (!this.page) return []; const issues = await this.page.evaluate(() => { return (window as any).__HYDRATION_DEBUG__?.mismatches || []; }); return issues.map((issue: any) => ({ element: this.extractElementFromError(issue.message), serverHTML: this.extractServerHTML(issue.message), clientHTML: this.extractClientHTML(issue.message), timestamp: new Date(issue.timestamp), stackTrace: issue.stack })); } private extractElementFromError(message: string): string { // Try to extract element info from common error messages const patterns = [ /Warning:.*?<(\w+).*?>/, /component:?\s*(\w+)/i, /element\s+(\w+)/i ]; for (const pattern of patterns) { const match = message.match(pattern); if (match) return match[1]; } return 'Unknown element'; } private extractServerHTML(message: string): string { const match = message.match(/server:?\s*(.+?)(?:client:|$)/i); return match ? match[1].trim() : 'N/A'; } private extractClientHTML(message: string): string { const match = message.match(/client:?\s*(.+?)$/i); return match ? match[1].trim() : 'N/A'; } async getBundleAnalysis(): Promise<{ bundles: BundleInfo[]; totalSize: number; lazyLoadedSize: number; cacheHitRate: number; coverage?: { used: number; total: number; percentage?: number }; }> { const bundles = Array.from(this.bundles.values()); // Get JS coverage if available let coverage; if (this.page) { try { const jsCoverage = await this.page.coverage.stopJSCoverage(); const totalBytes = jsCoverage.reduce((sum, entry) => sum + (entry as any).source.length, 0); const usedBytes = jsCoverage.reduce((sum, entry) => { return sum + (entry.functions || []).reduce((s: number, func: any) => { return s + func.ranges.reduce((rs: number, range: any) => rs + (range.end - range.start), 0); }, 0); }, 0); coverage = { used: usedBytes, total: totalBytes }; // Update bundle coverage jsCoverage.forEach(entry => { const bundle = this.bundles.get(entry.url); if (bundle) { const usedInBundle = (entry.functions || []).reduce((s: number, func: any) => { return s + func.ranges.reduce((rs: number, range: any) => rs + (range.end - range.start), 0); }, 0); bundle.coverage = (usedInBundle / (entry as any).source.length) * 100; } }); // Restart coverage await this.page.coverage.startJSCoverage(); } catch (error) { console.error('Coverage analysis failed:', error); } } const totalSize = bundles.reduce((sum, b) => sum + b.size, 0); const lazyLoadedSize = bundles.filter(b => b.isLazyLoaded).reduce((sum, b) => sum + b.size, 0); const cacheHits = bundles.filter(b => b.cacheStatus === 'hit').length; const cacheHitRate = bundles.length > 0 ? (cacheHits / bundles.length) * 100 : 0; return { bundles: bundles.sort((a, b) => b.size - a.size), // Largest first totalSize, lazyLoadedSize, cacheHitRate, coverage }; } async getRouteChanges(): Promise<RouteChange[]> { if (!this.page) return this.routeChanges; const changes = await this.page.evaluate(() => { return (window as any).__ROUTE_DEBUG__?.changes || []; }); // Merge with any we've tracked const allChanges = [...this.routeChanges]; changes.forEach((change: any) => { if (!allChanges.some(c => c.timestamp.toISOString() === change.timestamp)) { allChanges.push({ from: change.from, to: change.to, duration: change.duration, timestamp: new Date(change.timestamp), type: change.type as 'push' | 'replace' | 'pop' }); } }); return allChanges.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); } async detectCommonProblems(): Promise<{ problem: string; severity: 'low' | 'medium' | 'high'; description: string; solution: string; }[]> { const problems = []; // Check hydration issues const hydrationIssues = await this.getHydrationIssues(); if (hydrationIssues.length > 0) { problems.push({ problem: 'Hydration Mismatches', severity: 'high' as const, description: `Found ${hydrationIssues.length} hydration errors. Server and client HTML don't match.`, solution: 'Ensure consistent rendering between server and client. Check for browser-only APIs, random values, or date/time differences.' }); } // Check bundle sizes const bundleAnalysis = await this.getBundleAnalysis(); const largeBundles = bundleAnalysis.bundles.filter(b => b.size > 500 * 1024); // 500KB if (largeBundles.length > 0) { problems.push({ problem: 'Large JavaScript Bundles', severity: 'medium' as const, description: `${largeBundles.length} bundles exceed 500KB. Largest: ${(largeBundles[0].size / 1024 / 1024).toFixed(2)}MB`, solution: 'Enable code splitting, lazy load routes, and tree-shake unused code.' }); } // Check code coverage if (bundleAnalysis.coverage) { const percentage = bundleAnalysis.coverage.total > 0 ? (bundleAnalysis.coverage.used / bundleAnalysis.coverage.total) * 100 : 0; if (percentage < 50) { problems.push({ problem: 'Low Code Usage', severity: 'medium' as const, description: `Only ${percentage.toFixed(1)}% of loaded JavaScript is being used.`, solution: 'Remove unused dependencies, enable tree-shaking, and lazy load features.' }); } } // Check route performance const routeChanges = await this.getRouteChanges(); const slowRoutes = routeChanges.filter(r => r.duration > 1000); // 1s if (slowRoutes.length > 0) { problems.push({ problem: 'Slow Route Transitions', severity: 'medium' as const, description: `${slowRoutes.length} route changes took over 1 second.`, solution: 'Optimize data fetching, add loading states, and consider prefetching.' }); } // Check cache efficiency if (bundleAnalysis.cacheHitRate < 50 && bundleAnalysis.bundles.length > 5) { problems.push({ problem: 'Poor Cache Utilization', severity: 'low' as const, description: `Only ${bundleAnalysis.cacheHitRate.toFixed(1)}% cache hit rate for JavaScript bundles.`, solution: 'Configure proper cache headers and use content hashing for static assets.' }); } return problems; } }