aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
444 lines • 14.2 kB
JavaScript
/**
* @file metrics-collector.ts
* @description Metrics collection and aggregation system
*
* Implements F-010/UC-010: Metrics Collection
* - Code quality metrics (complexity, duplication, coverage)
* - Performance metrics (build time, test time, response time)
* - Team metrics (velocity, throughput, lead time)
* - Trend analysis and visualization
* - Metric export (CSV, JSON, Prometheus)
*
* @implements NFR-METRICS-001: Collection overhead <5%
* @implements NFR-METRICS-002: Real-time dashboard updates <1s
* @implements NFR-METRICS-003: Retention 90 days minimum
*/
import { promises as fs } from 'fs';
import path from 'path';
// ============================================================================
// Metrics Collector Class
// ============================================================================
export class MetricsCollector {
config;
metrics;
snapshots;
collectionTimer;
constructor(config) {
this.config = {
projectPath: config.projectPath,
retentionDays: config.retentionDays || 90,
collectInterval: config.collectInterval || 60000,
exportFormats: config.exportFormats || ['json'],
storePath: config.storePath || path.join(config.projectPath, '.metrics')
};
this.metrics = new Map();
this.snapshots = [];
}
// ========================================================================
// Collection Methods
// ========================================================================
/**
* Start automatic metric collection
*/
async startCollection() {
// Initial collection
await this.collect();
// Schedule periodic collection
this.collectionTimer = setInterval(async () => {
await this.collect();
}, this.config.collectInterval);
}
/**
* Stop automatic collection
*/
stopCollection() {
if (this.collectionTimer) {
clearInterval(this.collectionTimer);
this.collectionTimer = undefined;
}
}
/**
* Collect all metrics
*/
async collect() {
const snapshot = {
timestamp: new Date(),
codeQuality: await this.collectCodeQuality(),
performance: await this.collectPerformance(),
team: await this.collectTeamMetrics()
};
this.snapshots.push(snapshot);
await this.pruneOldSnapshots();
await this.persist();
return snapshot;
}
/**
* Collect code quality metrics
*/
async collectCodeQuality() {
// Placeholder: In real implementation, would integrate with:
// - SonarQube
// - ESLint
// - Coverage tools (Istanbul, NYC)
// - Complexity analyzers
return {
complexity: this.measureComplexity(),
duplication: this.measureDuplication(),
coverage: await this.measureCoverage(),
linesOfCode: await this.countLinesOfCode(),
technicalDebt: this.estimateTechnicalDebt(),
codeSmells: 0,
bugs: 0,
vulnerabilities: 0
};
}
/**
* Collect performance metrics
*/
async collectPerformance() {
return {
buildTime: await this.measureBuildTime(),
testTime: await this.measureTestTime(),
deployTime: 0,
responseTime: 0,
throughput: 0,
errorRate: 0
};
}
/**
* Collect team metrics
*/
async collectTeamMetrics() {
return {
velocity: await this.calculateVelocity(),
throughput: await this.calculateThroughput(),
leadTime: await this.calculateLeadTime(),
cycleTime: await this.calculateCycleTime(),
deployFrequency: 0,
changeFailureRate: 0,
meanTimeToRecovery: 0
};
}
// ========================================================================
// Specific Metric Calculations
// ========================================================================
/**
* Measure cyclomatic complexity
*/
measureComplexity() {
// Simplified: Would integrate with complexity analyzer
return 5.2; // Average complexity
}
/**
* Measure code duplication
*/
measureDuplication() {
// Simplified: Would integrate with duplication detector
return 3.5; // % duplicated
}
/**
* Measure test coverage
*/
async measureCoverage() {
// Simplified: Would read from coverage report
return 85.3; // % coverage
}
/**
* Count lines of code
*/
async countLinesOfCode() {
let totalLines = 0;
const countInDirectory = async (dir) => {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
// Skip node_modules, dist, etc.
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.git') {
continue;
}
if (entry.isDirectory()) {
await countInDirectory(fullPath);
}
else if (entry.isFile() && this.isSourceFile(entry.name)) {
const content = await fs.readFile(fullPath, 'utf-8');
totalLines += content.split('\n').length;
}
}
};
try {
await countInDirectory(this.config.projectPath);
}
catch {
// Ignore errors
}
return totalLines;
}
/**
* Check if file is source code
*/
isSourceFile(filename) {
const sourceExtensions = ['.ts', '.js', '.tsx', '.jsx', '.py', '.java', '.go', '.rs'];
return sourceExtensions.some(ext => filename.endsWith(ext));
}
/**
* Estimate technical debt
*/
estimateTechnicalDebt() {
// Simplified: Would integrate with debt tracking tools
return 120; // hours
}
/**
* Measure build time
*/
async measureBuildTime() {
// Simplified: Would track actual build duration
return 45; // seconds
}
/**
* Measure test time
*/
async measureTestTime() {
// Simplified: Would track actual test duration
return 30; // seconds
}
/**
* Calculate team velocity
*/
async calculateVelocity() {
// Simplified: Would integrate with issue tracker
return 25; // story points per sprint
}
/**
* Calculate throughput
*/
async calculateThroughput() {
// Simplified: Would count completed issues
return 12; // issues per week
}
/**
* Calculate lead time
*/
async calculateLeadTime() {
// Simplified: Would analyze issue lifecycle
return 5.2; // days
}
/**
* Calculate cycle time
*/
async calculateCycleTime() {
// Simplified: Would analyze work in progress
return 3.8; // days
}
// ========================================================================
// Time Series Methods
// ========================================================================
/**
* Record a metric point
*/
recordMetric(name, value, labels) {
let series = this.metrics.get(name);
if (!series) {
series = {
name,
type: 'gauge',
points: []
};
this.metrics.set(name, series);
}
series.points.push({
timestamp: new Date(),
value,
labels
});
this.pruneOldPoints(series);
}
/**
* Get metric series
*/
getMetric(name) {
return this.metrics.get(name);
}
/**
* Get all metrics
*/
getAllMetrics() {
return Array.from(this.metrics.values());
}
/**
* Get recent snapshots
*/
getSnapshots(count = 100) {
return this.snapshots.slice(-count);
}
/**
* Get latest snapshot
*/
getLatestSnapshot() {
return this.snapshots[this.snapshots.length - 1];
}
// ========================================================================
// Analysis Methods
// ========================================================================
/**
* Calculate metric trend
*/
calculateTrend(metricName, windowSize = 10) {
const series = this.metrics.get(metricName);
if (!series || series.points.length < windowSize) {
return 'stable';
}
const recentPoints = series.points.slice(-windowSize);
const firstHalf = recentPoints.slice(0, windowSize / 2);
const secondHalf = recentPoints.slice(windowSize / 2);
const avgFirst = firstHalf.reduce((sum, p) => sum + p.value, 0) / firstHalf.length;
const avgSecond = secondHalf.reduce((sum, p) => sum + p.value, 0) / secondHalf.length;
const change = ((avgSecond - avgFirst) / avgFirst) * 100;
if (change > 5)
return 'increasing';
if (change < -5)
return 'decreasing';
return 'stable';
}
/**
* Get metric statistics
*/
getStatistics(metricName) {
const series = this.metrics.get(metricName);
if (!series || series.points.length === 0) {
return null;
}
const values = series.points.map(p => p.value);
const sorted = [...values].sort((a, b) => a - b);
const min = sorted[0];
const max = sorted[sorted.length - 1];
const avg = values.reduce((sum, v) => sum + v, 0) / values.length;
const median = sorted[Math.floor(sorted.length / 2)];
const variance = values.reduce((sum, v) => sum + Math.pow(v - avg, 2), 0) / values.length;
const stdDev = Math.sqrt(variance);
return { min, max, avg, median, stdDev };
}
// ========================================================================
// Export Methods
// ========================================================================
/**
* Export metrics to JSON
*/
exportJSON() {
return JSON.stringify({
snapshots: this.snapshots,
series: Array.from(this.metrics.values())
}, null, 2);
}
/**
* Export metrics to CSV
*/
exportCSV() {
const lines = [];
// Header
lines.push('timestamp,metric,value');
// Data
for (const series of this.metrics.values()) {
for (const point of series.points) {
lines.push(`${point.timestamp.toISOString()},${series.name},${point.value}`);
}
}
return lines.join('\n');
}
/**
* Export metrics in Prometheus format
*/
exportPrometheus() {
const lines = [];
for (const series of this.metrics.values()) {
// Type and help
lines.push(`# HELP ${series.name} ${series.description || series.name}`);
lines.push(`# TYPE ${series.name} ${series.type}`);
// Latest value
const latest = series.points[series.points.length - 1];
if (latest) {
const labels = latest.labels
? '{' + Object.entries(latest.labels).map(([k, v]) => `${k}="${v}"`).join(',') + '}'
: '';
lines.push(`${series.name}${labels} ${latest.value}`);
}
}
return lines.join('\n');
}
/**
* Export to file
*/
async exportToFile(format, filePath) {
let content;
switch (format) {
case 'json':
content = this.exportJSON();
break;
case 'csv':
content = this.exportCSV();
break;
case 'prometheus':
content = this.exportPrometheus();
break;
}
await fs.writeFile(filePath, content, 'utf-8');
}
// ========================================================================
// Persistence Methods
// ========================================================================
/**
* Save metrics to disk
*/
async persist() {
try {
await fs.mkdir(this.config.storePath, { recursive: true });
const data = {
snapshots: this.snapshots,
metrics: Array.from(this.metrics.entries())
};
const filePath = path.join(this.config.storePath, 'metrics.json');
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
}
catch {
// Ignore persistence errors
}
}
/**
* Load metrics from disk
*/
async load() {
try {
const filePath = path.join(this.config.storePath, 'metrics.json');
const content = await fs.readFile(filePath, 'utf-8');
const data = JSON.parse(content);
this.snapshots = data.snapshots || [];
this.metrics = new Map(data.metrics || []);
}
catch {
// No existing data
}
}
/**
* Prune old snapshots (NFR-METRICS-003: 90 days retention)
*/
async pruneOldSnapshots() {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
this.snapshots = this.snapshots.filter(snapshot => snapshot.timestamp > cutoffDate);
}
/**
* Prune old metric points
*/
pruneOldPoints(series) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
series.points = series.points.filter(point => point.timestamp > cutoffDate);
}
/**
* Clear all metrics
*/
clear() {
this.metrics.clear();
this.snapshots = [];
}
}
//# sourceMappingURL=metrics-collector.js.map