Add Claude N8N toolkit with Docker mock API server
- Added comprehensive N8N development tools collection - Added Docker-containerized mock API server for testing - Added complete documentation and setup guides - Added mock API server with health checks and data endpoints - Tools include workflow analyzers, debuggers, and controllers 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
438
claude_n8n/tools/workflow_analyzer.py
Normal file
438
claude_n8n/tools/workflow_analyzer.py
Normal file
@@ -0,0 +1,438 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Workflow Analyzer - Tools for analyzing N8N workflows and execution results
|
||||
Provides debugging, error analysis, and performance insights
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Dict, List, Optional, Any, Tuple
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnalysisResult:
|
||||
"""Result of workflow analysis"""
|
||||
workflow_id: str
|
||||
issues: List[Dict]
|
||||
suggestions: List[str]
|
||||
performance_metrics: Dict
|
||||
error_patterns: List[Dict]
|
||||
health_score: float
|
||||
|
||||
|
||||
class WorkflowAnalyzer:
|
||||
"""Analyzes N8N workflows for issues, performance, and optimization opportunities"""
|
||||
|
||||
def __init__(self):
|
||||
self.common_issues = {
|
||||
'missing_error_handling': 'Node lacks error handling configuration',
|
||||
'hardcoded_credentials': 'Credentials are hardcoded instead of using credential store',
|
||||
'inefficient_loops': 'Loop structure may cause performance issues',
|
||||
'missing_validation': 'Input validation is missing or insufficient',
|
||||
'timeout_issues': 'Request timeout settings may be too aggressive',
|
||||
'rate_limit_violations': 'API rate limits not properly handled'
|
||||
}
|
||||
|
||||
def analyze_workflow_structure(self, workflow: Dict) -> Dict:
|
||||
"""Analyze workflow structure for common issues"""
|
||||
issues = []
|
||||
suggestions = []
|
||||
|
||||
nodes = workflow.get('nodes', [])
|
||||
connections = workflow.get('connections', {})
|
||||
|
||||
# Check for common structural issues
|
||||
issues.extend(self._check_error_handling(nodes))
|
||||
issues.extend(self._check_credential_usage(nodes))
|
||||
issues.extend(self._check_node_configurations(nodes))
|
||||
issues.extend(self._check_workflow_complexity(nodes, connections))
|
||||
|
||||
# Generate suggestions based on issues
|
||||
suggestions.extend(self._generate_suggestions(issues))
|
||||
|
||||
return {
|
||||
'issues': issues,
|
||||
'suggestions': suggestions,
|
||||
'node_count': len(nodes),
|
||||
'connection_count': sum(len(conns.get('main', [])) for conns in connections.values()),
|
||||
'complexity_score': self._calculate_complexity_score(nodes, connections)
|
||||
}
|
||||
|
||||
def analyze_execution_logs(self, execution_data: Dict) -> Dict:
|
||||
"""Analyze execution logs for errors and performance issues"""
|
||||
execution_id = execution_data.get('id')
|
||||
status = execution_data.get('status')
|
||||
data = execution_data.get('data', {})
|
||||
|
||||
analysis = {
|
||||
'execution_id': execution_id,
|
||||
'status': status,
|
||||
'errors': [],
|
||||
'warnings': [],
|
||||
'performance_issues': [],
|
||||
'node_timings': {},
|
||||
'total_duration': 0
|
||||
}
|
||||
|
||||
if status == 'error':
|
||||
analysis['errors'] = self._extract_errors(data)
|
||||
|
||||
# Analyze node performance
|
||||
if 'resultData' in data:
|
||||
analysis['node_timings'] = self._analyze_node_timings(data['resultData'])
|
||||
analysis['performance_issues'] = self._identify_performance_issues(analysis['node_timings'])
|
||||
|
||||
# Calculate total execution time
|
||||
start_time = execution_data.get('startedAt')
|
||||
finish_time = execution_data.get('finishedAt')
|
||||
if start_time and finish_time:
|
||||
start_dt = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
|
||||
finish_dt = datetime.fromisoformat(finish_time.replace('Z', '+00:00'))
|
||||
analysis['total_duration'] = (finish_dt - start_dt).total_seconds()
|
||||
|
||||
return analysis
|
||||
|
||||
def find_error_patterns(self, executions: List[Dict]) -> List[Dict]:
|
||||
"""Identify recurring error patterns across multiple executions"""
|
||||
error_patterns = defaultdict(int)
|
||||
error_details = defaultdict(list)
|
||||
|
||||
for execution in executions:
|
||||
if execution.get('status') == 'error':
|
||||
errors = self._extract_errors(execution.get('data', {}))
|
||||
for error in errors:
|
||||
error_type = self._categorize_error(error)
|
||||
error_patterns[error_type] += 1
|
||||
error_details[error_type].append({
|
||||
'execution_id': execution.get('id'),
|
||||
'timestamp': execution.get('startedAt'),
|
||||
'error': error
|
||||
})
|
||||
|
||||
patterns = []
|
||||
for pattern, count in error_patterns.items():
|
||||
patterns.append({
|
||||
'pattern': pattern,
|
||||
'frequency': count,
|
||||
'percentage': (count / len(executions)) * 100,
|
||||
'examples': error_details[pattern][:3] # First 3 examples
|
||||
})
|
||||
|
||||
return sorted(patterns, key=lambda x: x['frequency'], reverse=True)
|
||||
|
||||
def generate_health_report(self, workflow: Dict, executions: List[Dict]) -> AnalysisResult:
|
||||
"""Generate comprehensive health report for a workflow"""
|
||||
workflow_id = workflow.get('id')
|
||||
|
||||
# Analyze workflow structure
|
||||
structure_analysis = self.analyze_workflow_structure(workflow)
|
||||
|
||||
# Analyze recent executions
|
||||
execution_analyses = [self.analyze_execution_logs(exec) for exec in executions[-10:]]
|
||||
error_patterns = self.find_error_patterns(executions)
|
||||
|
||||
# Calculate performance metrics
|
||||
performance_metrics = self._calculate_performance_metrics(execution_analyses)
|
||||
|
||||
# Calculate health score
|
||||
health_score = self._calculate_health_score(structure_analysis, execution_analyses, error_patterns)
|
||||
|
||||
# Combine all issues and suggestions
|
||||
all_issues = structure_analysis['issues']
|
||||
all_suggestions = structure_analysis['suggestions']
|
||||
|
||||
# Add execution-based suggestions
|
||||
if error_patterns:
|
||||
all_suggestions.extend(self._suggest_error_fixes(error_patterns))
|
||||
|
||||
return AnalysisResult(
|
||||
workflow_id=workflow_id,
|
||||
issues=all_issues,
|
||||
suggestions=all_suggestions,
|
||||
performance_metrics=performance_metrics,
|
||||
error_patterns=error_patterns,
|
||||
health_score=health_score
|
||||
)
|
||||
|
||||
def _check_error_handling(self, nodes: List[Dict]) -> List[Dict]:
|
||||
"""Check for missing error handling in nodes"""
|
||||
issues = []
|
||||
|
||||
for node in nodes:
|
||||
node_type = node.get('type', '')
|
||||
if node_type in ['n8n-nodes-base.httpRequest', 'n8n-nodes-base.webhook']:
|
||||
# Check if error handling is configured
|
||||
parameters = node.get('parameters', {})
|
||||
if not parameters.get('continueOnFail') and not parameters.get('errorHandling'):
|
||||
issues.append({
|
||||
'type': 'missing_error_handling',
|
||||
'node': node.get('name'),
|
||||
'severity': 'medium',
|
||||
'description': f"Node '{node.get('name')}' lacks error handling configuration"
|
||||
})
|
||||
|
||||
return issues
|
||||
|
||||
def _check_credential_usage(self, nodes: List[Dict]) -> List[Dict]:
|
||||
"""Check for hardcoded credentials"""
|
||||
issues = []
|
||||
|
||||
for node in nodes:
|
||||
parameters = node.get('parameters', {})
|
||||
param_str = json.dumps(parameters)
|
||||
|
||||
# Look for potential hardcoded credentials
|
||||
suspicious_patterns = [
|
||||
r'password.*["\'].*["\']',
|
||||
r'token.*["\'].*["\']',
|
||||
r'key.*["\'].*["\']',
|
||||
r'secret.*["\'].*["\']'
|
||||
]
|
||||
|
||||
for pattern in suspicious_patterns:
|
||||
if re.search(pattern, param_str, re.IGNORECASE):
|
||||
issues.append({
|
||||
'type': 'hardcoded_credentials',
|
||||
'node': node.get('name'),
|
||||
'severity': 'high',
|
||||
'description': f"Node '{node.get('name')}' may contain hardcoded credentials"
|
||||
})
|
||||
break
|
||||
|
||||
return issues
|
||||
|
||||
def _check_node_configurations(self, nodes: List[Dict]) -> List[Dict]:
|
||||
"""Check node configurations for common issues"""
|
||||
issues = []
|
||||
|
||||
for node in nodes:
|
||||
node_type = node.get('type', '')
|
||||
parameters = node.get('parameters', {})
|
||||
|
||||
# Check HTTP request timeouts
|
||||
if node_type == 'n8n-nodes-base.httpRequest':
|
||||
timeout = parameters.get('timeout', 300) # Default 5 minutes
|
||||
if timeout < 30:
|
||||
issues.append({
|
||||
'type': 'timeout_issues',
|
||||
'node': node.get('name'),
|
||||
'severity': 'low',
|
||||
'description': f"HTTP timeout ({timeout}s) may be too aggressive"
|
||||
})
|
||||
|
||||
# Check for missing required parameters
|
||||
if not parameters:
|
||||
issues.append({
|
||||
'type': 'missing_validation',
|
||||
'node': node.get('name'),
|
||||
'severity': 'medium',
|
||||
'description': f"Node '{node.get('name')}' has no parameters configured"
|
||||
})
|
||||
|
||||
return issues
|
||||
|
||||
def _check_workflow_complexity(self, nodes: List[Dict], connections: Dict) -> List[Dict]:
|
||||
"""Check workflow complexity and structure"""
|
||||
issues = []
|
||||
|
||||
# Check for overly complex workflows (>20 nodes)
|
||||
if len(nodes) > 20:
|
||||
issues.append({
|
||||
'type': 'workflow_complexity',
|
||||
'severity': 'medium',
|
||||
'description': f"Workflow has {len(nodes)} nodes, consider breaking into smaller workflows"
|
||||
})
|
||||
|
||||
# Check for disconnected nodes
|
||||
connected_nodes = set()
|
||||
for source, targets in connections.items():
|
||||
connected_nodes.add(source)
|
||||
for target_list in targets.get('main', []):
|
||||
for target in target_list:
|
||||
connected_nodes.add(target.get('node'))
|
||||
|
||||
all_nodes = {node.get('name') for node in nodes}
|
||||
disconnected = all_nodes - connected_nodes
|
||||
|
||||
if disconnected:
|
||||
issues.append({
|
||||
'type': 'disconnected_nodes',
|
||||
'severity': 'high',
|
||||
'description': f"Disconnected nodes found: {', '.join(disconnected)}"
|
||||
})
|
||||
|
||||
return issues
|
||||
|
||||
def _extract_errors(self, execution_data: Dict) -> List[Dict]:
|
||||
"""Extract error information from execution data"""
|
||||
errors = []
|
||||
|
||||
if 'resultData' in execution_data:
|
||||
result_data = execution_data['resultData']
|
||||
if 'error' in result_data:
|
||||
error_info = result_data['error']
|
||||
errors.append({
|
||||
'message': error_info.get('message', ''),
|
||||
'stack': error_info.get('stack', ''),
|
||||
'type': error_info.get('name', 'Unknown'),
|
||||
'node': error_info.get('node', 'Unknown')
|
||||
})
|
||||
|
||||
return errors
|
||||
|
||||
def _categorize_error(self, error: Dict) -> str:
|
||||
"""Categorize error by type"""
|
||||
message = error.get('message', '').lower()
|
||||
|
||||
if 'timeout' in message:
|
||||
return 'timeout_error'
|
||||
elif 'connection' in message or 'network' in message:
|
||||
return 'connection_error'
|
||||
elif 'authentication' in message or 'unauthorized' in message:
|
||||
return 'auth_error'
|
||||
elif 'rate limit' in message or '429' in message:
|
||||
return 'rate_limit_error'
|
||||
elif 'validation' in message or 'invalid' in message:
|
||||
return 'validation_error'
|
||||
else:
|
||||
return 'generic_error'
|
||||
|
||||
def _analyze_node_timings(self, result_data: Dict) -> Dict:
|
||||
"""Analyze timing data for each node"""
|
||||
timings = {}
|
||||
|
||||
# Extract timing information from result data
|
||||
# This would need to be adapted based on actual N8N execution data structure
|
||||
run_data = result_data.get('runData', {})
|
||||
|
||||
for node_name, node_data in run_data.items():
|
||||
if isinstance(node_data, list) and node_data:
|
||||
node_execution = node_data[0]
|
||||
start_time = node_execution.get('startTime')
|
||||
execution_time = node_execution.get('executionTime')
|
||||
|
||||
if start_time and execution_time:
|
||||
timings[node_name] = {
|
||||
'start_time': start_time,
|
||||
'execution_time': execution_time,
|
||||
'data_count': len(node_execution.get('data', {}).get('main', []))
|
||||
}
|
||||
|
||||
return timings
|
||||
|
||||
def _identify_performance_issues(self, node_timings: Dict) -> List[Dict]:
|
||||
"""Identify performance issues from node timing data"""
|
||||
issues = []
|
||||
|
||||
for node_name, timing in node_timings.items():
|
||||
execution_time = timing.get('execution_time', 0)
|
||||
|
||||
# Flag nodes taking longer than 30 seconds
|
||||
if execution_time > 30000: # milliseconds
|
||||
issues.append({
|
||||
'type': 'slow_node',
|
||||
'node': node_name,
|
||||
'execution_time': execution_time,
|
||||
'description': f"Node '{node_name}' took {execution_time/1000:.2f}s to execute"
|
||||
})
|
||||
|
||||
return issues
|
||||
|
||||
def _calculate_performance_metrics(self, execution_analyses: List[Dict]) -> Dict:
|
||||
"""Calculate performance metrics from execution analyses"""
|
||||
if not execution_analyses:
|
||||
return {}
|
||||
|
||||
durations = [analysis['total_duration'] for analysis in execution_analyses if analysis['total_duration'] > 0]
|
||||
error_count = len([analysis for analysis in execution_analyses if analysis['status'] == 'error'])
|
||||
|
||||
return {
|
||||
'avg_duration': sum(durations) / len(durations) if durations else 0,
|
||||
'max_duration': max(durations) if durations else 0,
|
||||
'min_duration': min(durations) if durations else 0,
|
||||
'error_rate': (error_count / len(execution_analyses)) * 100,
|
||||
'total_executions': len(execution_analyses)
|
||||
}
|
||||
|
||||
def _calculate_complexity_score(self, nodes: List[Dict], connections: Dict) -> float:
|
||||
"""Calculate workflow complexity score (0-100)"""
|
||||
node_count = len(nodes)
|
||||
connection_count = sum(len(conns.get('main', [])) for conns in connections.values())
|
||||
|
||||
# Simple complexity calculation
|
||||
complexity = (node_count * 2) + connection_count
|
||||
|
||||
# Normalize to 0-100 scale
|
||||
return min(complexity / 2, 100)
|
||||
|
||||
def _calculate_health_score(self, structure_analysis: Dict, execution_analyses: List[Dict], error_patterns: List[Dict]) -> float:
|
||||
"""Calculate overall workflow health score (0-100)"""
|
||||
score = 100.0
|
||||
|
||||
# Deduct points for structural issues
|
||||
high_severity_issues = len([issue for issue in structure_analysis['issues'] if issue.get('severity') == 'high'])
|
||||
medium_severity_issues = len([issue for issue in structure_analysis['issues'] if issue.get('severity') == 'medium'])
|
||||
|
||||
score -= (high_severity_issues * 20)
|
||||
score -= (medium_severity_issues * 10)
|
||||
|
||||
# Deduct points for execution errors
|
||||
if execution_analyses:
|
||||
error_rate = len([analysis for analysis in execution_analyses if analysis['status'] == 'error']) / len(execution_analyses)
|
||||
score -= (error_rate * 50)
|
||||
|
||||
# Deduct points for recurring error patterns
|
||||
for pattern in error_patterns:
|
||||
if pattern['frequency'] > 1:
|
||||
score -= min(pattern['frequency'] * 5, 30)
|
||||
|
||||
return max(score, 0)
|
||||
|
||||
def _generate_suggestions(self, issues: List[Dict]) -> List[str]:
|
||||
"""Generate improvement suggestions based on issues"""
|
||||
suggestions = []
|
||||
|
||||
for issue in issues:
|
||||
issue_type = issue.get('type')
|
||||
|
||||
if issue_type == 'missing_error_handling':
|
||||
suggestions.append("Add error handling to HTTP and webhook nodes using 'Continue on Fail' option")
|
||||
elif issue_type == 'hardcoded_credentials':
|
||||
suggestions.append("Move credentials to N8N credential store for better security")
|
||||
elif issue_type == 'timeout_issues':
|
||||
suggestions.append("Review and adjust timeout settings based on expected response times")
|
||||
elif issue_type == 'workflow_complexity':
|
||||
suggestions.append("Consider breaking complex workflow into smaller, manageable sub-workflows")
|
||||
elif issue_type == 'disconnected_nodes':
|
||||
suggestions.append("Remove unused nodes or connect them to the workflow")
|
||||
|
||||
return list(set(suggestions)) # Remove duplicates
|
||||
|
||||
def _suggest_error_fixes(self, error_patterns: List[Dict]) -> List[str]:
|
||||
"""Suggest fixes for common error patterns"""
|
||||
suggestions = []
|
||||
|
||||
for pattern in error_patterns:
|
||||
pattern_type = pattern['pattern']
|
||||
|
||||
if pattern_type == 'timeout_error':
|
||||
suggestions.append("Increase timeout settings or implement retry logic for timeout-prone operations")
|
||||
elif pattern_type == 'connection_error':
|
||||
suggestions.append("Add connection retry logic and check network connectivity")
|
||||
elif pattern_type == 'auth_error':
|
||||
suggestions.append("Verify and refresh authentication credentials")
|
||||
elif pattern_type == 'rate_limit_error':
|
||||
suggestions.append("Implement rate limiting and backoff strategies")
|
||||
elif pattern_type == 'validation_error':
|
||||
suggestions.append("Add input validation and data sanitization steps")
|
||||
|
||||
return suggestions
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Quick test of the analyzer
|
||||
analyzer = WorkflowAnalyzer()
|
||||
print("Workflow Analyzer initialized successfully.")
|
||||
Reference in New Issue
Block a user