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

619 lines • 27.9 kB
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