UNPKG

@digitalnodecom/node-red-contrib-analyzer

Version:

A Node-RED global service that monitors function nodes for debugging artifacts and performance issues. Features real-time quality metrics, Vue.js dashboard, and comprehensive code analysis.

639 lines (557 loc) 26.6 kB
const path = require('path'); const express = require('express'); const cors = require('cors'); const { initDatabase } = require('./database/db'); const apiRouter = require('./api/router'); const { detectDebuggingTraits } = require('./detection/detector'); const QualityMetrics = require('./detection/quality-metrics'); const PerformanceMonitor = require('./monitoring/performance-monitor'); module.exports = function(RED) { // Global state let db = null; let scanTimer = null; let performanceTimer = null; let settings = {}; let isScanning = false; // Initialize database on Node-RED startup - but don't start services yet (async () => { if (!db) { const userDir = RED.settings.userDir || process.cwd(); const dbPath = path.join(userDir, 'analyzer.db'); try { db = await initDatabase(dbPath); console.log('Code Analyzer global service initialized at:', dbPath); // Load settings from database await loadSettings(); console.log('Code Analyzer: Database and settings loaded, waiting for proper startup trigger...'); } catch (error) { console.error('Failed to initialize Code Analyzer:', error); } } })(); // Load settings from database async function loadSettings() { if (!db) return; try { const settingsRows = await db.all('SELECT key, value, type FROM settings'); settings = {}; settingsRows.forEach(row => { let value = row.value; if (row.type === 'boolean') { value = row.value === 'true'; } else if (row.type === 'number') { value = parseFloat(row.value); } settings[row.key] = value; }); console.log('Code Analyzer settings loaded:', Object.keys(settings).length, 'settings'); } catch (error) { console.error('Failed to load settings:', error); // Set default values settings = { codeAnalysis: true, scanInterval: 0, // Disabled - on-demand only detectionLevel: 1, autoStart: false, // No automatic startup performanceMonitoring: false, performanceInterval: 10 }; } } // Global background scanning service async function performGlobalScan() { if (isScanning || !settings.codeAnalysis) return; // Check if Node-RED runtime is ready if (!RED.nodes || !RED.nodes.eachNode) { console.log('Code Analyzer: Node-RED runtime not ready, skipping scan'); return; } isScanning = true; try { const qualityMetrics = new QualityMetrics(); const flowResults = new Map(); // Safety check before scanning flows let nodeCount = 0; try { RED.nodes.eachNode(() => nodeCount++); } catch (error) { console.log('Code Analyzer: Node-RED flows not ready, skipping scan'); isScanning = false; return; } if (nodeCount === 0) { console.log('Code Analyzer: No nodes found, skipping scan'); isScanning = false; return; } // Scan all flows RED.nodes.eachNode(function(nodeConfig) { if (nodeConfig.type === 'tab') { flowResults.set(nodeConfig.id, { flowId: nodeConfig.id, flowName: nodeConfig.label || nodeConfig.name || `Flow ${nodeConfig.id.substring(0, 8)}`, functionNodes: [], totalIssues: 0, nodesWithIssues: 0 }); } }); // Scan all function nodes RED.nodes.eachNode(async function(nodeConfig) { if (nodeConfig.type === 'function' && nodeConfig.func && nodeConfig.z) { const flowResult = flowResults.get(nodeConfig.z); if (!flowResult) return; const issues = detectDebuggingTraits(nodeConfig.func, settings.detectionLevel || 1); const linesOfCode = nodeConfig.func.split('\n').length; const complexityScore = qualityMetrics.calculateComplexityScore(nodeConfig.func); const nodeQualityScore = qualityMetrics.calculateNodeQualityScore(issues, linesOfCode); flowResult.functionNodes.push({ nodeId: nodeConfig.id, nodeName: nodeConfig.name || `Function Node ${nodeConfig.id.substring(0, 8)}`, linesOfCode, complexityScore, qualityScore: nodeQualityScore, issues, issuesCount: issues.length }); if (issues.length > 0) { flowResult.totalIssues += issues.length; flowResult.nodesWithIssues += 1; // Update node status if possible const runtimeNode = RED.nodes.getNode(nodeConfig.id); if (runtimeNode && runtimeNode.status) { const hasLevel1 = issues.some(issue => issue.type === 'top-level-return'); const hasLevel2 = issues.some(issue => issue.type === 'node-warn' || issue.type === 'todo-comment' || issue.type === 'console-log' || issue.type === 'debugger-statement' ); let statusColor = 'blue'; let text = 'Minor debug traits noticed'; if (hasLevel1) { statusColor = 'red'; text = 'Severe debugging traits'; } else if (hasLevel2) { statusColor = 'yellow'; text = 'Important debugging traits'; } runtimeNode.status({ fill: statusColor, shape: 'dot', text }); } } else { // Clear node status for clean nodes const runtimeNode = RED.nodes.getNode(nodeConfig.id); if (runtimeNode && runtimeNode.status) { runtimeNode.status({}); } } // Store node quality metrics for ALL nodes (with or without issues) if (db) { try { await db.run(`INSERT INTO node_quality_metrics (flow_id, node_id, node_name, issues_count, issue_details, quality_score, complexity_score, lines_of_code) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [ nodeConfig.z, nodeConfig.id, nodeConfig.name || `Function Node ${nodeConfig.id.substring(0, 8)}`, issues.length, JSON.stringify(issues), nodeQualityScore, complexityScore, linesOfCode ]); } catch (err) { console.error('Failed to store node quality metrics:', err); } } } }); // Store flow-level quality metrics if (db) { for (const [flowId, flowResult] of flowResults) { if (flowResult.functionNodes.length > 0) { try { // Calculate flow quality metrics from node results directly const totalIssues = flowResult.functionNodes.reduce((sum, n) => sum + n.issuesCount, 0); const nodesWithIssues = flowResult.functionNodes.filter(n => n.issuesCount > 0).length; const nodesWithCriticalIssues = flowResult.functionNodes.filter(n => n.issues.some(i => ['top-level-return', 'debugger-statement'].includes(i.type)) ).length; const totalFunctionNodes = flowResult.functionNodes.length; // Calculate weighted average quality score const totalWeightedQuality = flowResult.functionNodes.reduce((sum, n) => sum + n.qualityScore, 0); let flowQualityScore = totalWeightedQuality / totalFunctionNodes; // Apply penalties for critical issues if (nodesWithCriticalIssues > 0) { flowQualityScore = Math.min(flowQualityScore, 50); } // Calculate weighted average complexity const avgComplexity = flowResult.functionNodes.reduce((sum, n) => sum + n.complexityScore, 0) / totalFunctionNodes; await db.run(`INSERT INTO code_quality_metrics (flow_id, flow_name, total_issues, nodes_with_issues, nodes_with_critical_issues, total_function_nodes, issue_types, quality_score, complexity_score) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ flowId, flowResult.flowName, totalIssues, nodesWithIssues, nodesWithCriticalIssues, totalFunctionNodes, JSON.stringify({}) /* Issue types aggregation */, Math.max(0, Math.round(flowQualityScore * 100) / 100), Math.round(avgComplexity * 100) / 100 ]); } catch (err) { console.error('Failed to store flow quality metrics:', err); } } } } console.log(`Code Analyzer global scan completed: ${flowResults.size} flows processed`); } catch (error) { console.error('Global scan error:', error); } finally { isScanning = false; } } // Global performance monitor instance let performanceMonitor = null; // Performance monitoring service async function performPerformanceCheck() { if (!settings.performanceMonitoring) return; try { // Get current system metrics directly const now = Date.now(); const memoryUsage = process.memoryUsage(); const totalMemory = require('os').totalmem(); // Calculate CPU usage (simplified) const currentCpu = process.cpuUsage(); let cpuPercent = 0; if (performanceMonitor && performanceMonitor.lastCpuUsage) { const cpuDelta = process.cpuUsage(performanceMonitor.lastCpuUsage); const totalTime = (cpuDelta.user + cpuDelta.system) / 1000; // microseconds to milliseconds const timeDelta = now - (performanceMonitor.lastCheck || now); cpuPercent = Math.min(100, (totalTime / timeDelta) * 100); } // Calculate memory percentage const memoryPercent = (memoryUsage.rss / totalMemory) * 100; // Simple event loop lag measurement const start = process.hrtime.bigint(); await new Promise(resolve => setImmediate(resolve)); const end = process.hrtime.bigint(); const eventLoopLag = Number(end - start) / 1000000; // nanoseconds to milliseconds // Store metrics in database if (db) { try { await db.run(`INSERT INTO performance_metrics (timestamp, cpu_usage, memory_usage, memory_rss, event_loop_lag) VALUES (?, ?, ?, ?, ?)`, [ now, cpuPercent, memoryPercent, memoryUsage.rss, eventLoopLag ]); console.log(`Performance metrics stored: CPU ${cpuPercent.toFixed(1)}%, Memory ${memoryPercent.toFixed(1)}%, Event Loop ${eventLoopLag.toFixed(1)}ms`); } catch (err) { console.error('Failed to store performance metrics:', err); } } // Update for next calculation if (!performanceMonitor) { performanceMonitor = {}; } performanceMonitor.lastCpuUsage = currentCpu; performanceMonitor.lastCheck = now; } catch (error) { console.error('Performance monitoring error:', error); } } // Start global background services function startGlobalServices() { // Code analysis is now on-demand only - no automatic scanning console.log('Code Analyzer: Services ready - scanning will be performed on-demand via dashboard'); // Start performance monitoring only if enabled if (settings.performanceMonitoring && settings.performanceInterval > 0) { console.log(`Starting Code Analyzer performance monitoring every ${settings.performanceInterval} seconds`); performanceTimer = setInterval(performPerformanceCheck, (settings.performanceInterval || 10) * 1000); } } // Stop global services function stopGlobalServices() { if (scanTimer) { clearInterval(scanTimer); scanTimer = null; } if (performanceTimer) { clearInterval(performanceTimer); performanceTimer = null; } } // Setup Express middleware for /analyzer route const app = RED.httpAdmin || RED.httpNode; // CORS configuration app.use('/analyzer/api', cors()); // API routes with database injection - BEFORE static routes app.use('/analyzer/api', async (req, res, next) => { // Ensure database is initialized if (!db) { const userDir = RED.settings.userDir || process.cwd(); const dbPath = path.join(userDir, 'analyzer.db'); try { db = await initDatabase(dbPath); console.log('Code Analyzer database initialized at:', dbPath); } catch (error) { console.error('Failed to initialize Code Analyzer database:', error); return res.status(500).json({ error: 'Database initialization failed' }); } } req.db = db; req.settings = settings; req.RED = RED; next(); }, apiRouter); // Vue admin UI routes - AFTER API routes const adminPath = path.join(__dirname, 'admin', 'dist'); // Serve built assets from dist folder (JS, CSS, etc.) app.use('/analyzer/assets', express.static(path.join(adminPath, 'assets'))); // Serve the main dashboard route app.get('/analyzer', (req, res) => { const indexPath = path.join(adminPath, 'index.html'); if (require('fs').existsSync(indexPath)) { res.sendFile(indexPath); } else { res.status(404).send(` <h1>Code Analyzer Dashboard</h1> <p>Vue.js admin interface is not built yet. Run <code>npm run build</code> to build the dashboard.</p> <p>For now, you can access the API directly at <a href="/analyzer/api">/analyzer/api</a></p> `); } }); // Catch-all route for Vue SPA routes (but NOT /api routes) app.get('/analyzer*', (req, res, next) => { // Skip API routes - let them be handled by the API router if (req.path.startsWith('/analyzer/api')) { return next(); } // Serve Vue SPA for all other /analyzer routes const indexPath = path.join(adminPath, 'index.html'); if (require('fs').existsSync(indexPath)) { res.sendFile(indexPath); } else { res.status(404).send('Vue.js admin interface not found'); } }); // Settings reload API endpoint app.post('/analyzer/api/settings/reload', async (req, res) => { try { await loadSettings(); // Always restart services with new settings stopGlobalServices(); startGlobalServices(); res.json({ success: true, message: 'Settings reloaded and services restarted', settings: Object.keys(settings), performanceMonitoring: settings.performanceMonitoring, performanceInterval: settings.performanceInterval }); } catch (error) { res.status(500).json({ error: 'Failed to reload settings', details: error.message }); } }); // Manual performance check trigger for testing app.post('/analyzer/api/performance/test', async (req, res) => { try { console.log('Manual performance check triggered'); await performPerformanceCheck(); res.json({ success: true, message: 'Performance check executed', monitoringEnabled: settings.performanceMonitoring, interval: settings.performanceInterval }); } catch (error) { console.error('Manual performance check failed:', error); res.status(500).json({ error: 'Failed to execute performance check', details: error.message }); } }); // Start performance monitoring endpoint app.post('/analyzer/api/performance/start', async (req, res) => { try { if (performanceTimer) { return res.json({ success: false, message: 'Performance monitoring is already running', status: 'running' }); } // Update settings settings.performanceMonitoring = true; // Persist setting to database if (db) { await db.run('INSERT OR REPLACE INTO settings (key, value, type) VALUES (?, ?, ?)', ['performanceMonitoring', 'true', 'boolean']); } // Start monitoring console.log(`Starting performance monitoring every ${settings.performanceInterval} seconds`); performanceTimer = setInterval(performPerformanceCheck, (settings.performanceInterval || 10) * 1000); res.json({ success: true, message: 'Performance monitoring started', status: 'running', interval: settings.performanceInterval, timestamp: new Date().toISOString() }); } catch (error) { console.error('Failed to start performance monitoring:', error); res.status(500).json({ error: 'Failed to start performance monitoring', details: error.message }); } }); // Stop performance monitoring endpoint app.post('/analyzer/api/performance/stop', async (req, res) => { try { if (!performanceTimer) { return res.json({ success: false, message: 'Performance monitoring is not running', status: 'stopped' }); } // Stop monitoring clearInterval(performanceTimer); performanceTimer = null; // Update settings settings.performanceMonitoring = false; // Persist setting to database if (db) { await db.run('INSERT OR REPLACE INTO settings (key, value, type) VALUES (?, ?, ?)', ['performanceMonitoring', 'false', 'boolean']); } console.log('Performance monitoring stopped'); res.json({ success: true, message: 'Performance monitoring stopped', status: 'stopped', timestamp: new Date().toISOString() }); } catch (error) { console.error('Failed to stop performance monitoring:', error); res.status(500).json({ error: 'Failed to stop performance monitoring', details: error.message }); } }); // Performance monitoring status endpoint app.get('/analyzer/api/performance/status', (req, res) => { const isRunning = !!performanceTimer; res.json({ status: isRunning ? 'running' : 'stopped', monitoring: isRunning, interval: settings.performanceInterval || 10, enabled: settings.performanceMonitoring, uptime: isRunning ? 'Active' : 'Inactive', timestamp: new Date().toISOString() }); }); // Manual scan trigger endpoint app.post('/analyzer/api/scan/trigger', async (req, res) => { try { if (isScanning) { return res.json({ success: false, message: 'Scan already in progress' }); } // Trigger manual scan performGlobalScan(); res.json({ success: true, message: 'Manual scan triggered', timestamp: new Date().toISOString() }); } catch (error) { res.status(500).json({ error: 'Failed to trigger scan', details: error.message }); } }); // Service status endpoint app.get('/analyzer/api/status', (req, res) => { res.json({ services: { database: !!db, scanning: isScanning, performanceMonitoring: !!performanceTimer, isScanning }, settings: { codeAnalysis: settings.codeAnalysis, scanInterval: settings.scanInterval, performanceMonitoring: settings.performanceMonitoring, performanceInterval: settings.performanceInterval }, timestamp: new Date().toISOString() }); }); // Use a more reliable approach - wait for Node-RED to be fully ready let startupCheckTimer = null; let startupAttempts = 0; const maxStartupAttempts = 30; // 30 attempts * 2 seconds = 1 minute max wait function checkNodeRedReadiness() { startupAttempts++; try { // Try to access Node-RED runtime safely if (RED.nodes && RED.nodes.eachNode) { let testCount = 0; RED.nodes.eachNode(() => testCount++); // If we can count nodes without error, Node-RED is ready console.log(`Code Analyzer: Node-RED is ready! Found ${testCount} nodes.`); clearInterval(startupCheckTimer); // Always start services now (but they won't auto-scan) setTimeout(startGlobalServices, 1000); return; } } catch (error) { // Node-RED not ready yet, will try again } if (startupAttempts >= maxStartupAttempts) { console.log('Code Analyzer: Timeout waiting for Node-RED to become ready. Services will be started manually.'); clearInterval(startupCheckTimer); } } // Start checking for Node-RED readiness after a short delay setTimeout(() => { console.log('Code Analyzer: Starting Node-RED readiness checks...'); startupCheckTimer = setInterval(checkNodeRedReadiness, 2000); }, 3000); // Still listen for shutdown events RED.events.on('runtime-event', function(event) { if (event.id === 'runtime-state' && event.payload && event.payload.state === 'stop') { console.log('Code Analyzer shutting down...'); stopGlobalServices(); if (startupCheckTimer) { clearInterval(startupCheckTimer); } } }); // No longer registering a draggable node - analyzer runs as global service only console.log('Code Analyzer global service registered successfully'); console.log('Dashboard will be served from:', adminPath); console.log('Dashboard index.html exists:', require('fs').existsSync(path.join(adminPath, 'index.html'))); };