@neurolint/cli
Version:
Professional React/Next.js modernization platform with CLI, VS Code, and Web App integrations
459 lines (397 loc) • 12.1 kB
JavaScript
const fs = require('fs').promises;
const path = require('path');
/**
* Analytics and Metrics Engine for NeuroLint
*
* Tracks usage, performance, and provides insights across
* CLI, VS Code, and Web App platforms.
*/
class Analytics {
constructor() {
this.metrics = {
analysis: {
totalFiles: 0,
totalIssues: 0,
issuesByLayer: {},
issuesByType: {},
performance: {
averageTime: 0,
totalTime: 0,
slowestFiles: []
}
},
fixes: {
totalApplied: 0,
successRate: 0,
fixesByLayer: {},
fixesByRule: {},
rollbacks: 0
},
usage: {
commands: {},
platforms: {},
sessions: 0,
activeUsers: new Set()
},
quality: {
codeQualityScore: 0,
modernizationProgress: 0,
technicalDebt: 0
}
};
this.analyticsPath = path.join(process.cwd(), '.neurolint', 'analytics.json');
this.sessionId = this.generateSessionId();
this.startTime = Date.now();
}
/**
* Generate unique session ID
*/
generateSessionId() {
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Track analysis event
*/
trackAnalysis(data) {
const {
files,
issues,
executionTime,
layers,
platform = 'cli'
} = data;
// Update file count
this.metrics.analysis.totalFiles += files.length;
// Update issue count
this.metrics.analysis.totalIssues += issues.length;
// Track issues by layer
issues.forEach(issue => {
const layer = issue.layer || 'unknown';
this.metrics.analysis.issuesByLayer[layer] =
(this.metrics.analysis.issuesByLayer[layer] || 0) + 1;
const type = issue.type || 'unknown';
this.metrics.analysis.issuesByType[type] =
(this.metrics.analysis.issuesByType[type] || 0) + 1;
});
// Track performance
this.metrics.analysis.performance.totalTime += executionTime;
this.metrics.analysis.performance.averageTime =
this.metrics.analysis.performance.totalTime /
(this.metrics.analysis.totalFiles || 1);
// Track slowest files
if (executionTime > 1000) { // Files taking more than 1 second
this.metrics.analysis.performance.slowestFiles.push({
file: files[0] || 'unknown',
time: executionTime,
timestamp: new Date().toISOString()
});
// Keep only top 10 slowest files
this.metrics.analysis.performance.slowestFiles.sort((a, b) => b.time - a.time);
this.metrics.analysis.performance.slowestFiles =
this.metrics.analysis.performance.slowestFiles.slice(0, 10);
}
// Track platform usage
this.metrics.usage.platforms[platform] =
(this.metrics.usage.platforms[platform] || 0) + 1;
}
/**
* Track fix application event
*/
trackFix(data) {
const {
appliedFixes,
success,
rollback,
layer,
rule,
platform = 'cli'
} = data;
if (success) {
this.metrics.fixes.totalApplied += appliedFixes.length;
// Track fixes by layer
if (layer) {
this.metrics.fixes.fixesByLayer[layer] =
(this.metrics.fixes.fixesByLayer[layer] || 0) + 1;
}
// Track fixes by rule
if (rule) {
this.metrics.fixes.fixesByRule[rule] =
(this.metrics.fixes.fixesByRule[rule] || 0) + 1;
}
} else {
this.metrics.fixes.rollbacks += 1;
}
// Update success rate
const totalAttempts = this.metrics.fixes.totalApplied + this.metrics.fixes.rollbacks;
this.metrics.fixes.successRate = totalAttempts > 0 ?
(this.metrics.fixes.totalApplied / totalAttempts) * 100 : 0;
}
/**
* Track command usage
*/
trackCommand(command, options = {}) {
const {
platform = 'cli',
executionTime,
success = true
} = options;
this.metrics.usage.commands[command] =
(this.metrics.usage.commands[command] || 0) + 1;
// Track platform usage
this.metrics.usage.platforms[platform] =
(this.metrics.usage.platforms[platform] || 0) + 1;
// Track session
this.metrics.usage.sessions += 1;
}
/**
* Track user activity
*/
trackUser(userId, action) {
this.metrics.usage.activeUsers.add(userId);
// Track user-specific metrics
if (!this.metrics.users) {
this.metrics.users = {};
}
if (!this.metrics.users[userId]) {
this.metrics.users[userId] = {
actions: [],
lastActive: new Date().toISOString(),
totalActions: 0
};
}
this.metrics.users[userId].actions.push({
action,
timestamp: new Date().toISOString()
});
this.metrics.users[userId].lastActive = new Date().toISOString();
this.metrics.users[userId].totalActions += 1;
}
/**
* Calculate code quality score
*/
calculateQualityScore(issues, totalFiles) {
if (totalFiles === 0) return 100;
const issueDensity = issues.length / totalFiles;
const baseScore = 100;
const penalty = Math.min(issueDensity * 10, 50); // Max 50 point penalty
this.metrics.quality.codeQualityScore = Math.max(0, baseScore - penalty);
return this.metrics.quality.codeQualityScore;
}
/**
* Calculate modernization progress
*/
calculateModernizationProgress(issuesByLayer) {
const totalIssues = Object.values(issuesByLayer).reduce((sum, count) => sum + count, 0);
const modernizedIssues = (issuesByLayer[1] || 0) + (issuesByLayer[2] || 0); // Layers 1-2 are basic modernization
this.metrics.quality.modernizationProgress = totalIssues > 0 ?
(modernizedIssues / totalIssues) * 100 : 0;
return this.metrics.quality.modernizationProgress;
}
/**
* Calculate technical debt
*/
calculateTechnicalDebt(issuesByType) {
const criticalIssues = (issuesByType.error || 0) * 3; // Errors count 3x
const warningIssues = (issuesByType.warning || 0) * 2; // Warnings count 2x
const infoIssues = (issuesByType.info || 0) * 1; // Info counts 1x
this.metrics.quality.technicalDebt = criticalIssues + warningIssues + infoIssues;
return this.metrics.quality.technicalDebt;
}
/**
* Generate analytics report
*/
generateReport(options = {}) {
const {
includeUsers = false,
includePerformance = true,
includeQuality = true
} = options;
const report = {
summary: {
totalFiles: this.metrics.analysis.totalFiles,
totalIssues: this.metrics.analysis.totalIssues,
totalFixes: this.metrics.fixes.totalApplied,
successRate: this.metrics.fixes.successRate,
sessions: this.metrics.usage.sessions,
activeUsers: this.metrics.usage.activeUsers.size
},
trends: {
issuesByLayer: this.metrics.analysis.issuesByLayer,
issuesByType: this.metrics.analysis.issuesByType,
fixesByLayer: this.metrics.fixes.fixesByLayer,
fixesByRule: this.metrics.fixes.fixesByRule,
platformUsage: this.metrics.usage.platforms,
commandUsage: this.metrics.usage.commands
}
};
if (includePerformance) {
report.performance = {
averageAnalysisTime: this.metrics.analysis.performance.averageTime,
slowestFiles: this.metrics.analysis.performance.slowestFiles,
totalExecutionTime: this.metrics.analysis.performance.totalTime
};
}
if (includeQuality) {
report.quality = {
codeQualityScore: this.metrics.quality.codeQualityScore,
modernizationProgress: this.metrics.quality.modernizationProgress,
technicalDebt: this.metrics.quality.technicalDebt
};
}
if (includeUsers) {
report.users = this.metrics.users;
}
return report;
}
/**
* Save analytics data
*/
async saveAnalytics() {
try {
const analyticsDir = path.dirname(this.analyticsPath);
await fs.mkdir(analyticsDir, { recursive: true });
const data = {
version: '1.2.1',
timestamp: new Date().toISOString(),
sessionId: this.sessionId,
sessionDuration: Date.now() - this.startTime,
metrics: this.metrics
};
await fs.writeFile(this.analyticsPath, JSON.stringify(data, null, 2));
return true;
} catch (error) {
console.warn('Failed to save analytics:', error.message);
return false;
}
}
/**
* Load analytics data
*/
async loadAnalytics() {
try {
const data = await fs.readFile(this.analyticsPath, 'utf8');
const analytics = JSON.parse(data);
// Merge with current metrics
this.metrics = {
analysis: { ...this.metrics.analysis, ...analytics.metrics.analysis },
fixes: { ...this.metrics.fixes, ...analytics.metrics.fixes },
usage: { ...this.metrics.usage, ...analytics.metrics.usage },
quality: { ...this.metrics.quality, ...analytics.metrics.quality }
};
return analytics;
} catch (error) {
// No existing analytics file, start fresh
return null;
}
}
/**
* Export analytics for external analysis
*/
exportAnalytics(format = 'json') {
const data = this.generateReport({ includeUsers: true, includePerformance: true, includeQuality: true });
switch (format) {
case 'json':
return JSON.stringify(data, null, 2);
case 'csv':
return this.convertToCSV(data);
case 'html':
return this.convertToHTML(data);
default:
return data;
}
}
/**
* Convert analytics to CSV format
*/
convertToCSV(data) {
const lines = [];
// Summary
lines.push('Metric,Value');
lines.push(`Total Files,${data.summary.totalFiles}`);
lines.push(`Total Issues,${data.summary.totalIssues}`);
lines.push(`Total Fixes,${data.summary.totalFixes}`);
lines.push(`Success Rate,${data.summary.successRate}%`);
// Issues by layer
lines.push('');
lines.push('Layer,Issues');
Object.entries(data.trends.issuesByLayer).forEach(([layer, count]) => {
lines.push(`${layer},${count}`);
});
return lines.join('\n');
}
/**
* Convert analytics to HTML format
*/
convertToHTML(data) {
return `
<!DOCTYPE html>
<html>
<head>
<title>NeuroLint Analytics Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.metric { margin: 10px 0; padding: 10px; background: #f5f5f5; }
.chart { margin: 20px 0; }
</style>
</head>
<body>
<h1>NeuroLint Analytics Report</h1>
<div class="metric">
<h2>Summary</h2>
<p>Total Files: ${data.summary.totalFiles}</p>
<p>Total Issues: ${data.summary.totalIssues}</p>
<p>Total Fixes: ${data.summary.totalFixes}</p>
<p>Success Rate: ${data.summary.successRate}%</p>
</div>
<div class="chart">
<h2>Issues by Layer</h2>
<ul>
${Object.entries(data.trends.issuesByLayer)
.map(([layer, count]) => `<li>Layer ${layer}: ${count} issues</li>`)
.join('')}
</ul>
</div>
</body>
</html>
`;
}
/**
* Reset analytics data
*/
resetAnalytics() {
this.metrics = {
analysis: {
totalFiles: 0,
totalIssues: 0,
issuesByLayer: {},
issuesByType: {},
performance: {
averageTime: 0,
totalTime: 0,
slowestFiles: []
}
},
fixes: {
totalApplied: 0,
successRate: 0,
fixesByLayer: {},
fixesByRule: {},
rollbacks: 0
},
usage: {
commands: {},
platforms: {},
sessions: 0,
activeUsers: new Set()
},
quality: {
codeQualityScore: 0,
modernizationProgress: 0,
technicalDebt: 0
}
};
}
}
// Create and export singleton instance
const analytics = new Analytics();
module.exports = analytics;