node-red-contrib-code-analyzer
Version:
A Node-RED package that provides a background service to detect debugging artifacts in function nodes across Node-RED flows. Features performance monitoring (CPU, memory, event loop), queue monitoring, and Slack alerting.
1,115 lines (962 loc) • 53.4 kB
JavaScript
const { detectDebuggingTraits } = require('../lib/detector');
const { findFlowVariables, adjustLineNumbers } = require('../lib/ast-detector');
const SlackNotifier = require('../lib/slack-notifier');
const PerformanceMonitor = require('../lib/performance-monitor');
const QualityMetrics = require('../lib/quality-metrics');
const PerformanceDatabase = require('../lib/performance-db');
module.exports = function(RED) {
// Global storage for flow variable maps
if (!RED.flowVariableMaps) {
RED.flowVariableMaps = {};
}
function CodeAnalyzer(config) {
RED.nodes.createNode(this, config);
const node = this;
node.codeAnalysis = config.codeAnalysis !== undefined ? config.codeAnalysis : true;
node.scanInterval = (config.scanInterval || 30) * 1000; // Convert seconds to milliseconds
node.detectionLevel = config.detectionLevel || 1;
node.queueScanning = config.queueScanning || false;
node.queueScanInterval = 3000; // Fixed at 3 seconds
node.queueMessageFrequency = (config.queueMessageFrequency || 1800) * 1000; // Convert seconds to milliseconds
node.queueScanMode = config.queueScanMode || 'all';
node.selectedQueueIds = config.selectedQueueIds || [];
node.queueLengthThreshold = config.queueLengthThreshold || 0;
node.slackWebhookUrl = config.slackWebhookUrl || '';
// Performance monitoring configuration
node.performanceMonitoring = config.performanceMonitoring || false;
node.performanceInterval = Math.max((config.performanceInterval || 10) * 1000, 1000); // Convert seconds to milliseconds, min 1 second
node.performanceThresholds = {
cpuThreshold: config.cpuThreshold || 75,
memoryThreshold: config.memoryThreshold || 80,
eventLoopThreshold: config.eventLoopThreshold || 20,
sustainedAlertDuration: Math.max((config.sustainedAlertDuration || 300) * 1000, 60000), // Convert seconds to milliseconds, min 1 minute
alertCooldown: Math.max((config.alertCooldown || 1800) * 1000, 300000), // Convert seconds to milliseconds, min 5 minutes
dbRetentionDays: Math.max(Math.min(config.dbRetentionDays || 7, 30), 1) // Between 1-30 days
};
// Track last message times for each queue
node.lastMessageTimes = {};
// Track pending alerts for grouped queue messages
node.pendingQueueAlerts = {};
node.lastQueueMessageTimes = {}; // Per-queue timing
// Track code analysis issues for grouped messages
node.lastCodeAnalysisMessageTime = 0;
// Track performance monitoring
node.lastPerformanceAlertTime = 0;
// Initialize Slack notifier
const slackNotifier = new SlackNotifier(node.slackWebhookUrl, RED);
// Initialize performance monitor
const performanceMonitor = new PerformanceMonitor();
performanceMonitor.updateConfig(node.performanceThresholds);
// Initialize quality metrics calculator
const qualityMetrics = new QualityMetrics();
// Initialize database for quality metrics storage (without auto-creation)
if (!RED.qualityDatabase) {
RED.qualityDatabase = new PerformanceDatabase();
// Only initialize if database already exists
if (RED.qualityDatabase.databaseExists()) {
RED.qualityDatabase.initDatabase().catch(err => {
node.warn(`Failed to initialize existing database: ${err.message}`);
RED.qualityDatabase.initialized = false;
});
}
}
let scanTimer;
let queueMonitorTimer;
let performanceMonitorTimer;
async function scanCurrentFlow() {
if (!node.codeAnalysis) {
// Code analysis disabled - just update node status
node.status({
fill: 'grey',
shape: 'dot',
text: 'Code analysis disabled'
});
return;
}
let totalIssues = 0;
let nodesWithIssues = 0;
const currentFlowId = node.z;
// Collect flow variables across all function nodes in the current flow
const flowVariableMap = {};
RED.nodes.eachNode(function (nodeConfig) {
if (nodeConfig.type === 'function' && nodeConfig.z === currentFlowId) {
const functionNode = RED.nodes.getNode(nodeConfig.id);
if (functionNode && functionNode.status) {
functionNode.status({});
}
}
});
// First pass: collect all flow variables
// Get flow name for better debugging
let flowName = `Flow ${currentFlowId.substring(0, 8)}`;
RED.nodes.eachNode(function(n) {
if (n.type === 'tab' && n.id === currentFlowId) {
flowName = n.label || n.name || flowName;
}
});
RED.nodes.eachNode(function (nodeConfig) {
if (nodeConfig.type === 'function' && nodeConfig.func && nodeConfig.z === currentFlowId) {
try {
const { parseScript } = require('meriyah');
let ast;
// Parse the code to AST
try {
ast = parseScript(nodeConfig.func, {
loc: true,
ranges: true,
module: false,
webcompat: true
});
} catch (scriptError) {
// If script parsing fails due to top-level return, wrap in function
if (scriptError.message.includes('Illegal return statement')) {
const wrappedCode = `function nodeRedWrapper() {\n${nodeConfig.func}\n}`;
ast = parseScript(wrappedCode, {
loc: true,
ranges: true,
module: false,
webcompat: true
});
// Adjust line numbers for wrapped code
adjustLineNumbers(ast, -1);
} else {
throw scriptError;
}
}
const flowVars = findFlowVariables(ast);
flowVars.forEach(flowVar => {
if (!flowVariableMap[flowVar.variableName]) {
flowVariableMap[flowVar.variableName] = {
gets: [],
sets: []
};
}
const varInfo = {
nodeId: nodeConfig.id,
nodeName: nodeConfig.name || `Function Node ${nodeConfig.id.substring(0, 8)}`,
line: flowVar.line,
column: flowVar.column,
endColumn: flowVar.endColumn,
fullCallStart: flowVar.fullCallStart,
fullCallEnd: flowVar.fullCallEnd
};
if (flowVar.type === 'flow-get') {
flowVariableMap[flowVar.variableName].gets.push(varInfo);
} else if (flowVar.type === 'flow-set') {
flowVariableMap[flowVar.variableName].sets.push(varInfo);
}
});
} catch (error) {
// If flow variable parsing fails, continue with regular analysis
// Silently continue
}
}
});
// Store flow variable map globally for editor access
RED.flowVariableMaps[currentFlowId] = flowVariableMap;
// Second pass: analyze debugging traits and calculate quality metrics
const functionNodes = [];
RED.nodes.eachNode(function (nodeConfig) {
if (nodeConfig.type === 'function' && nodeConfig.func && nodeConfig.z === currentFlowId) {
functionNodes.push(nodeConfig);
const issues = detectDebuggingTraits(nodeConfig.func, node.detectionLevel);
if (issues.length > 0) {
totalIssues += issues.length;
nodesWithIssues++;
nodeConfig._debugIssues = issues;
const functionNode = RED.nodes.getNode(nodeConfig.id);
if (functionNode && functionNode.status) {
let statusColor = 'blue';
let text = 'Minor debug traits noticed';
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' ||
issue.type === 'unused-variable'
);
if (hasLevel1) {
statusColor = 'red';
text = 'Severe debugging traits.';
} else if (hasLevel2) {
statusColor = 'yellow';
text = 'Important debugging traits.';
}
functionNode.status({
fill: statusColor,
shape: 'dot',
text
});
}
} else {
delete nodeConfig._debugIssues;
}
}
});
// Calculate and store quality metrics
try {
const flowQualityMetrics = qualityMetrics.calculateFlowQualityMetrics(functionNodes, node.detectionLevel);
// Store flow-level metrics
if (RED.qualityDatabase && RED.qualityDatabase.initialized) {
RED.qualityDatabase.storeCodeQualityMetrics(
currentFlowId,
flowName,
flowQualityMetrics.totalIssues,
flowQualityMetrics.nodesWithIssues,
flowQualityMetrics.nodesWithCriticalIssues || 0,
flowQualityMetrics.totalFunctionNodes,
flowQualityMetrics.issueTypes,
flowQualityMetrics.qualityScore,
flowQualityMetrics.complexityScore
).catch(err => node.warn(`Failed to store flow quality metrics: ${err.message}`));
// Store all node-level metrics in a single batch operation
if (flowQualityMetrics.nodeMetrics && flowQualityMetrics.nodeMetrics.length > 0) {
RED.qualityDatabase.storeNodeQualityMetricsBatch(
currentFlowId,
flowQualityMetrics.nodeMetrics
).catch(err => node.warn(`Failed to store batch node quality metrics: ${err.message}`));
}
}
// Calculate and store system-wide trends (with coordination to avoid duplicates)
try {
await storeSystemTrends();
} catch (error) {
node.warn(`Failed to store system trends: ${error.message}`);
}
} catch (error) {
node.warn(`Failed to calculate quality metrics: ${error.message}`);
}
if (totalIssues > 0) {
node.status({
fill: 'yellow',
shape: 'dot',
text: `Found ${totalIssues} debugging traits in ${nodesWithIssues} nodes`
});
// Send code analysis message if frequency interval has passed
const now = Date.now();
if (now - node.lastCodeAnalysisMessageTime >= node.queueMessageFrequency) {
slackNotifier.sendCodeAnalysisAlert(currentFlowId, totalIssues, nodesWithIssues, (msg) => node.warn(msg));
node.lastCodeAnalysisMessageTime = now;
}
} else {
node.status({
fill: 'green',
shape: 'dot',
text: 'No debugging traits found'
});
}
}
// Store system-wide trends with coordination to prevent duplicates
async function storeSystemTrends() {
if (!RED.qualityDatabase || !RED.qualityDatabase.initialized) {
return;
}
// Use a simple coordination mechanism: only the "first" analyzer node stores trends
const allAnalyzerNodes = [];
RED.nodes.eachNode((nodeConfig) => {
if (nodeConfig.type === 'code-analyzer') {
allAnalyzerNodes.push(nodeConfig.id);
}
});
// Sort node IDs and only let the first one store system trends
allAnalyzerNodes.sort();
if (allAnalyzerNodes[0] !== node.id) {
return; // This node is not the coordinator
}
// Get all current quality data and calculate system trends
const qualitySummary = await RED.qualityDatabase.getQualitySummary();
const qualityMetrics = new (require('../lib/quality-metrics'))();
const transformedFlows = qualitySummary.flows.map(flow => ({
totalFunctionNodes: flow.total_function_nodes,
totalIssues: flow.total_issues,
nodesWithIssues: flow.nodes_with_issues,
nodesWithCriticalIssues: flow.nodes_with_critical_issues || 0,
qualityScore: flow.quality_score,
complexityScore: flow.complexity_score
}));
const systemTrends = qualityMetrics.calculateSystemQualityTrends(transformedFlows);
}
function monitorQueues() {
if (!node.queueScanning) return;
const currentFlowId = node.z;
RED.nodes.eachNode(function (nodeConfig) {
if (nodeConfig.type === 'delay' && nodeConfig.pauseType == 'rate' && nodeConfig.z === currentFlowId) {
// Check if we should monitor this specific queue
const shouldMonitor = node.queueScanMode === 'all' ||
(node.queueScanMode === 'specific' && node.selectedQueueIds.includes(nodeConfig.id));
if (shouldMonitor) {
const delayNode = RED.nodes.getNode(nodeConfig.id);
if (delayNode) {
const queueLength = delayNode?.buffer.length;
if (queueLength > node.queueLengthThreshold) {
const now = Date.now();
// Add to pending alerts for grouped messaging
if (queueLength > node.queueLengthThreshold) {
// Get flow information
let flowName = `Flow ${currentFlowId.substring(0, 8)}`;
RED.nodes.eachNode(function(n) {
if (n.type === 'tab' && n.id === currentFlowId) {
flowName = n.label || n.name || flowName;
}
});
const queueName = nodeConfig.name || `Queue ${nodeConfig.id.substring(0, 8)}`;
// Store alert info for grouping
node.pendingQueueAlerts[nodeConfig.id] = {
queueName: queueName,
flowName: flowName,
queueLength: queueLength,
timestamp: now
};
}
// Check if this specific queue can send a message
const lastQueueMessageTime = node.lastQueueMessageTimes[nodeConfig.id] || 0;
if (now - lastQueueMessageTime >= node.queueMessageFrequency) {
// Send individual queue alert for this specific queue
const singleQueueAlert = {};
singleQueueAlert[nodeConfig.id] = node.pendingQueueAlerts[nodeConfig.id];
slackNotifier.sendQueueAlert(singleQueueAlert, (msg) => node.warn(msg));
// Update timing for this specific queue
node.lastQueueMessageTimes[nodeConfig.id] = now;
// Remove this queue from pending alerts
delete node.pendingQueueAlerts[nodeConfig.id];
}
}
}
}
}
});
}
async function monitorPerformance() {
if (!node.performanceMonitoring) return;
try {
const performanceSummary = await performanceMonitor.getPerformanceSummary();
const now = Date.now();
// Check if we should send performance alerts
if (performanceSummary.alerts.length > 0) {
// Check if enough time has passed since last alert
if (now - node.lastPerformanceAlertTime >= node.queueMessageFrequency) {
slackNotifier.sendPerformanceAlert(performanceSummary, (msg) => node.warn(msg));
node.lastPerformanceAlertTime = now;
}
// Update node status to show performance issues
const alertCount = performanceSummary.alerts.length;
const highestSeverity = performanceSummary.alerts.some(a => a.severity === 'warning') ? 'warning' : 'info';
node.status({
fill: highestSeverity === 'warning' ? 'red' : 'yellow',
shape: 'ring',
text: `Performance: ${alertCount} sustained alert${alertCount > 1 ? 's' : ''} - CPU: ${performanceSummary.current.cpu.toFixed(1)}%, Mem: ${performanceSummary.current.memory.toFixed(1)}%`
});
} else {
// Check if any metrics are currently over threshold (but not sustained)
const current = performanceSummary.current;
const isOverThreshold = current.cpu > node.performanceThresholds.cpuThreshold ||
current.memory > node.performanceThresholds.memoryThreshold ||
(current.eventLoopLag && current.eventLoopLag > node.performanceThresholds.eventLoopThreshold);
if (isOverThreshold) {
// Show warning status for current threshold violations
node.status({
fill: 'yellow',
shape: 'ring',
text: `Performance: Over threshold - CPU: ${current.cpu.toFixed(1)}%, Mem: ${current.memory.toFixed(1)}%`
});
} else {
// Show OK status when all metrics are within thresholds
node.status({
fill: 'green',
shape: 'ring',
text: `Performance: OK - CPU: ${current.cpu.toFixed(1)}%, Mem: ${current.memory.toFixed(1)}%`
});
}
}
} catch (error) {
node.error('Error monitoring performance: ' + error.message);
}
}
function startScanning() {
scanCurrentFlow().catch(err => node.warn(`Initial scan failed: ${err.message}`));
if (node.codeAnalysis && node.scanInterval > 0) {
scanTimer = setInterval(() => {
scanCurrentFlow().catch(err => node.warn(`Scheduled scan failed: ${err.message}`));
}, node.scanInterval);
}
// Start queue monitoring if enabled
if (node.queueScanning) {
queueMonitorTimer = setInterval(monitorQueues, node.queueScanInterval);
}
// Start performance monitoring if enabled
if (node.performanceMonitoring) {
performanceMonitor.start();
performanceMonitorTimer = setInterval(monitorPerformance, node.performanceInterval);
}
}
node.on('input', function(msg) {
if (node.codeAnalysis) {
scanCurrentFlow().catch(err => node.warn(`Manual scan failed: ${err.message}`));
msg.payload = { action: 'scan_completed', timestamp: new Date().toISOString() };
} else {
msg.payload = { action: 'scan_skipped', reason: 'code_analysis_disabled', timestamp: new Date().toISOString() };
}
node.send(msg);
});
node.on('close', function() {
if (scanTimer) {
clearInterval(scanTimer);
scanTimer = null;
}
if (queueMonitorTimer) {
clearInterval(queueMonitorTimer);
queueMonitorTimer = null;
}
if (performanceMonitorTimer) {
clearInterval(performanceMonitorTimer);
performanceMonitorTimer = null;
}
if (performanceMonitor) {
performanceMonitor.stop();
}
});
RED.events.on('nodes-started', function() {
scanCurrentFlow().catch(err => node.warn(`Startup scan failed: ${err.message}`));
});
setTimeout(startScanning, 1000);
}
RED.nodes.registerType('code-analyzer', CodeAnalyzer);
// API endpoint to get database path for UI display
RED.httpAdmin.get('/code-analyzer/db-path', function(_, res) {
const dbPath = RED.qualityDatabase && RED.qualityDatabase.dbPath
? RED.qualityDatabase.dbPath
: require('path').join(process.cwd(), 'performance_metrics.db');
res.json({ dbPath: dbPath });
});
// API endpoint to get all flow variable mappings (must be before /:flowId route)
RED.httpAdmin.get('/code-analyzer/flow-variables/all-flows', function(_, res) {
const allFlowMaps = RED.flowVariableMaps || {};
res.json(allFlowMaps);
});
// API endpoint to get flow variable mapping for a specific flow
RED.httpAdmin.get('/code-analyzer/flow-variables/:flowId', function(req, res) {
const flowId = req.params.flowId;
const flowVariableMap = (RED.flowVariableMaps && RED.flowVariableMaps[flowId]) || {};
res.json(flowVariableMap);
});
// API endpoint to get the actual value of a flow variable
RED.httpAdmin.get('/code-analyzer/flow-variable-value/:flowId/:variableName', function(req, res) {
try {
const flowId = req.params.flowId;
const variableName = decodeURIComponent(req.params.variableName);
let value = undefined;
let found = false;
// Find any runtime node in the target flow to access its flow context
RED.nodes.eachNode(function(nodeConfig) {
if (nodeConfig.z === flowId && !found) {
const runtimeNode = RED.nodes.getNode(nodeConfig.id);
if (runtimeNode && runtimeNode.context) {
try {
const flowContext = runtimeNode.context().flow;
if (flowContext) {
// Use synchronous get - this is the correct approach
const contextValue = flowContext.get(variableName);
if (contextValue !== undefined) {
value = contextValue;
found = true;
}
}
} catch (contextError) {
// Continue to next node
}
}
}
});
// Return the result immediately
res.json({
variableName: variableName,
value: value,
found: found
});
} catch (error) {
res.status(500).json({
error: 'Error retrieving flow variable value',
details: error.message
});
}
});
// API endpoint to get the actual value of an environment variable
RED.httpAdmin.get('/code-analyzer/env-variable-value/:flowId/:variableName', function(req, res) {
try {
const flowId = req.params.flowId;
const variableName = decodeURIComponent(req.params.variableName);
let value;
let found = false;
// First, try to get the environment variable from the flow's configuration
let flowNode = null;
RED.nodes.eachNode(function(nodeConfig) {
if (nodeConfig.type === 'tab' && nodeConfig.id === flowId) {
flowNode = nodeConfig;
}
});
// Check if the flow has environment variables defined
if (flowNode && flowNode.env) {
for (const envVar of flowNode.env) {
if (envVar.name === variableName) {
value = envVar.value;
found = true;
break;
}
}
}
// If not found in flow env, try accessing through Node-RED's env context
if (!found) {
RED.nodes.eachNode(function(nodeConfig) {
if (!found && nodeConfig.z === flowId) {
const runtimeNode = RED.nodes.getNode(nodeConfig.id);
if (runtimeNode && runtimeNode.context) {
try {
const envContext = runtimeNode.context().env;
if (envContext) {
const envValue = envContext.get(variableName);
if (envValue !== undefined) {
value = envValue;
found = true;
}
}
} catch (envError) {
// Continue to next node
}
}
}
});
}
// Fallback to process.env if not found anywhere else
if (!found) {
const processValue = process.env[variableName];
if (processValue !== undefined) {
value = processValue;
found = true;
}
}
res.json({
variableName: variableName,
value: value,
found: found
});
} catch (error) {
res.status(500).json({
error: 'Error retrieving environment variable value',
details: error.message
});
}
});
// Legacy endpoint for env variables (without flow context) - keep for backward compatibility
RED.httpAdmin.get('/code-analyzer/env-variable-value/:variableName', function(req, res) {
try {
const variableName = decodeURIComponent(req.params.variableName);
// Just check process.env for legacy calls
const value = process.env[variableName];
res.json({
variableName: variableName,
value: value,
found: value !== undefined
});
} catch (error) {
res.status(500).json({
error: 'Error retrieving environment variable value',
details: error.message
});
}
});
// ===== DASHBOARD API ENDPOINTS =====
// Serve dashboard static files
const path = require('path');
const fs = require('fs');
// Diagnostic endpoint to test if routes are working
RED.httpAdmin.get('/code-analyzer/test', function(_, res) {
res.json({
message: 'Code analyzer routes are working!',
timestamp: new Date().toISOString(),
paths: {
dashboard: path.join(__dirname, '../static/dashboard.html'),
dashboardExists: fs.existsSync(path.join(__dirname, '../static/dashboard.html')),
javascript: path.join(__dirname, '../static/dashboard.js'),
javascriptExists: fs.existsSync(path.join(__dirname, '../static/dashboard.js'))
}
});
});
RED.httpAdmin.get('/code-analyzer/dashboard', function(_, res) {
const dashboardPath = path.join(__dirname, '../static/dashboard.html');
if (fs.existsSync(dashboardPath)) {
res.sendFile(dashboardPath);
} else {
res.status(404).send('Dashboard not found. Please ensure dashboard files are installed.');
}
});
// Serve dashboard JavaScript file
RED.httpAdmin.get('/code-analyzer/dashboard.js', function(_, res) {
const jsPath = path.join(__dirname, '../static/dashboard.js');
if (fs.existsSync(jsPath)) {
res.setHeader('Content-Type', 'application/javascript');
res.sendFile(jsPath);
} else {
res.status(404).send('Dashboard JavaScript not found');
}
});
// Serve other static assets
RED.httpAdmin.get('/code-analyzer/static/:file', function(req, res) {
const filePath = path.join(__dirname, '../static', req.params.file);
if (fs.existsSync(filePath)) {
// Set appropriate content type
if (req.params.file.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript');
} else if (req.params.file.endsWith('.css')) {
res.setHeader('Content-Type', 'text/css');
}
res.sendFile(filePath);
} else {
res.status(404).send('File not found');
}
});
// API: Get dashboard summary data
RED.httpAdmin.get('/code-analyzer/api/dashboard/summary', async function(_, res) {
try {
if (!RED.qualityDatabase || !RED.qualityDatabase.initialized) {
return res.status(503).json({ error: 'Quality database not available' });
}
const [qualitySummary, performanceStats] = await Promise.all([
RED.qualityDatabase.getQualitySummary(),
RED.qualityDatabase.getStats()
]);
const qualityMetrics = new QualityMetrics();
// Transform database results to match expected format
const transformedFlows = qualitySummary.flows.map(flow => ({
totalFunctionNodes: flow.total_function_nodes,
totalIssues: flow.total_issues,
nodesWithIssues: flow.nodes_with_issues,
nodesWithCriticalIssues: flow.nodes_with_critical_issues || 0,
qualityScore: flow.quality_score,
complexityScore: flow.complexity_score
}));
// Calculate system-wide trends
const systemTrends = qualityMetrics.calculateSystemQualityTrends(transformedFlows);
// Note: System trends are stored during actual scans, not dashboard requests
const dashboardData = {
quality: {
...qualitySummary,
systemTrends,
overallGrade: qualityMetrics.getQualityGrade(systemTrends.overallQuality)
},
performance: performanceStats,
timestamp: new Date().toISOString()
};
res.json(dashboardData);
} catch (error) {
res.status(500).json({
error: 'Failed to get dashboard summary',
details: error.message
});
}
});
// API: Get quality trends over time
RED.httpAdmin.get('/code-analyzer/api/dashboard/quality-trends', async function(req, res) {
try {
if (!RED.qualityDatabase || !RED.qualityDatabase.initialized) {
return res.status(503).json({ error: 'Quality database not available' });
}
const hours = parseInt(req.query.hours) || 24;
const limit = parseInt(req.query.limit) || 100;
const qualityTrends = await RED.qualityDatabase.getCodeQualityTrends(hours, limit);
res.json({
trends: qualityTrends,
timeframe: `${hours} hours`,
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({
error: 'Failed to get quality trends',
details: error.message
});
}
});
// API: Get most problematic nodes
RED.httpAdmin.get('/code-analyzer/api/dashboard/problematic-nodes', async function(req, res) {
try {
if (!RED.qualityDatabase || !RED.qualityDatabase.initialized) {
return res.status(503).json({ error: 'Quality database not available' });
}
const limit = parseInt(req.query.limit) || 20;
const problematicNodes = await RED.qualityDatabase.getMostProblematicNodes(limit);
const qualityMetrics = new QualityMetrics();
const enhancedNodes = problematicNodes.map(node => ({
...node,
grade: qualityMetrics.getQualityGrade(node.quality_score),
recommendations: qualityMetrics.generateRecommendations({
totalIssues: node.issues_count,
complexityScore: node.complexity_score,
issueTypes: [] // We could parse issue_details JSON here if needed
})
}));
res.json({
nodes: enhancedNodes,
count: enhancedNodes.length,
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({
error: 'Failed to get problematic nodes',
details: error.message
});
}
});
// API: Get detailed flow analysis with node-level issues
RED.httpAdmin.get('/code-analyzer/api/dashboard/flows/:flowId/details', async function(req, res) {
try {
if (!RED.qualityDatabase || !RED.qualityDatabase.initialized) {
return res.status(503).json({ error: 'Quality database not available' });
}
const flowId = req.params.flowId;
const qualityMetrics = new QualityMetrics();
// Get current function nodes in this flow
const functionNodes = [];
let flowName = `Flow ${flowId.substring(0, 8)}`;
// Find flow name
RED.nodes.eachNode(function(n) {
if (n.type === 'tab' && n.id === flowId) {
flowName = n.label || n.name || flowName;
}
});
// Get all function nodes in this flow with current analysis
RED.nodes.eachNode(function (nodeConfig) {
if (nodeConfig.type === 'function' && nodeConfig.func && nodeConfig.z === flowId) {
const issues = detectDebuggingTraits(nodeConfig.func, 3); // Use level 3 for comprehensive analysis
const linesOfCode = nodeConfig.func.split('\n').length;
const complexityScore = qualityMetrics.calculateComplexityScore(nodeConfig.func);
const nodeQualityScore = qualityMetrics.calculateNodeQualityScore(issues, linesOfCode);
// Add severity and priority to each issue
const enhancedIssues = issues.map(issue => {
const severity = qualityMetrics.getIssueSeverity(issue.type);
return {
...issue,
severity: severity.level,
priority: severity.priority,
color: severity.color,
weight: qualityMetrics.weights[issue.type] || 1
};
});
// Sort issues by priority (critical first)
enhancedIssues.sort((a, b) => a.priority - b.priority);
functionNodes.push({
nodeId: nodeConfig.id,
nodeName: nodeConfig.name || `Function Node ${nodeConfig.id.substring(0, 8)}`,
linesOfCode,
complexityScore,
qualityScore: nodeQualityScore,
qualityGrade: qualityMetrics.getQualityGrade(nodeQualityScore),
issues: enhancedIssues,
issuesCount: issues.length,
criticalIssues: enhancedIssues.filter(i => i.severity === 'critical').length,
warningIssues: enhancedIssues.filter(i => i.severity === 'warning').length,
infoIssues: enhancedIssues.filter(i => i.severity === 'info').length,
recommendations: qualityMetrics.generateRecommendations({
totalIssues: issues.length,
complexityScore,
issueTypes: issues.map(i => i.type)
}),
// Navigation information for editor opening
navigation: {
flowId: flowId,
nodeId: nodeConfig.id,
nodeName: nodeConfig.name || `Function Node ${nodeConfig.id.substring(0, 8)}`,
editorUrl: `/red/#flow/${flowId}`,
nodeType: nodeConfig.type
}
});
}
});
// Calculate flow-level metrics
const flowMetrics = qualityMetrics.calculateFlowQualityMetrics(
functionNodes.map(n => ({
id: n.nodeId,
name: n.nodeName,
type: 'function',
func: 'placeholder' // We already calculated issues above
})),
3
);
// Sort nodes by severity (most problematic first)
functionNodes.sort((a, b) => {
if (a.criticalIssues !== b.criticalIssues) {
return b.criticalIssues - a.criticalIssues;
}
if (a.warningIssues !== b.warningIssues) {
return b.warningIssues - a.warningIssues;
}
return b.issuesCount - a.issuesCount;
});
const response = {
flowId,
flowName,
totalNodes: functionNodes.length,
nodesWithIssues: functionNodes.filter(n => n.issuesCount > 0).length,
totalIssues: functionNodes.reduce((sum, n) => sum + n.issuesCount, 0),
criticalIssues: functionNodes.reduce((sum, n) => sum + n.criticalIssues, 0),
warningIssues: functionNodes.reduce((sum, n) => sum + n.warningIssues, 0),
infoIssues: functionNodes.reduce((sum, n) => sum + n.infoIssues, 0),
overallQuality: flowMetrics.qualityScore,
overallComplexity: flowMetrics.complexityScore,
overallGrade: qualityMetrics.getQualityGrade(flowMetrics.qualityScore),
healthPercentage: Math.round((functionNodes.length - functionNodes.filter(n => n.issuesCount > 0).length) / Math.max(1, functionNodes.length) * 100),
nodes: functionNodes,
recommendations: qualityMetrics.generateRecommendations({
...flowMetrics,
totalIssues: functionNodes.reduce((sum, node) => sum + node.issuesCount, 0),
issueTypes: [...new Set(functionNodes.flatMap(node => node.issueTypes || []))]
}),
timestamp: new Date().toISOString()
};
res.json(response);
} catch (error) {
res.status(500).json({
error: 'Failed to get detailed flow analysis',
details: error.message
});
}
});
// API: Get flow quality details (simplified for trends)
RED.httpAdmin.get('/code-analyzer/api/dashboard/flows/:flowId', async function(req, res) {
try {
if (!RED.qualityDatabase || !RED.qualityDatabase.initialized) {
return res.status(503).json({ error: 'Quality database not available' });
}
const flowId = req.params.flowId;
const hours = parseInt(req.query.hours) || 24;
// Get recent quality metrics for this flow
const qualityTrends = await RED.qualityDatabase.getCodeQualityTrends(hours, 100);
const flowTrends = qualityTrends.filter(trend => trend.flow_id === flowId);
if (flowTrends.length === 0) {
return res.status(404).json({ error: 'Flow not found or no recent data' });
}
const latestMetrics = flowTrends[0];
const qualityMetrics = new QualityMetrics();
const report = qualityMetrics.generateFlowQualityReport(latestMetrics);
res.json({
flow: report,
trends: flowTrends,
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({
error: 'Failed to get flow details',
details: error.message
});
}
});
// API: Navigate to specific node and line (for editor integration)
RED.httpAdmin.post('/code-analyzer/api/navigate-to-node', function(req, res) {
try {
const { nodeId, flowId, lineNumber, columnNumber } = req.body;
// Verify node exists
let nodeExists = false;
RED.nodes.eachNode(function(n) {
if (n.id === nodeId && n.z === flowId) {
nodeExists = true;
}
});
if (!nodeExists) {
return res.status(404).json({
error: 'Node not found',
nodeId,
flowId
});
}
// Return navigation information
res.json({
success: true,
navigation: {
nodeId,
flowId,
lineNumber: lineNumber || 1,
columnNumber: columnNumber || 1,
editorUrl: `/red/#flow/${flowId}`,
timestamp: new Date().toISOString()
}
});
} catch (error) {
res.status(500).json({
error: 'Failed to prepare navigation',
details: error.message
});
}
});
// API: Get performance metrics for charts
RED.httpAdmin.get('/code-analyzer/api/dashboard/performance-metrics', async function(req, res) {
try {
if (!RED.qualityDatabase || !RED.qualityDatabase.initialized) {
return res.status(503).json({ error: 'Quality database not available' });
}
const metricType = req.query.type || 'cpu';
const count = parseInt(req.query.count) || 50;
const [recentMetrics, averages, alertHistory] = await Promise.all([
RED.qualityDatabase.getRecentMetrics(metricType, count),
RED.qualityDatabase.getAverages(60), // 1 hour average
RED.qualityDatabase.getAlertHistory(20)
]);
res.json({
metrics: recentMetrics,
averages: averages,
alerts: alertHistory.filter(alert => alert.metric_type === metricType),
metricType: metricType,
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({
error: 'Failed to get performance metrics',
details: error.message
});
}
});
// API: Get system alerts
RED.httpAdmin.get('/code-analyzer/api/dashboard/alerts', async function(req, res) {
try {
if (!RED.qualityDatabase || !RED.qualityDatabase.initialized) {
return res.status(503).json({ error: 'Quality database not available' });
}
const limit = parseInt(req.query.limit) || 50;
const alertHistory = await RED.qualityDatabase.getAlertHistory(limit);
res.json({
alerts: alertHistory,
count: alertHistory.length,
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({
error: 'Failed to get alerts',
details: error.message
});
}
});
// API: Create database with fallback locations
RED.httpAdmin.post('/code-analyzer/api/create-database', async function(req, res) {
try {
if (!RED.qualityDatabase) {
RED.qualityDatabase = new PerformanceDatabase();
}
// Check if database already exists
if (RED.qualityDatabase.databaseExists()) {
return res.json({
success: true,
message: 'Database already exists',
dbPath: RED.qualityDatabase.dbPath
});
}
// Create the database with progress tracking
const result = await RED.qualityDatabase.createDatabase();
res.json({
success: true,
message: result.message,
dbPath: result.dbPath,
attemptedLocations: result.attemptedLocations,
creationMessages: result.messages
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to create database in all locations'