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
430 lines • 17.7 kB
JavaScript
export class AsyncTrackingEngine {
page = null;
isTracking = false;
asyncTrace = {
operations: new Map(),
timeline: [],
eventLoopSnapshots: [],
promiseChains: new Map()
};
operationCounter = 0;
snapshotInterval = null;
attach(page) {
this.page = page;
}
async enableTracking() {
if (!this.page) {
throw new Error('No page attached');
}
this.isTracking = true;
this.asyncTrace = {
operations: new Map(),
timeline: [],
eventLoopSnapshots: [],
promiseChains: new Map()
};
// Inject async tracking code
await this.injectAsyncTracking();
// Start periodic event loop snapshots
this.snapshotInterval = setInterval(() => {
this.captureEventLoopSnapshot();
}, 100); // Every 100ms
}
async disableTracking() {
this.isTracking = false;
if (this.snapshotInterval) {
clearInterval(this.snapshotInterval);
this.snapshotInterval = null;
}
}
async injectAsyncTracking() {
if (!this.page)
return;
await this.page.addInitScript(() => {
window.__asyncTrace = {
operations: new Map(),
operationCounter: 0,
promiseChains: new Map(),
createOperation: function (type, metadata) {
const id = `async_${window.__asyncTrace.operationCounter++}`;
const stack = new Error().stack?.split('\n').slice(2) || [];
const operation = {
id,
type,
status: 'pending',
createdAt: performance.now(),
stack,
metadata
};
window.__asyncTrace.operations.set(id, operation);
// Send timeline event
window.postMessage({
type: '__asyncTrace',
event: 'created',
operationId: id,
timestamp: performance.now()
}, '*');
return id;
},
resolveOperation: function (id, value) {
const op = window.__asyncTrace.operations.get(id);
if (op) {
op.status = 'resolved';
op.resolvedAt = performance.now();
op.duration = op.resolvedAt - op.createdAt;
op.value = value;
window.postMessage({
type: '__asyncTrace',
event: 'resolved',
operationId: id,
timestamp: performance.now()
}, '*');
}
},
rejectOperation: function (id, error) {
const op = window.__asyncTrace.operations.get(id);
if (op) {
op.status = 'rejected';
op.resolvedAt = performance.now();
op.duration = op.resolvedAt - op.createdAt;
op.error = error;
window.postMessage({
type: '__asyncTrace',
event: 'rejected',
operationId: id,
timestamp: performance.now()
}, '*');
}
},
linkPromises: function (parentId, childId) {
const children = window.__asyncTrace.promiseChains.get(parentId) || [];
children.push(childId);
window.__asyncTrace.promiseChains.set(parentId, children);
const childOp = window.__asyncTrace.operations.get(childId);
if (childOp) {
childOp.parentId = parentId;
}
}
};
// Track Promises
const OriginalPromise = window.Promise;
const asyncTrace = window.__asyncTrace;
// Create a properly typed Promise constructor
const TrackedPromise = function (executor) {
const asyncId = asyncTrace.createOperation('promise');
let isResolved = false;
const promise = new OriginalPromise((resolve, reject) => {
const trackedResolve = (value) => {
if (!isResolved) {
isResolved = true;
asyncTrace.resolveOperation(asyncId, value);
}
return resolve(value);
};
const trackedReject = (reason) => {
if (!isResolved) {
isResolved = true;
asyncTrace.rejectOperation(asyncId, reason);
}
return reject(reason);
};
try {
executor(trackedResolve, trackedReject);
}
catch (error) {
trackedReject(error);
}
});
promise._asyncId = asyncId;
return promise;
};
// Copy static methods
TrackedPromise.resolve = OriginalPromise.resolve;
TrackedPromise.reject = OriginalPromise.reject;
TrackedPromise.all = OriginalPromise.all;
TrackedPromise.race = OriginalPromise.race;
// Set prototype
TrackedPromise.prototype = OriginalPromise.prototype;
window.Promise = TrackedPromise;
// Track setTimeout
const originalSetTimeout = window.setTimeout;
window.setTimeout = function (handler, delay, ...args) {
const asyncId = asyncTrace.createOperation('timeout', { delay });
const wrappedHandler = function () {
asyncTrace.resolveOperation(asyncId);
if (typeof handler === 'function') {
return handler.apply(this, args);
}
};
return originalSetTimeout(wrappedHandler, delay);
};
// Track setInterval
const originalSetInterval = window.setInterval;
const intervalMap = new Map();
window.setInterval = function (handler, delay, ...args) {
const asyncId = asyncTrace.createOperation('interval', { delay });
let count = 0;
const wrappedHandler = function () {
// Create a new operation for each interval tick
const tickId = asyncTrace.createOperation('interval', {
delay,
parentInterval: asyncId,
tick: ++count
});
asyncTrace.resolveOperation(tickId);
if (typeof handler === 'function') {
return handler.apply(this, args);
}
};
const intervalId = originalSetInterval(wrappedHandler, delay);
intervalMap.set(intervalId, asyncId);
return intervalId;
};
// Track clearInterval
const originalClearInterval = window.clearInterval;
window.clearInterval = function (intervalId) {
const asyncId = intervalMap.get(intervalId);
if (asyncId) {
const op = asyncTrace.operations.get(asyncId);
if (op) {
op.status = 'cancelled';
op.resolvedAt = performance.now();
op.duration = op.resolvedAt - op.createdAt;
}
intervalMap.delete(intervalId);
}
return originalClearInterval(intervalId);
};
// Track requestAnimationFrame
const originalRAF = window.requestAnimationFrame;
window.requestAnimationFrame = function (callback) {
const asyncId = asyncTrace.createOperation('animationFrame');
const wrappedCallback = function (timestamp) {
asyncTrace.resolveOperation(asyncId, timestamp);
return callback(timestamp);
};
return originalRAF(wrappedCallback);
};
// Track fetch
const originalFetch = window.fetch;
window.fetch = function (input, init) {
const url = typeof input === 'string' ? input :
input instanceof URL ? input.href : input.url;
const asyncId = asyncTrace.createOperation('fetch', { url });
const promise = originalFetch(input, init);
promise._asyncId = asyncId;
promise.then(response => {
asyncTrace.resolveOperation(asyncId, {
status: response.status,
statusText: response.statusText,
headers: (() => {
const hdrs = {};
response.headers.forEach((value, key) => {
hdrs[key] = value;
});
return hdrs;
})()
});
return response;
}, error => {
asyncTrace.rejectOperation(asyncId, error);
throw error;
});
return promise;
};
// Track event listeners
const originalAddEventListener = EventTarget.prototype.addEventListener;
const eventListenerMap = new Map();
EventTarget.prototype.addEventListener = function (type, listener, options) {
const target = this.constructor.name;
const asyncId = asyncTrace.createOperation('event', {
eventType: type,
target
});
const key = `${target}_${type}`;
const listeners = eventListenerMap.get(key) || new Set();
listeners.add(asyncId);
eventListenerMap.set(key, listeners);
const wrappedListener = function (event) {
// Create a new operation for each event dispatch
const dispatchId = asyncTrace.createOperation('event', {
eventType: type,
target,
parentListener: asyncId
});
try {
const result = listener.call(this, event);
asyncTrace.resolveOperation(dispatchId, result);
return result;
}
catch (error) {
asyncTrace.rejectOperation(dispatchId, error);
throw error;
}
};
return originalAddEventListener.call(this, type, wrappedListener, options);
};
});
// Listen for async trace events
await this.page.evaluate(() => {
window.addEventListener('message', (event) => {
if (event.data && event.data.type === '__asyncTrace') {
// Store events for later retrieval
window.__asyncTraceEvents = window.__asyncTraceEvents || [];
window.__asyncTraceEvents.push({
event: event.data.event,
operationId: event.data.operationId,
timestamp: event.data.timestamp
});
}
});
});
}
async captureEventLoopSnapshot() {
if (!this.page || !this.isTracking)
return;
try {
const snapshot = await this.page.evaluate(() => {
const trace = window.__asyncTrace;
if (!trace)
return null;
const now = performance.now();
const snapshot = {
timestamp: now,
macrotaskQueue: [],
microtaskQueue: [],
pendingPromises: [],
activeTimers: [],
animationFrames: []
};
// Categorize operations
for (const [id, op] of trace.operations) {
if (op.status === 'pending') {
switch (op.type) {
case 'promise':
snapshot.pendingPromises.push(op);
break;
case 'timeout':
case 'interval':
snapshot.activeTimers.push(op);
break;
case 'animationFrame':
snapshot.animationFrames.push(op);
break;
case 'fetch':
snapshot.macrotaskQueue.push(op);
break;
}
}
}
return snapshot;
});
if (snapshot) {
this.asyncTrace.eventLoopSnapshots.push(snapshot);
}
}
catch (error) {
// Page might be navigating or closed
}
}
async getAsyncTrace() {
if (!this.page) {
throw new Error('No page attached');
}
// Get trace data from page
const traceData = await this.page.evaluate(() => {
const trace = window.__asyncTrace;
const events = window.__asyncTraceEvents || [];
if (!trace)
return null;
return {
operations: Array.from(trace.operations.entries()),
promiseChains: Array.from(trace.promiseChains.entries()),
events
};
});
if (traceData) {
// Convert to our format
this.asyncTrace.operations.clear();
traceData.operations.forEach((entry) => {
const [id, op] = entry;
this.asyncTrace.operations.set(id, op);
});
this.asyncTrace.promiseChains.clear();
traceData.promiseChains.forEach((entry) => {
const [parentId, childIds] = entry;
this.asyncTrace.promiseChains.set(parentId, childIds);
});
this.asyncTrace.timeline = traceData.events.map((e) => ({
timestamp: e.timestamp,
type: e.event,
operationId: e.operationId
}));
}
return this.asyncTrace;
}
async getPromiseChain(promiseId) {
const trace = await this.getAsyncTrace();
const chain = [];
const visited = new Set();
const buildChain = (id) => {
if (visited.has(id))
return;
visited.add(id);
const op = trace.operations.get(id);
if (op) {
chain.push(op);
const children = trace.promiseChains.get(id) || [];
children.forEach(childId => buildChain(childId));
}
};
buildChain(promiseId);
return chain;
}
async getAsyncStackTrace(operationId) {
const trace = await this.getAsyncTrace();
const operation = trace.operations.get(operationId);
if (!operation) {
return [];
}
const fullStack = [...operation.stack];
// Add parent async contexts
let currentOp = operation;
while (currentOp.parentId) {
const parent = trace.operations.get(currentOp.parentId);
if (parent) {
fullStack.push(`--- async ---`);
fullStack.push(...parent.stack);
currentOp = parent;
}
else {
break;
}
}
return fullStack;
}
async detectAsyncLeaks() {
const trace = await this.getAsyncTrace();
const now = performance.now();
const leaks = new Map();
// Find long-running pending operations
for (const [_, op] of trace.operations) {
if (op.status === 'pending') {
const age = now - op.createdAt;
if (age > 5000) { // 5 seconds threshold
const key = op.type;
const list = leaks.get(key) || [];
list.push(op);
leaks.set(key, list);
}
}
}
return Array.from(leaks.entries()).map(([type, operations]) => ({
type,
count: operations.length,
oldestAge: Math.max(...operations.map(op => now - op.createdAt)),
operations: operations.sort((a, b) => a.createdAt - b.createdAt).slice(0, 10)
}));
}
}
//# sourceMappingURL=async-tracking-engine.js.map