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
368 lines • 15.6 kB
JavaScript
export class JavaScriptExecutionEngine {
page = null;
context = null;
isTracingEnabled = false;
currentTrace = {
functionCalls: new Map(),
variableStates: [],
codeCoverage: [],
errors: [],
timeline: []
};
callIdCounter = 0;
callStack = [];
attach(page, context) {
this.page = page;
this.context = context;
}
async enableTracing() {
if (!this.page) {
throw new Error('No page attached');
}
this.isTracingEnabled = true;
this.currentTrace = {
functionCalls: new Map(),
variableStates: [],
codeCoverage: [],
errors: [],
timeline: []
};
// Enable Chrome DevTools Protocol features
const client = await this.page.context().newCDPSession(this.page);
// Enable runtime
await client.send('Runtime.enable');
// Enable debugger
await client.send('Debugger.enable');
// Enable profiler for code coverage
await client.send('Profiler.enable');
await client.send('Profiler.startPreciseCoverage', {
callCount: true,
detailed: true
});
// Inject our tracing instrumentation
await this.injectTracingCode();
// Set up error tracking
this.page.on('pageerror', (error) => {
this.currentTrace.errors.push({
message: error.message,
stack: error.stack || '',
timestamp: Date.now(),
functionId: this.callStack[this.callStack.length - 1]
});
});
}
async disableTracing() {
if (!this.page || !this.isTracingEnabled) {
return;
}
const client = await this.page.context().newCDPSession(this.page);
// Get final code coverage
const coverage = await client.send('Profiler.takePreciseCoverage');
this.currentTrace.codeCoverage = coverage.result;
// Stop profiling
await client.send('Profiler.stopPreciseCoverage');
await client.send('Profiler.disable');
await client.send('Debugger.disable');
await client.send('Runtime.disable');
this.isTracingEnabled = false;
}
async injectTracingCode() {
if (!this.page)
return;
await this.page.addInitScript(() => {
// Create a global object to store our tracing data
window.__jsTrace = {
calls: new Map(),
callIdCounter: 0,
callStack: [],
timeline: [],
// Function to wrap other functions with tracing
wrapFunction: function (fn, name) {
return function (...args) {
const callId = `call_${window.__jsTrace.callIdCounter++}`;
const startTime = performance.now();
const parentId = window.__jsTrace.callStack[window.__jsTrace.callStack.length - 1];
// Record function call
const callInfo = {
id: callId,
name: name,
args: args.map(arg => {
try {
return JSON.parse(JSON.stringify(arg));
}
catch {
return String(arg);
}
}),
startTime: startTime,
callStack: [...window.__jsTrace.callStack],
parentId: parentId,
children: []
};
window.__jsTrace.calls.set(callId, callInfo);
window.__jsTrace.callStack.push(callId);
window.__jsTrace.timeline.push({
type: 'call',
functionId: callId,
timestamp: startTime
});
// Update parent's children
if (parentId) {
const parent = window.__jsTrace.calls.get(parentId);
if (parent) {
parent.children.push(callId);
}
}
try {
const result = fn.apply(this, args);
// Handle async functions
if (result && typeof result.then === 'function') {
return result.then((value) => {
const endTime = performance.now();
callInfo.returnValue = value;
callInfo.endTime = endTime;
callInfo.duration = endTime - startTime;
window.__jsTrace.callStack.pop();
window.__jsTrace.timeline.push({
type: 'return',
functionId: callId,
timestamp: endTime
});
return value;
}).catch((error) => {
const endTime = performance.now();
callInfo.error = error;
callInfo.endTime = endTime;
callInfo.duration = endTime - startTime;
window.__jsTrace.callStack.pop();
window.__jsTrace.timeline.push({
type: 'error',
functionId: callId,
timestamp: endTime
});
throw error;
});
}
// Sync function
const endTime = performance.now();
callInfo.returnValue = result;
callInfo.endTime = endTime;
callInfo.duration = endTime - startTime;
window.__jsTrace.callStack.pop();
window.__jsTrace.timeline.push({
type: 'return',
functionId: callId,
timestamp: endTime
});
return result;
}
catch (error) {
const endTime = performance.now();
callInfo.error = error;
callInfo.endTime = endTime;
callInfo.duration = endTime - startTime;
window.__jsTrace.callStack.pop();
window.__jsTrace.timeline.push({
type: 'error',
functionId: callId,
timestamp: endTime
});
throw error;
}
};
},
// Auto-instrument global functions
instrumentGlobals: function () {
// Instrument setTimeout/setInterval
const originalSetTimeout = window.setTimeout;
window.setTimeout = window.__jsTrace.wrapFunction(originalSetTimeout, 'setTimeout');
const originalSetInterval = window.setInterval;
window.setInterval = window.__jsTrace.wrapFunction(originalSetInterval, 'setInterval');
// Instrument Promise
const OriginalPromise = window.Promise;
window.Promise = new Proxy(OriginalPromise, {
construct(target, args) {
const executor = args[0];
const wrappedExecutor = window.__jsTrace.wrapFunction(executor, 'Promise.executor');
return new target(wrappedExecutor);
}
});
// Instrument fetch
const originalFetch = window.fetch;
window.fetch = window.__jsTrace.wrapFunction(originalFetch, 'fetch');
// Instrument console methods
['log', 'warn', 'error', 'info', 'debug'].forEach(method => {
const original = console[method];
console[method] = window.__jsTrace.wrapFunction(original, `console.${method}`);
});
}
};
// Start instrumentation
window.__jsTrace.instrumentGlobals();
});
}
async getExecutionTrace() {
if (!this.page) {
throw new Error('No page attached');
}
// Get trace data from the page
const traceData = await this.page.evaluate(() => {
const trace = window.__jsTrace;
if (!trace)
return null;
return {
calls: Array.from(trace.calls.entries()),
timeline: trace.timeline
};
});
if (traceData) {
// Convert the data back to our format
traceData.calls.forEach((entry) => {
const [id, call] = entry;
this.currentTrace.functionCalls.set(id, call);
});
this.currentTrace.timeline = traceData.timeline;
}
return this.currentTrace;
}
async instrumentFunction(functionPath, functionName) {
if (!this.page) {
throw new Error('No page attached');
}
await this.page.evaluate(({ path, name }) => {
const parts = path.split('.');
let obj = window;
for (let i = 0; i < parts.length - 1; i++) {
obj = obj[parts[i]];
if (!obj) {
throw new Error(`Cannot find object at path: ${parts.slice(0, i + 1).join('.')}`);
}
}
const funcName = parts[parts.length - 1];
const originalFunc = obj[funcName];
if (typeof originalFunc !== 'function') {
throw new Error(`${path} is not a function`);
}
obj[funcName] = window.__jsTrace.wrapFunction(originalFunc, name || path);
}, { path: functionPath, name: functionName });
}
async captureVariableState(variablePath, scope = 'global') {
if (!this.page) {
throw new Error('No page attached');
}
const state = await this.page.evaluate(({ path, scope }) => {
const parts = path.split('.');
let obj = scope === 'global' ? window : eval(scope);
let value = obj;
for (const part of parts) {
value = value[part];
if (value === undefined)
break;
}
return {
name: path,
value: value,
type: typeof value,
scope: scope,
timestamp: Date.now()
};
}, { path: variablePath, scope });
this.currentTrace.variableStates.push(state);
return state;
}
async getCallGraph() {
const trace = await this.getExecutionTrace();
// Build a hierarchical call graph
const rootCalls = [];
const callMap = new Map(trace.functionCalls);
for (const [id, call] of callMap) {
if (!call.parentId) {
rootCalls.push(this.buildCallTree(id, callMap));
}
}
return {
rootCalls,
totalCalls: callMap.size,
totalDuration: this.calculateTotalDuration(rootCalls),
errors: trace.errors
};
}
buildCallTree(callId, callMap) {
const call = callMap.get(callId);
if (!call)
return null;
const children = call.children
.map(childId => this.buildCallTree(childId, callMap))
.filter(child => child !== null);
return {
id: call.id,
name: call.name,
duration: call.duration,
hasError: !!call.error,
children
};
}
calculateTotalDuration(calls) {
return calls.reduce((total, call) => {
return total + (call.duration || 0);
}, 0);
}
async getPerformanceProfile() {
try {
if (!this.page || this.page.isClosed()) {
console.debug('⚠️ Performance profiling not available - page is closed or not attached');
return {
functions: [],
totalTime: 0,
error: 'Page not available for profiling'
};
}
const trace = await this.getExecutionTrace();
// Aggregate performance data by function name
const profile = new Map();
for (const [_, call] of trace.functionCalls) {
if (!call.duration)
continue;
const existing = profile.get(call.name) || {
callCount: 0,
totalDuration: 0,
avgDuration: 0,
minDuration: Infinity,
maxDuration: 0
};
existing.callCount++;
existing.totalDuration += call.duration;
existing.minDuration = Math.min(existing.minDuration, call.duration);
existing.maxDuration = Math.max(existing.maxDuration, call.duration);
existing.avgDuration = existing.totalDuration / existing.callCount;
profile.set(call.name, existing);
}
// Sort by total duration
const sorted = Array.from(profile.entries())
.sort((a, b) => b[1].totalDuration - a[1].totalDuration)
.map(([name, stats]) => ({ name, ...stats }));
const result = {
functions: sorted,
totalTime: sorted.reduce((sum, f) => sum + f.totalDuration, 0)
};
console.debug(`📊 Performance profile generated: ${sorted.length} functions, ${result.totalTime}ms total time`);
return result;
}
catch (error) {
console.debug('⚠️ Error generating performance profile:', error);
if (error.message.includes('Target page, context or browser has been closed')) {
return {
functions: [],
totalTime: 0,
error: 'Browser context closed during profiling'
};
}
return {
functions: [],
totalTime: 0,
error: error.message || 'Unknown profiling error'
};
}
}
}
//# sourceMappingURL=javascript-execution-engine.js.map