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
619 lines • 27.9 kB
JavaScript
import { EventListenerManager } from './utils/event-listener-manager.js';
export class ServerFrameworkDebugEngine {
page;
turboEvents = [];
csrfIssues = [];
liveReloadEvents = [];
phoenixLiveViewEvents = [];
eventListenerManager = new EventListenerManager();
async attachToPage(page) {
// Cleanup any existing listeners before attaching to new page
await this.cleanup();
this.page = page;
await this.injectServerFrameworkMonitoring();
await this.setupCSRFTracking();
await this.setupFormTracking();
}
async detectServerFramework(page) {
const detection = await page.evaluate(() => {
// Phoenix/LiveView detection
const hasPhoenixSocket = !!window.Phoenix;
const hasLiveSocket = !!window.liveSocket || !!window.LiveSocket;
const phoenixMeta = document.querySelector('meta[name="csrf-token"][content]');
const liveViewElements = document.querySelectorAll('[phx-session], [data-phx-session], [phx-static], [data-phx-static]');
const phoenixScripts = document.querySelector('script[src*="phoenix"]') ||
document.querySelector('script[src*="phoenix_live_view"]');
// Rails detection
const hasRailsUJS = !!window.Rails;
const hasTurbo = !!window.Turbo;
const hasActionCable = !!window.ActionCable;
const railsMeta = document.querySelector('meta[name="csrf-token"]');
const railsData = document.querySelector('[data-rails-form]') ||
document.querySelector('[data-remote="true"]');
// Django detection
const djangoCSRF = document.querySelector('input[name="csrfmiddlewaretoken"]');
const djangoAdmin = document.querySelector('.django-admin-container');
const djangoDebugToolbar = document.getElementById('djDebug');
const djangoMessages = document.querySelector('.django-messages');
// Phoenix/LiveView takes precedence (most specific)
if (hasPhoenixSocket || hasLiveSocket || liveViewElements.length > 0 || phoenixScripts) {
return 'phoenix';
}
if (hasRailsUJS || hasTurbo || hasActionCable || railsMeta || railsData) {
return 'rails';
}
if (djangoCSRF || djangoAdmin || djangoDebugToolbar || djangoMessages) {
return 'django';
}
return null;
});
return detection;
}
async injectServerFrameworkMonitoring() {
if (!this.page)
return;
await this.page.addInitScript(() => {
// Initialize browser-side EventListenerManager
window.__EVENT_LISTENER_MANAGER__ = {
listeners: new Map(),
addEventListener: function (target, event, handler) {
const listenerId = `${Date.now()}-${Math.random()}`;
target.addEventListener(event, handler);
this.listeners.set(listenerId, { target, event, handler, addedAt: new Date() });
return listenerId;
},
cleanup: function () {
for (const [id, listener] of this.listeners.entries()) {
try {
listener.target.removeEventListener(listener.event, listener.handler);
}
catch (error) {
console.warn('Failed to remove listener:', error);
}
}
this.listeners.clear();
},
getStats: function () {
return {
totalListeners: this.listeners.size,
listeners: Array.from(this.listeners.values())
};
}
};
window.__SERVER_FRAMEWORK_DEBUG__ = {
turboEvents: [],
stimulusControllers: [],
formSubmissions: [],
ajaxRequests: [],
phoenixLiveViewEvents: [],
init: function () {
// Monitor Phoenix LiveView
if (window.liveSocket || window.LiveSocket) {
this.monitorPhoenixLiveView();
}
// Monitor Turbo (Rails)
if (window.Turbo) {
this.monitorTurbo();
}
// Monitor Stimulus
if (window.Stimulus) {
this.monitorStimulus();
}
// Monitor Rails UJS
if (window.Rails) {
this.monitorRailsUJS();
}
// Monitor Django forms
this.monitorForms();
// Monitor AJAX/Fetch
this.monitorAjax();
},
monitorPhoenixLiveView: function () {
const debug = this;
const liveSocket = window.liveSocket;
if (!liveSocket)
return;
// Hook into LiveView lifecycle events
const originalLog = liveSocket.log;
if (originalLog) {
liveSocket.log = function (kind, msgCallback, obj) {
if (kind === 'event' || kind === 'receive' || kind === 'push') {
debug.phoenixLiveViewEvents.push({
type: kind,
event: obj?.event,
params: obj?.payload,
timestamp: new Date().toISOString()
});
}
return originalLog.call(this, kind, msgCallback, obj);
};
}
// Monitor phx: events on DOM elements
const eventManager = window.__EVENT_LISTENER_MANAGER__;
const phxEvents = ['phx:page-loading-start', 'phx:page-loading-stop', 'phx:disconnect', 'phx:error'];
phxEvents.forEach(eventName => {
eventManager.addEventListener(window, eventName, (event) => {
debug.phoenixLiveViewEvents.push({
type: 'event',
event: eventName,
params: event.detail,
timestamp: new Date().toISOString()
});
});
});
},
monitorTurbo: function () {
const debug = this;
const eventManager = window.__EVENT_LISTENER_MANAGER__;
eventManager.addEventListener(document, 'turbo:visit', (event) => {
debug.turboEvents.push({
type: 'visit',
target: event.detail.url,
timing: Date.now(),
timestamp: new Date().toISOString()
});
});
eventManager.addEventListener(document, 'turbo:cache-miss', (event) => {
debug.turboEvents.push({
type: 'cache-miss',
target: window.location.href,
timing: Date.now(),
timestamp: new Date().toISOString()
});
});
eventManager.addEventListener(document, 'turbo:frame-load', (event) => {
debug.turboEvents.push({
type: 'frame-load',
target: event.target.id,
timing: Date.now(),
timestamp: new Date().toISOString()
});
});
eventManager.addEventListener(document, 'turbo:submit-start', (event) => {
const form = event.target;
debug.formSubmissions.push({
action: form.action,
method: form.method,
turbo: true,
hasFile: form.querySelector('input[type="file"]') !== null,
timestamp: new Date().toISOString()
});
});
},
monitorStimulus: function () {
const debug = this;
// Try to access Stimulus application
const app = window.Stimulus;
if (!app)
return;
// Monitor controller connections
const originalRegister = app.register;
app.register = function (identifier, controller) {
debug.stimulusControllers.push({
identifier,
controller: controller.name,
timestamp: new Date().toISOString()
});
return originalRegister.call(this, identifier, controller);
};
},
monitorRailsUJS: function () {
const debug = this;
const eventManager = window.__EVENT_LISTENER_MANAGER__;
eventManager.addEventListener(document, 'ajax:send', (event) => {
debug.ajaxRequests.push({
url: event.detail[0].url,
method: event.detail[0].type,
rails: true,
timestamp: new Date().toISOString()
});
});
eventManager.addEventListener(document, 'ajax:error', (event) => {
const lastRequest = debug.ajaxRequests[debug.ajaxRequests.length - 1];
if (lastRequest) {
lastRequest.error = true;
lastRequest.status = event.detail[2].status;
}
});
},
monitorForms: function () {
const debug = this;
const eventManager = window.__EVENT_LISTENER_MANAGER__;
eventManager.addEventListener(document, 'submit', (event) => {
const form = event.target;
if (form.tagName !== 'FORM')
return;
// Check for CSRF token
const railsToken = form.querySelector('input[name="authenticity_token"]');
const djangoToken = form.querySelector('input[name="csrfmiddlewaretoken"]');
debug.formSubmissions.push({
action: form.action,
method: form.method,
hasCSRF: !!(railsToken || djangoToken),
csrfField: railsToken ? 'authenticity_token' : djangoToken ? 'csrfmiddlewaretoken' : null,
hasFile: form.querySelector('input[type="file"]') !== null,
timestamp: new Date().toISOString()
});
});
},
monitorAjax: function () {
const debug = this;
// Monitor fetch
const originalFetch = window.fetch;
window.fetch = async function (...args) {
const url = typeof args[0] === 'string' ? args[0] : args[0].url;
const options = args[1] || {};
const request = {
url,
method: options.method || 'GET',
hasCSRF: false,
timestamp: new Date().toISOString()
};
// Check for CSRF in headers
if (options.headers) {
const headers = options.headers;
request.hasCSRF = !!(headers['X-CSRF-Token'] || headers['X-CSRFToken']);
}
debug.ajaxRequests.push(request);
try {
const response = await originalFetch.apply(this, args);
request.status = response.status;
return response;
}
catch (error) {
request.error = true;
throw error;
}
};
}
};
// Initialize after DOM ready
if (document.readyState === 'loading') {
const eventManager = window.__EVENT_LISTENER_MANAGER__;
eventManager.addEventListener(document, 'DOMContentLoaded', () => {
window.__SERVER_FRAMEWORK_DEBUG__.init();
});
}
else {
window.__SERVER_FRAMEWORK_DEBUG__.init();
}
});
}
async setupCSRFTracking() {
if (!this.page)
return;
// Monitor network requests for CSRF issues
this.page.on('response', async (response) => {
const request = response.request();
const method = request.method();
// Only check state-changing methods
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
const headers = request.headers();
const url = request.url();
// Check for CSRF tokens
const hasCSRFHeader = !!(headers['x-csrf-token'] ||
headers['x-csrftoken'] ||
headers['x-xsrf-token'] ||
headers['x-requested-with'] === 'XMLHttpRequest');
// Check response for CSRF errors
if (response.status() === 403) {
const text = await response.text().catch(() => '');
if (text.includes('CSRF') || text.includes('csrf')) {
this.csrfIssues.push({
endpoint: url,
method,
hasToken: hasCSRFHeader,
tokenName: 'Unknown',
timestamp: new Date()
});
}
}
}
});
}
async setupFormTracking() {
if (!this.page)
return;
// Track form submissions for multipart issues
await this.page.addInitScript(() => {
const originalSubmit = HTMLFormElement.prototype.submit;
HTMLFormElement.prototype.submit = function () {
const hasFile = this.querySelector('input[type="file"]') !== null;
const enctype = this.getAttribute('enctype');
if (hasFile && enctype !== 'multipart/form-data') {
console.error('Form with file input missing multipart/form-data enctype');
}
return originalSubmit.call(this);
};
});
}
async getTurboEvents() {
if (!this.page)
return [];
const events = await this.page.evaluate(() => {
return window.__SERVER_FRAMEWORK_DEBUG__?.turboEvents || [];
});
return events.map((e) => ({
...e,
timestamp: new Date(e.timestamp)
}));
}
async getStimulusControllers() {
if (!this.page)
return [];
return await this.page.evaluate(() => {
const controllers = [];
// Find all Stimulus controlled elements
document.querySelectorAll('[data-controller]').forEach((element) => {
const identifier = element.getAttribute('data-controller') || '';
// Extract actions
const actionAttr = element.getAttribute('data-action') || '';
const actions = actionAttr.split(' ').filter(a => a.length > 0);
// Extract targets
const targets = [];
Array.from(element.attributes).forEach(attr => {
if (attr.name.startsWith('data-') && attr.name.endsWith('-target')) {
targets.push(attr.value);
}
});
controllers.push({
identifier,
element: element.tagName + (element.id ? `#${element.id}` : ''),
actions,
targets,
connected: true // Assume connected if found in DOM
});
});
return controllers;
});
}
async getFormSubmissions() {
if (!this.page)
return [];
return await this.page.evaluate(() => {
return window.__SERVER_FRAMEWORK_DEBUG__?.formSubmissions || [];
});
}
async getCSRFIssues() {
return this.csrfIssues;
}
async detectServerFrameworkProblems() {
const problems = [];
const framework = await this.detectServerFramework(this.page);
// CSRF Issues
const csrfIssues = await this.getCSRFIssues();
if (csrfIssues.length > 0) {
problems.push({
problem: 'CSRF Token Issues',
severity: 'high',
description: `${csrfIssues.length} requests failed or missing CSRF tokens.`,
solution: framework === 'rails'
? 'Ensure <%= csrf_meta_tags %> in layout and use Rails UJS or include token in AJAX headers.'
: 'Include {% csrf_token %} in forms and add token to AJAX requests.'
});
}
// Form Issues
const forms = await this.getFormSubmissions();
const formsWithoutCSRF = forms.filter(f => !f.hasCSRF && f.method.toUpperCase() !== 'GET');
if (formsWithoutCSRF.length > 0) {
problems.push({
problem: 'Forms Missing CSRF Protection',
severity: 'high',
description: `${formsWithoutCSRF.length} forms lack CSRF tokens.`,
solution: framework === 'rails'
? 'Add <%= form_with %> or <%= form_tag %> helpers which include CSRF automatically.'
: 'Ensure {% csrf_token %} is inside all Django forms.'
});
}
// Multipart form issues
const fileFormsWithoutMultipart = forms.filter(f => f.hasFile && f.enctype !== 'multipart/form-data');
if (fileFormsWithoutMultipart.length > 0) {
problems.push({
problem: 'File Upload Forms Misconfigured',
severity: 'high',
description: `${fileFormsWithoutMultipart.length} file upload forms missing multipart encoding.`,
solution: 'Add enctype="multipart/form-data" to forms with file inputs.'
});
}
// Turbo/Hotwire Issues (Rails)
if (framework === 'rails') {
const turboEvents = await this.getTurboEvents();
const cacheMisses = turboEvents.filter(e => e.type === 'cache-miss');
if (cacheMisses.length > turboEvents.filter(e => e.type === 'visit').length * 0.5) {
problems.push({
problem: 'Poor Turbo Cache Hit Rate',
severity: 'low',
description: `Over 50% of Turbo visits are cache misses, reducing navigation speed.`,
solution: 'Ensure Turbo cache is enabled and pages are cacheable. Avoid dynamic content in cached pages.'
});
}
// Check for Stimulus issues
const controllers = await this.getStimulusControllers();
const duplicateControllers = controllers.filter((c, i) => controllers.findIndex(c2 => c2.identifier === c.identifier) !== i);
if (duplicateControllers.length > 0) {
problems.push({
problem: 'Duplicate Stimulus Controllers',
severity: 'medium',
description: `Found duplicate controller registrations which may cause conflicts.`,
solution: 'Ensure each Stimulus controller is registered only once.'
});
}
}
// AJAX without CSRF
const ajaxRequests = await this.page.evaluate(() => window.__SERVER_FRAMEWORK_DEBUG__?.ajaxRequests || []);
const unsafeAjax = ajaxRequests.filter((r) => ['POST', 'PUT', 'PATCH', 'DELETE'].includes(r.method.toUpperCase()) && !r.hasCSRF);
if (unsafeAjax.length > 0) {
problems.push({
problem: 'AJAX Requests Missing CSRF',
severity: 'high',
description: `${unsafeAjax.length} state-changing AJAX requests lack CSRF tokens.`,
solution: framework === 'rails'
? 'Use Rails.ajax() or add X-CSRF-Token header from meta tag.'
: 'Add X-CSRFToken header from cookie or hidden input.'
});
}
return problems;
}
async getLiveReloadStats() {
const events = this.liveReloadEvents;
const avgReloadTime = events.length > 0
? events.reduce((sum, e) => sum + e.reloadTime, 0) / events.length
: 0;
const fileTypes = {};
events.forEach(e => {
fileTypes[e.type] = (fileTypes[e.type] || 0) + 1;
});
return { events, avgReloadTime, fileTypes };
}
/**
* Get tracked listeners for debugging and monitoring
*/
async getTrackedListeners() {
if (!this.page)
return [];
try {
const browserListeners = await this.page.evaluate(() => {
const eventManager = window.__EVENT_LISTENER_MANAGER__;
return eventManager ? eventManager.getStats() : { totalListeners: 0, listeners: [] };
});
return browserListeners.listeners || [];
}
catch (error) {
console.error('Failed to get tracked listeners:', error);
return [];
}
}
/**
* Get count of active event listeners
*/
getActiveListenerCount() {
return this.eventListenerManager.getActiveListenerCount();
}
/**
* Get count of active browser-side event listeners
*/
async getBrowserListenerCount() {
if (!this.page)
return 0;
try {
return await this.page.evaluate(() => {
const eventManager = window.__EVENT_LISTENER_MANAGER__;
return eventManager ? eventManager.getStats().totalListeners : 0;
});
}
catch (error) {
console.error('Failed to get browser listener count:', error);
return 0;
}
}
async getPhoenixLiveViewEvents() {
if (!this.page)
return [];
const events = await this.page.evaluate(() => {
return window.__SERVER_FRAMEWORK_DEBUG__?.phoenixLiveViewEvents || [];
});
return events.map((e) => ({
...e,
timestamp: new Date(e.timestamp)
}));
}
async getPhoenixSocketInfo() {
if (!this.page)
return [];
return await this.page.evaluate(() => {
const sockets = [];
const liveSocket = window.liveSocket;
if (liveSocket && liveSocket.socket) {
const socket = liveSocket.socket;
sockets.push({
id: socket.endPointURL || 'unknown',
state: socket.connectionState?.() || socket.isConnected?.() ? 'open' : 'closed',
channels: Object.keys(socket.channels || {}).map((topic) => {
const channel = socket.channels[topic];
return `${topic} (${channel.state || 'unknown'})`;
}),
lastHeartbeat: socket.lastHeartbeatAt ? new Date(socket.lastHeartbeatAt) : undefined
});
}
return sockets;
});
}
async getLiveViewInfo() {
if (!this.page)
return null;
return await this.page.evaluate(() => {
const hooks = document.querySelectorAll('[phx-hook]');
const components = document.querySelectorAll('[data-phx-component], [phx-component]');
const views = document.querySelectorAll('[data-phx-view], [phx-view]');
return {
hooks: Array.from(hooks).map((el) => ({
name: el.getAttribute('phx-hook'),
id: el.id,
classes: el.className
})),
components: components.length,
views: views.length,
liveSocketConnected: !!window.liveSocket?.isConnected?.()
};
});
}
/**
* Cleanup all event listeners to prevent memory leaks
*/
async cleanup() {
// Cleanup browser-side listeners
if (this.page) {
try {
await this.page.evaluate(() => {
const eventManager = window.__EVENT_LISTENER_MANAGER__;
if (eventManager) {
eventManager.cleanup();
}
});
}
catch (error) {
console.error('Failed to cleanup browser listeners:', error);
}
}
// Cleanup Node.js side listeners
this.eventListenerManager.cleanup();
// Clear internal state
this.turboEvents = [];
this.csrfIssues = [];
this.liveReloadEvents = [];
}
/**
* Get detailed listener statistics for debugging
*/
async getListenerStats() {
const nodeListeners = this.getActiveListenerCount();
const browserListeners = await this.getBrowserListenerCount();
let browserEventTypes = {};
if (this.page) {
try {
const browserStats = await this.page.evaluate(() => {
const eventManager = window.__EVENT_LISTENER_MANAGER__;
if (!eventManager)
return { listeners: [] };
const stats = eventManager.getStats();
const eventTypes = {};
for (const listener of stats.listeners) {
eventTypes[listener.event] = (eventTypes[listener.event] || 0) + 1;
}
return { eventTypes };
});
browserEventTypes = browserStats.eventTypes || {};
}
catch (error) {
console.error('Failed to get browser event types:', error);
}
}
return {
nodeListeners,
browserListeners,
totalListeners: nodeListeners + browserListeners,
eventTypes: browserEventTypes
};
}
}
//# sourceMappingURL=server-framework-debug-engine.js.map