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
text/typescript
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;
}
}