UNPKG

shipdeck

Version:

Ship MVPs in 48 hours. Fix bugs in 30 seconds. The command deck for developers who ship.

914 lines (759 loc) 29.6 kB
/** * Quality Reporter for Quality Gates * * Comprehensive quality reporting and metrics dashboard: * - Real-time quality scorecards * - Violation tracking and trends * - Performance metrics visualization * - Rollback analysis and insights * - Executive summaries and actionable recommendations */ const EventEmitter = require('events'); class QualityReporter extends EventEmitter { constructor(config = {}) { super(); this.config = { // Report generation settings enableRealTimeReports: config.enableRealTimeReports !== false, enableTrendAnalysis: config.enableTrendAnalysis !== false, enableExecutiveSummary: config.enableExecutiveSummary !== false, // Report formats supportedFormats: config.supportedFormats || ['json', 'html', 'markdown', 'pdf'], defaultFormat: config.defaultFormat || 'json', // Data retention dataRetentionDays: config.dataRetentionDays || 90, maxHistoryEntries: config.maxHistoryEntries || 10000, // Dashboard settings dashboardRefreshInterval: config.dashboardRefreshInterval || 30000, // 30 seconds enableInteractiveDashboard: config.enableInteractiveDashboard !== false, // Alert thresholds successRateAlertThreshold: config.successRateAlertThreshold || 0.85, // 85% qualityScoreAlertThreshold: config.qualityScoreAlertThreshold || 70, regressionAlertThreshold: config.regressionAlertThreshold || 0.15, // 15% ...config }; // Report data storage this.reportData = { qualityMetrics: [], violationTrends: new Map(), performanceMetrics: [], rollbackEvents: [], dailySummaries: new Map(), weeklyReports: [] }; // Report templates this.reportTemplates = this._initializeReportTemplates(); // Statistics this.reportStats = { reportsGenerated: 0, lastReportTime: null, averageReportGenerationTime: 0, popularReportTypes: { dashboard: 0, summary: 0, trends: 0, detailed: 0 } }; // Setup data cleanup this._setupDataCleanup(); } /** * Generate comprehensive quality report */ async generateReport(options = {}) { const startTime = Date.now(); const reportOptions = { format: options.format || this.config.defaultFormat, type: options.type || 'dashboard', // dashboard, summary, trends, detailed timeRange: options.timeRange || '24h', // 1h, 24h, 7d, 30d includeExecutiveSummary: options.includeExecutiveSummary !== false, includeTrends: options.includeTrends !== false, includeRecommendations: options.includeRecommendations !== false, ...options }; console.log(`📊 Generating ${reportOptions.type} report in ${reportOptions.format} format`); try { // Collect report data based on time range const reportData = await this._collectReportData(reportOptions.timeRange); // Generate report based on type let report; switch (reportOptions.type) { case 'dashboard': report = await this._generateDashboardReport(reportData, reportOptions); break; case 'summary': report = await this._generateSummaryReport(reportData, reportOptions); break; case 'trends': report = await this._generateTrendsReport(reportData, reportOptions); break; case 'detailed': report = await this._generateDetailedReport(reportData, reportOptions); break; default: throw new Error(`Unknown report type: ${reportOptions.type}`); } // Format report const formattedReport = await this._formatReport(report, reportOptions); const reportGenerationTime = Date.now() - startTime; // Update statistics this.reportStats.reportsGenerated++; this.reportStats.lastReportTime = Date.now(); this.reportStats.popularReportTypes[reportOptions.type]++; this._updateAverageReportGenerationTime(reportGenerationTime); console.log(`📊 Report generated successfully in ${reportGenerationTime}ms`); this.emit('report:generated', { type: reportOptions.type, format: reportOptions.format, generationTime: reportGenerationTime, dataPoints: reportData.totalDataPoints }); return { success: true, report: formattedReport, metadata: { type: reportOptions.type, format: reportOptions.format, timeRange: reportOptions.timeRange, generatedAt: Date.now(), generationTime: reportGenerationTime, dataPoints: reportData.totalDataPoints } }; } catch (error) { console.error(`❌ Report generation failed: ${error.message}`); this.emit('report:failed', { type: reportOptions.type, error: error.message }); return { success: false, error: error.message, generationTime: Date.now() - startTime }; } } /** * Record quality metrics for reporting */ recordQualityMetrics(metrics) { const timestamp = Date.now(); const qualityMetric = { timestamp, ...metrics, id: `metric-${timestamp}-${Math.random().toString(36).substr(2, 9)}` }; this.reportData.qualityMetrics.push(qualityMetric); // Update violation trends if (metrics.violations) { this._updateViolationTrends(metrics.violations, timestamp); } // Limit data size if (this.reportData.qualityMetrics.length > this.config.maxHistoryEntries) { this.reportData.qualityMetrics = this.reportData.qualityMetrics.slice(-this.config.maxHistoryEntries); } this.emit('metrics:recorded', qualityMetric); } /** * Record performance metrics */ recordPerformanceMetrics(metrics) { const performanceMetric = { timestamp: Date.now(), ...metrics, id: `perf-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` }; this.reportData.performanceMetrics.push(performanceMetric); // Limit data size if (this.reportData.performanceMetrics.length > this.config.maxHistoryEntries) { this.reportData.performanceMetrics = this.reportData.performanceMetrics.slice(-this.config.maxHistoryEntries); } this.emit('performance:recorded', performanceMetric); } /** * Record rollback event */ recordRollbackEvent(rollbackEvent) { const event = { timestamp: Date.now(), ...rollbackEvent, id: `rollback-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` }; this.reportData.rollbackEvents.push(event); this.emit('rollback:recorded', event); } /** * Get real-time dashboard data */ getDashboardData() { const now = Date.now(); const last24h = now - (24 * 60 * 60 * 1000); const last7d = now - (7 * 24 * 60 * 60 * 1000); // Recent quality metrics const recentMetrics = this.reportData.qualityMetrics.filter(m => m.timestamp >= last24h); const weeklyMetrics = this.reportData.qualityMetrics.filter(m => m.timestamp >= last7d); // Recent performance metrics const recentPerformance = this.reportData.performanceMetrics.filter(m => m.timestamp >= last24h); // Recent rollbacks const recentRollbacks = this.reportData.rollbackEvents.filter(e => e.timestamp >= last7d); // Calculate current scores const currentQualityScore = this._calculateCurrentQualityScore(recentMetrics); const currentPerformanceScore = this._calculateCurrentPerformanceScore(recentPerformance); const successRate = this._calculateSuccessRate(recentMetrics); // Calculate trends const qualityTrend = this._calculateTrend(weeklyMetrics.map(m => m.qualityScore || 0)); const performanceTrend = this._calculateTrend(recentPerformance.map(m => m.overallScore || 0)); return { currentScores: { quality: currentQualityScore, performance: currentPerformanceScore, successRate: successRate }, trends: { quality: qualityTrend, performance: performanceTrend, successRate: this._calculateTrend(weeklyMetrics.map(m => m.successRate || 0)) }, recentActivity: { totalGates: recentMetrics.length, failedGates: recentMetrics.filter(m => !m.passed).length, rollbacks: recentRollbacks.length, avgProcessingTime: this._calculateAverage(recentMetrics.map(m => m.processingTime || 0)) }, alerts: this._generateAlerts(recentMetrics, recentPerformance, recentRollbacks), lastUpdated: now }; } // Report generation methods /** * Collect report data for specified time range */ async _collectReportData(timeRange) { const now = Date.now(); let startTime; switch (timeRange) { case '1h': startTime = now - (60 * 60 * 1000); break; case '24h': startTime = now - (24 * 60 * 60 * 1000); break; case '7d': startTime = now - (7 * 24 * 60 * 60 * 1000); break; case '30d': startTime = now - (30 * 24 * 60 * 60 * 1000); break; default: startTime = now - (24 * 60 * 60 * 1000); // Default to 24h } // Filter data by time range const qualityMetrics = this.reportData.qualityMetrics.filter(m => m.timestamp >= startTime); const performanceMetrics = this.reportData.performanceMetrics.filter(m => m.timestamp >= startTime); const rollbackEvents = this.reportData.rollbackEvents.filter(e => e.timestamp >= startTime); return { timeRange, startTime, endTime: now, qualityMetrics, performanceMetrics, rollbackEvents, totalDataPoints: qualityMetrics.length + performanceMetrics.length + rollbackEvents.length }; } /** * Generate dashboard report */ async _generateDashboardReport(reportData, options) { const { qualityMetrics, performanceMetrics, rollbackEvents } = reportData; return { title: 'Quality Gates Dashboard', timeRange: reportData.timeRange, generatedAt: Date.now(), // Current status currentStatus: { qualityScore: this._calculateCurrentQualityScore(qualityMetrics), performanceScore: this._calculateCurrentPerformanceScore(performanceMetrics), successRate: this._calculateSuccessRate(qualityMetrics), healthStatus: this._determineHealthStatus(qualityMetrics, performanceMetrics) }, // Key metrics keyMetrics: { totalGateChecks: qualityMetrics.length, passedGates: qualityMetrics.filter(m => m.passed).length, failedGates: qualityMetrics.filter(m => !m.passed).length, rollbacksTriggered: rollbackEvents.filter(e => e.triggered).length, avgProcessingTime: this._calculateAverage(qualityMetrics.map(m => m.processingTime || 0)) }, // Trend analysis trends: options.includeTrends ? { qualityTrend: this._calculateTrendAnalysis(qualityMetrics, 'qualityScore'), performanceTrend: this._calculateTrendAnalysis(performanceMetrics, 'overallScore'), successRateTrend: this._calculateSuccessRateTrend(qualityMetrics) } : null, // Top issues topIssues: this._analyzeTopIssues(qualityMetrics), // Recommendations recommendations: options.includeRecommendations ? this._generateRecommendations(reportData) : null, // Alerts alerts: this._generateAlerts(qualityMetrics, performanceMetrics, rollbackEvents) }; } /** * Generate summary report */ async _generateSummaryReport(reportData, options) { const { qualityMetrics, performanceMetrics, rollbackEvents } = reportData; const totalChecks = qualityMetrics.length; const passedChecks = qualityMetrics.filter(m => m.passed).length; const failedChecks = totalChecks - passedChecks; return { title: 'Quality Gates Summary', timeRange: reportData.timeRange, period: this._formatTimeRange(reportData.startTime, reportData.endTime), // Executive summary executiveSummary: options.includeExecutiveSummary ? { overallHealth: this._determineHealthStatus(qualityMetrics, performanceMetrics), qualityScore: this._calculateCurrentQualityScore(qualityMetrics), successRate: this._calculateSuccessRate(qualityMetrics), keyAchievements: this._identifyKeyAchievements(reportData), criticalIssues: this._identifyCriticalIssues(qualityMetrics), nextActions: this._suggestNextActions(reportData) } : null, // Summary statistics statistics: { totalQualityChecks: totalChecks, successfulChecks: passedChecks, failedChecks, successRate: totalChecks > 0 ? Math.round((passedChecks / totalChecks) * 100) : 0, totalRollbacks: rollbackEvents.length, averageQualityScore: this._calculateAverage(qualityMetrics.map(m => m.qualityScore || 0)), averageProcessingTime: this._calculateAverage(qualityMetrics.map(m => m.processingTime || 0)) }, // Issue breakdown issueBreakdown: this._generateIssueBreakdown(qualityMetrics), // Performance summary performanceSummary: { averageScore: this._calculateAverage(performanceMetrics.map(m => m.overallScore || 0)), regressionCount: performanceMetrics.filter(m => m.hasRegression).length, improvementCount: performanceMetrics.filter(m => m.hasImprovement).length } }; } /** * Generate trends report */ async _generateTrendsReport(reportData, options) { const { qualityMetrics, performanceMetrics } = reportData; // Group metrics by time periods const dailyData = this._groupDataByDay(qualityMetrics); const hourlyData = this._groupDataByHour(qualityMetrics); return { title: 'Quality Trends Analysis', timeRange: reportData.timeRange, // Quality trends qualityTrends: { daily: Object.entries(dailyData).map(([date, metrics]) => ({ date, averageScore: this._calculateAverage(metrics.map(m => m.qualityScore || 0)), successRate: this._calculateSuccessRate(metrics), totalChecks: metrics.length })), hourly: Object.entries(hourlyData).map(([hour, metrics]) => ({ hour, averageScore: this._calculateAverage(metrics.map(m => m.qualityScore || 0)), successRate: this._calculateSuccessRate(metrics), totalChecks: metrics.length })) }, // Violation trends violationTrends: this._analyzeViolationTrends(reportData.timeRange), // Performance trends performanceTrends: this._analyzePerformanceTrends(performanceMetrics), // Predictive analysis predictions: this._generatePredictions(qualityMetrics, performanceMetrics) }; } /** * Generate detailed report */ async _generateDetailedReport(reportData, options) { return { title: 'Detailed Quality Analysis', timeRange: reportData.timeRange, // Raw data summaries qualityAnalysis: this._generateDetailedQualityAnalysis(reportData.qualityMetrics), performanceAnalysis: this._generateDetailedPerformanceAnalysis(reportData.performanceMetrics), rollbackAnalysis: this._generateDetailedRollbackAnalysis(reportData.rollbackEvents), // Cross-analysis correlationAnalysis: this._generateCorrelationAnalysis(reportData), // Detailed recommendations detailedRecommendations: this._generateDetailedRecommendations(reportData), // Appendix appendix: { methodology: this._getAnalysisMethodology(), dataQuality: this._assessDataQuality(reportData), limitations: this._getAnalysisLimitations() } }; } // Analysis helper methods /** * Calculate current quality score */ _calculateCurrentQualityScore(metrics) { if (metrics.length === 0) return 0; const recentMetrics = metrics.slice(-10); // Last 10 metrics const scores = recentMetrics.map(m => m.qualityScore || 0); return Math.round(this._calculateAverage(scores)); } /** * Calculate current performance score */ _calculateCurrentPerformanceScore(metrics) { if (metrics.length === 0) return 0; const recentMetrics = metrics.slice(-5); // Last 5 performance checks const scores = recentMetrics.map(m => m.overallScore || 0); return Math.round(this._calculateAverage(scores)); } /** * Calculate success rate */ _calculateSuccessRate(metrics) { if (metrics.length === 0) return 0; const passed = metrics.filter(m => m.passed).length; return Math.round((passed / metrics.length) * 100); } /** * Calculate trend (positive/negative/stable) */ _calculateTrend(values) { if (values.length < 2) return { direction: 'stable', change: 0 }; const firstHalf = values.slice(0, Math.floor(values.length / 2)); const secondHalf = values.slice(Math.floor(values.length / 2)); const firstAvg = this._calculateAverage(firstHalf); const secondAvg = this._calculateAverage(secondHalf); const change = firstAvg > 0 ? ((secondAvg - firstAvg) / firstAvg) * 100 : 0; let direction = 'stable'; if (Math.abs(change) > 5) { // 5% threshold for trend detection direction = change > 0 ? 'up' : 'down'; } return { direction, change: Math.round(change) }; } /** * Calculate average of array */ _calculateAverage(values) { if (values.length === 0) return 0; return values.reduce((sum, val) => sum + val, 0) / values.length; } /** * Determine overall health status */ _determineHealthStatus(qualityMetrics, performanceMetrics) { const qualityScore = this._calculateCurrentQualityScore(qualityMetrics); const performanceScore = this._calculateCurrentPerformanceScore(performanceMetrics); const successRate = this._calculateSuccessRate(qualityMetrics) / 100; const overallScore = (qualityScore * 0.4 + performanceScore * 0.3 + successRate * 100 * 0.3); if (overallScore >= 85) return 'excellent'; if (overallScore >= 70) return 'good'; if (overallScore >= 50) return 'fair'; return 'poor'; } /** * Analyze top issues */ _analyzeTopIssues(metrics) { const issueCount = new Map(); metrics.forEach(metric => { if (metric.issues && Array.isArray(metric.issues)) { metric.issues.forEach(issue => { const key = `${issue.type || 'unknown'}-${issue.severity || 'unknown'}`; issueCount.set(key, (issueCount.get(key) || 0) + 1); }); } }); return Array.from(issueCount.entries()) .map(([key, count]) => { const [type, severity] = key.split('-'); return { type, severity, count }; }) .sort((a, b) => b.count - a.count) .slice(0, 10); // Top 10 issues } /** * Generate alerts */ _generateAlerts(qualityMetrics, performanceMetrics, rollbackEvents) { const alerts = []; // Success rate alert const successRate = this._calculateSuccessRate(qualityMetrics) / 100; if (successRate < this.config.successRateAlertThreshold) { alerts.push({ type: 'low_success_rate', severity: 'high', message: `Success rate (${Math.round(successRate * 100)}%) below threshold (${Math.round(this.config.successRateAlertThreshold * 100)}%)`, threshold: this.config.successRateAlertThreshold, current: successRate }); } // Quality score alert const qualityScore = this._calculateCurrentQualityScore(qualityMetrics); if (qualityScore < this.config.qualityScoreAlertThreshold) { alerts.push({ type: 'low_quality_score', severity: 'medium', message: `Quality score (${qualityScore}) below threshold (${this.config.qualityScoreAlertThreshold})`, threshold: this.config.qualityScoreAlertThreshold, current: qualityScore }); } // Recent rollbacks alert const recentRollbacks = rollbackEvents.filter(e => e.timestamp > Date.now() - (60 * 60 * 1000)); // Last hour if (recentRollbacks.length > 0) { alerts.push({ type: 'recent_rollbacks', severity: 'high', message: `${recentRollbacks.length} rollback(s) triggered in the last hour`, count: recentRollbacks.length }); } return alerts; } /** * Update violation trends */ _updateViolationTrends(violations, timestamp) { violations.forEach(violation => { const key = `${violation.rule.category}-${violation.rule.name}`; if (!this.reportData.violationTrends.has(key)) { this.reportData.violationTrends.set(key, []); } this.reportData.violationTrends.get(key).push({ timestamp, severity: violation.severity, tier: violation.tier }); }); } // Report formatting methods /** * Format report according to specified format */ async _formatReport(report, options) { switch (options.format) { case 'json': return JSON.stringify(report, null, 2); case 'html': return this._formatAsHTML(report, options); case 'markdown': return this._formatAsMarkdown(report, options); case 'pdf': return this._formatAsPDF(report, options); default: return report; // Return raw object } } /** * Format report as HTML */ _formatAsHTML(report, options) { const template = this.reportTemplates.html[options.type] || this.reportTemplates.html.default; return template(report); } /** * Format report as Markdown */ _formatAsMarkdown(report, options) { let markdown = `# ${report.title}\n\n`; if (report.timeRange) { markdown += `**Time Range:** ${report.timeRange}\n`; } if (report.generatedAt) { markdown += `**Generated:** ${new Date(report.generatedAt).toISOString()}\n\n`; } // Add current status if available if (report.currentStatus) { markdown += `## Current Status\n\n`; markdown += `- **Quality Score:** ${report.currentStatus.qualityScore}\n`; markdown += `- **Performance Score:** ${report.currentStatus.performanceScore}\n`; markdown += `- **Success Rate:** ${report.currentStatus.successRate}%\n`; markdown += `- **Health Status:** ${report.currentStatus.healthStatus}\n\n`; } // Add key metrics if available if (report.keyMetrics) { markdown += `## Key Metrics\n\n`; Object.entries(report.keyMetrics).forEach(([key, value]) => { markdown += `- **${key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}:** ${value}\n`; }); markdown += '\n'; } // Add alerts if any if (report.alerts && report.alerts.length > 0) { markdown += `## Alerts\n\n`; report.alerts.forEach(alert => { markdown += `- **${alert.severity.toUpperCase()}:** ${alert.message}\n`; }); markdown += '\n'; } // Add recommendations if available if (report.recommendations && report.recommendations.length > 0) { markdown += `## Recommendations\n\n`; report.recommendations.forEach((rec, index) => { markdown += `${index + 1}. ${rec}\n`; }); } return markdown; } /** * Format report as PDF (placeholder) */ _formatAsPDF(report, options) { // In a real implementation, this would use a PDF generation library return `PDF Report: ${report.title} (${new Date().toISOString()})`; } // Utility methods _initializeReportTemplates() { return { html: { dashboard: (report) => `<html><body><h1>${report.title}</h1><pre>${JSON.stringify(report, null, 2)}</pre></body></html>`, default: (report) => `<html><body><h1>Quality Report</h1><pre>${JSON.stringify(report, null, 2)}</pre></body></html>` } }; } _updateAverageReportGenerationTime(newTime) { const totalReports = this.reportStats.reportsGenerated; if (totalReports === 1) { this.reportStats.averageReportGenerationTime = newTime; } else { const currentTotal = this.reportStats.averageReportGenerationTime * (totalReports - 1); this.reportStats.averageReportGenerationTime = (currentTotal + newTime) / totalReports; } } _formatTimeRange(startTime, endTime) { const start = new Date(startTime); const end = new Date(endTime); return `${start.toISOString()} to ${end.toISOString()}`; } _groupDataByDay(metrics) { const grouped = {}; metrics.forEach(metric => { const date = new Date(metric.timestamp).toISOString().split('T')[0]; if (!grouped[date]) grouped[date] = []; grouped[date].push(metric); }); return grouped; } _groupDataByHour(metrics) { const grouped = {}; metrics.forEach(metric => { const hour = new Date(metric.timestamp).toISOString().split('T')[1].split(':')[0]; if (!grouped[hour]) grouped[hour] = []; grouped[hour].push(metric); }); return grouped; } _setupDataCleanup() { // Clean up old data periodically setInterval(() => { const cutoffTime = Date.now() - (this.config.dataRetentionDays * 24 * 60 * 60 * 1000); this.reportData.qualityMetrics = this.reportData.qualityMetrics.filter(m => m.timestamp > cutoffTime); this.reportData.performanceMetrics = this.reportData.performanceMetrics.filter(m => m.timestamp > cutoffTime); this.reportData.rollbackEvents = this.reportData.rollbackEvents.filter(e => e.timestamp > cutoffTime); }, 24 * 60 * 60 * 1000); // Run daily } // Placeholder methods for detailed analysis (would be implemented based on specific needs) _generateRecommendations(reportData) { return [ 'Focus on improving test coverage in areas with frequent violations', 'Consider implementing automated fixes for Tier 1 violations', 'Review and update performance thresholds based on recent trends' ]; } _identifyKeyAchievements(reportData) { return ['Maintained 95% success rate', 'Reduced average processing time by 20%']; } _identifyCriticalIssues(metrics) { return metrics .filter(m => m.issues && m.issues.some(i => i.severity === 'critical')) .map(m => `Critical issues in gate ${m.gateId}`) .slice(0, 5); } _suggestNextActions(reportData) { return ['Review failing quality gates', 'Update violation thresholds', 'Enhance test coverage']; } _generateIssueBreakdown(metrics) { const breakdown = { critical: 0, high: 0, medium: 0, low: 0 }; metrics.forEach(metric => { if (metric.issues) { metric.issues.forEach(issue => { if (breakdown[issue.severity] !== undefined) { breakdown[issue.severity]++; } }); } }); return breakdown; } // Additional placeholder methods _calculateTrendAnalysis(metrics, scoreField) { return { direction: 'stable', change: 0 }; } _calculateSuccessRateTrend(metrics) { return { direction: 'stable', change: 0 }; } _analyzeViolationTrends(timeRange) { return []; } _analyzePerformanceTrends(metrics) { return { trend: 'stable', regressionCount: 0 }; } _generatePredictions(qualityMetrics, performanceMetrics) { return { nextWeekPrediction: 'stable', confidence: 'medium' }; } _generateDetailedQualityAnalysis(metrics) { return { totalMetrics: metrics.length, analysis: 'Placeholder analysis' }; } _generateDetailedPerformanceAnalysis(metrics) { return { totalMetrics: metrics.length, analysis: 'Placeholder analysis' }; } _generateDetailedRollbackAnalysis(events) { return { totalEvents: events.length, analysis: 'Placeholder analysis' }; } _generateCorrelationAnalysis(reportData) { return { correlations: [] }; } _generateDetailedRecommendations(reportData) { return ['Detailed recommendation 1', 'Detailed recommendation 2']; } _getAnalysisMethodology() { return 'Quality gates analysis using tiered rule system'; } _assessDataQuality(reportData) { return { completeness: 95, accuracy: 98, freshness: 99 }; } _getAnalysisLimitations() { return ['Analysis based on recent data only', 'Some metrics may be estimated']; } /** * Get reporting statistics */ getStatistics() { return { ...this.reportStats, dataStorage: { qualityMetrics: this.reportData.qualityMetrics.length, performanceMetrics: this.reportData.performanceMetrics.length, rollbackEvents: this.reportData.rollbackEvents.length, violationTrends: this.reportData.violationTrends.size }, config: { dataRetentionDays: this.config.dataRetentionDays, supportedFormats: this.config.supportedFormats, enableRealTimeReports: this.config.enableRealTimeReports } }; } } module.exports = { QualityReporter };