Skip to main content
Glama
reporting-analytics-manager.ts18.8 kB
/** * TestRail Reporting and Analytics Tools * Comprehensive reporting, metrics, and analytics functionality */ import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { TestRailService } from '../utils/testrail-service'; import { TestRailStats, TestRailExecutionSummary, TestRailCaseMetrics, TestRailTrendData, TestRailErrorCodes, } from '../types'; export class ReportingAnalyticsManager { private testRailService: TestRailService; constructor(testRailService: TestRailService) { this.testRailService = testRailService; } /** * Generate comprehensive project dashboard */ async generateProjectDashboard(params: { projectId: number; timeRange?: { start: string; // YYYY-MM-DD end: string; // YYYY-MM-DD }; includeMetrics?: boolean; includeTrends?: boolean; includeTopFailures?: boolean; }): Promise<CallToolResult> { try { const project = await this.testRailService.getProject(params.projectId); const suites = await this.testRailService.getSuites(params.projectId); const runs = await this.testRailService.getRuns(params.projectId, { limit: 100 }); const milestones = await this.testRailService.getMilestones(params.projectId); // Calculate overall project statistics const overallStats = this.calculateProjectStats(runs); // Generate suite breakdown const suiteBreakdown = await this.generateSuiteBreakdown(params.projectId, suites); // Generate trends if requested let trends = null; if (params.includeTrends) { trends = await this.generateTrendAnalysis(params.projectId, params.timeRange); } // Top failures analysis let topFailures = null; if (params.includeTopFailures) { topFailures = await this.analyzeTopFailures(params.projectId, runs.slice(0, 10)); } const dashboard = { project: { id: project.id, name: project.name, suite_mode: project.suite_mode, is_completed: project.is_completed, }, overview: { total_suites: suites.length, total_runs: runs.length, active_milestones: milestones.filter((m) => !m.is_completed).length, completed_milestones: milestones.filter((m) => m.is_completed).length, }, statistics: overallStats, suite_breakdown: suiteBreakdown, trends, top_failures: topFailures, generated_at: new Date().toISOString(), }; return this.createSuccessResponse(dashboard, 'Project dashboard generated successfully'); } catch (error) { return this.createErrorResponse( error instanceof Error ? error.message : 'Failed to generate project dashboard', TestRailErrorCodes.API_ERROR ); } } /** * Generate detailed test execution report */ async generateExecutionReport(params: { runId?: number; planId?: number; projectId: number; format?: 'summary' | 'detailed' | 'executive'; includeFailureAnalysis?: boolean; includePerformanceMetrics?: boolean; }): Promise<CallToolResult> { try { let reportData: any = { project_id: params.projectId, format: params.format || 'summary', generated_at: new Date().toISOString(), }; if (params.runId) { // Single run report const run = await this.testRailService.getRun(params.runId); const tests = await this.testRailService.getTests(params.runId); reportData.run = run; reportData.execution_summary = this.generateExecutionSummary(run, tests); if (params.includeFailureAnalysis) { reportData.failure_analysis = await this.analyzeRunFailures(params.runId, tests); } if (params.includePerformanceMetrics) { reportData.performance_metrics = await this.calculatePerformanceMetrics(tests); } } else if (params.planId) { // Test plan report const plan = await this.testRailService.getPlan(params.planId); reportData.plan = plan; reportData.plan_summary = this.generatePlanSummary(plan); } return this.createSuccessResponse(reportData, 'Execution report generated successfully'); } catch (error) { return this.createErrorResponse( error instanceof Error ? error.message : 'Failed to generate execution report', TestRailErrorCodes.API_ERROR ); } } /** * Analyze test case metrics and health */ async analyzeCaseMetrics(params: { projectId: number; suiteId?: number; timeRange?: { start: string; end: string; }; includeFlakiness?: boolean; includeExecutionTrends?: boolean; }): Promise<CallToolResult> { try { const cases = await this.testRailService.getCases(params.projectId, params.suiteId); const runs = await this.testRailService.getRuns(params.projectId, { limit: 50 }); const caseMetrics = []; for (const testCase of cases.slice(0, 100)) { // Limit for performance const metrics = await this.calculateCaseMetrics(testCase, runs); if (params.includeFlakiness) { metrics.flakiness_score = await this.calculateFlakiness(testCase.id, runs); } caseMetrics.push(metrics); } // Sort by various criteria const analysis = { total_cases: cases.length, analyzed_cases: caseMetrics.length, metrics: { most_executed: caseMetrics .sort((a, b) => b.execution_count - a.execution_count) .slice(0, 10), least_executed: caseMetrics .filter((m) => m.execution_count > 0) .sort((a, b) => a.execution_count - b.execution_count) .slice(0, 10), most_failures: caseMetrics.sort((a, b) => b.fail_count - a.fail_count).slice(0, 10), never_executed: caseMetrics.filter((m) => m.execution_count === 0).length, high_flakiness: params.includeFlakiness ? caseMetrics.filter((m) => (m.flakiness_score || 0) > 0.3).slice(0, 10) : undefined, }, summary: { avg_execution_count: caseMetrics.reduce((sum, m) => sum + m.execution_count, 0) / caseMetrics.length, total_executions: caseMetrics.reduce((sum, m) => sum + m.execution_count, 0), avg_pass_rate: caseMetrics.reduce( (sum, m) => sum + (m.execution_count > 0 ? m.pass_count / m.execution_count : 0), 0 ) / caseMetrics.length, }, }; return this.createSuccessResponse(analysis, 'Case metrics analysis completed successfully'); } catch (error) { return this.createErrorResponse( error instanceof Error ? error.message : 'Failed to analyze case metrics', TestRailErrorCodes.API_ERROR ); } } /** * Generate test coverage report */ async generateCoverageReport(params: { projectId: number; suiteId?: number; requirementField?: string; // Custom field containing requirements componentField?: string; // Custom field containing components }): Promise<CallToolResult> { try { const cases = await this.testRailService.getCases(params.projectId, params.suiteId); // Analyze coverage by priority const priorityCoverage = this.analyzePriorityCoverage(cases); // Analyze coverage by type const typeCoverage = this.analyzeTypeCoverage(cases); // Analyze automation coverage const automationCoverage = this.analyzeAutomationCoverage(cases); // Analyze requirement coverage if field specified let requirementCoverage = null; if (params.requirementField) { requirementCoverage = this.analyzeRequirementCoverage(cases, params.requirementField); } // Analyze component coverage if field specified let componentCoverage = null; if (params.componentField) { componentCoverage = this.analyzeComponentCoverage(cases, params.componentField); } const coverage = { project_id: params.projectId, suite_id: params.suiteId, total_cases: cases.length, priority_coverage: priorityCoverage, type_coverage: typeCoverage, automation_coverage: automationCoverage, requirement_coverage: requirementCoverage, component_coverage: componentCoverage, gaps: this.identifyCoverageGaps(priorityCoverage, typeCoverage, automationCoverage), recommendations: this.generateCoverageRecommendations( priorityCoverage, typeCoverage, automationCoverage ), }; return this.createSuccessResponse(coverage, 'Coverage report generated successfully'); } catch (error) { return this.createErrorResponse( error instanceof Error ? error.message : 'Failed to generate coverage report', TestRailErrorCodes.API_ERROR ); } } /** * Generate trend analysis */ async generateTrendAnalysis( projectId: number, timeRange?: { start: string; end: string } ): Promise<any> { const runs = await this.testRailService.getRuns(projectId, { limit: 50 }); // Filter runs by time range if provided let filteredRuns = runs; if (timeRange) { const startTime = new Date(timeRange.start).getTime() / 1000; const endTime = new Date(timeRange.end).getTime() / 1000; filteredRuns = runs.filter((run) => run.created_on >= startTime && run.created_on <= endTime); } const trendData: TestRailTrendData[] = filteredRuns.map((run) => ({ date: new Date(run.created_on * 1000).toISOString().split('T')[0], stats: { total_tests: run.passed_count + run.failed_count + run.blocked_count + run.untested_count + run.retest_count, passed: run.passed_count, failed: run.failed_count, blocked: run.blocked_count, untested: run.untested_count, retest: run.retest_count, pass_rate: this.calculatePassRate(run), completion_rate: this.calculateCompletionRate(run), }, })); return { period: timeRange || { start: 'all_time', end: 'now' }, data_points: trendData.length, trends: trendData, analysis: { avg_pass_rate: trendData.reduce((sum, d) => sum + d.stats.pass_rate, 0) / trendData.length, avg_completion_rate: trendData.reduce((sum, d) => sum + d.stats.completion_rate, 0) / trendData.length, trend_direction: this.calculateTrendDirection(trendData), }, }; } // Helper methods private calculateProjectStats(runs: any[]): TestRailStats { const totalTests = runs.reduce( (sum, run) => sum + run.passed_count + run.failed_count + run.blocked_count + run.untested_count + run.retest_count, 0 ); const totalPassed = runs.reduce((sum, run) => sum + run.passed_count, 0); const totalFailed = runs.reduce((sum, run) => sum + run.failed_count, 0); const totalBlocked = runs.reduce((sum, run) => sum + run.blocked_count, 0); const totalUntested = runs.reduce((sum, run) => sum + run.untested_count, 0); const totalRetest = runs.reduce((sum, run) => sum + run.retest_count, 0); return { total_tests: totalTests, passed: totalPassed, failed: totalFailed, blocked: totalBlocked, untested: totalUntested, retest: totalRetest, pass_rate: totalTests > 0 ? (totalPassed / totalTests) * 100 : 0, completion_rate: totalTests > 0 ? ((totalTests - totalUntested) / totalTests) * 100 : 0, }; } private async generateSuiteBreakdown(projectId: number, suites: any[]): Promise<any[]> { const breakdown = []; for (const suite of suites.slice(0, 10)) { // Limit for performance try { const cases = await this.testRailService.getCases(projectId, suite.id); breakdown.push({ suite, case_count: cases.length, automation_count: cases.filter( (c) => c.custom_automation_type === 'Automated' || c.title.toLowerCase().includes('automated') ).length, }); } catch (error) { // Skip failed suites } } return breakdown; } private generateExecutionSummary(run: any, tests: any[]): TestRailExecutionSummary { const stats = { total_tests: tests.length, passed: tests.filter((t) => t.status_id === 1).length, failed: tests.filter((t) => t.status_id === 5).length, blocked: tests.filter((t) => t.status_id === 2).length, untested: tests.filter((t) => t.status_id === 3).length, retest: tests.filter((t) => t.status_id === 4).length, pass_rate: 0, completion_rate: 0, }; stats.pass_rate = stats.total_tests > 0 ? (stats.passed / stats.total_tests) * 100 : 0; stats.completion_rate = stats.total_tests > 0 ? ((stats.total_tests - stats.untested) / stats.total_tests) * 100 : 0; return { run_id: run.id, run_name: run.name, project_name: 'Unknown', suite_name: 'Unknown', stats, start_date: new Date(run.created_on * 1000).toISOString(), end_date: run.completed_on ? new Date(run.completed_on * 1000).toISOString() : '', assigned_to: run.assignedto_id?.toString(), }; } private async calculateCaseMetrics(testCase: any, runs: any[]): Promise<TestRailCaseMetrics> { let executionCount = 0; let passCount = 0; let failCount = 0; let lastExecuted = ''; // This is a simplified calculation - in real implementation, // you'd need to get actual test results for each case for (const run of runs.slice(0, 10)) { try { const tests = await this.testRailService.getTests(run.id); const caseTest = tests.find((t) => t.case_id === testCase.id); if (caseTest && caseTest.status_id !== 3) { // Not untested executionCount++; if (caseTest.status_id === 1) passCount++; if (caseTest.status_id === 5) failCount++; lastExecuted = new Date(run.created_on * 1000).toISOString(); } } catch (error) { // Skip failed runs } } return { case_id: testCase.id, title: testCase.title, execution_count: executionCount, pass_count: passCount, fail_count: failCount, last_executed: lastExecuted || 'Never', }; } private analyzePriorityCoverage(cases: any[]): any { const priorityCount = cases.reduce( (acc, case_) => { acc[case_.priority_id] = (acc[case_.priority_id] || 0) + 1; return acc; }, {} as Record<number, number> ); return { by_priority: priorityCount, total: cases.length, coverage_distribution: Object.entries(priorityCount).map(([priority, count]) => ({ priority_id: parseInt(priority), count, percentage: ((count as number) / cases.length) * 100, })), }; } private analyzeTypeCoverage(cases: any[]): any { const typeCount = cases.reduce( (acc, case_) => { acc[case_.type_id] = (acc[case_.type_id] || 0) + 1; return acc; }, {} as Record<number, number> ); return { by_type: typeCount, total: cases.length, }; } private analyzeAutomationCoverage(cases: any[]): any { const automated = cases.filter( (c) => c.custom_automation_type === 'Automated' || c.title.toLowerCase().includes('automated') ).length; const manual = cases.length - automated; return { total_cases: cases.length, automated_cases: automated, manual_cases: manual, automation_percentage: cases.length > 0 ? (automated / cases.length) * 100 : 0, }; } private calculatePassRate(run: any): number { const total = run.passed_count + run.failed_count + run.blocked_count + run.retest_count; return total > 0 ? (run.passed_count / total) * 100 : 0; } private calculateCompletionRate(run: any): number { const total = run.passed_count + run.failed_count + run.blocked_count + run.untested_count + run.retest_count; return total > 0 ? ((total - run.untested_count) / total) * 100 : 0; } private calculateTrendDirection(trendData: TestRailTrendData[]): string { if (trendData.length < 2) return 'insufficient_data'; const recent = trendData.slice(-3); const older = trendData.slice(0, 3); const recentAvg = recent.reduce((sum, d) => sum + d.stats.pass_rate, 0) / recent.length; const olderAvg = older.reduce((sum, d) => sum + d.stats.pass_rate, 0) / older.length; if (recentAvg > olderAvg * 1.05) return 'improving'; if (recentAvg < olderAvg * 0.95) return 'declining'; return 'stable'; } private createSuccessResponse(data: any, message?: string): CallToolResult { return { content: [ { type: 'text', text: JSON.stringify( { success: true, data, message: message || 'Operation completed successfully', }, null, 2 ), }, ], }; } private createErrorResponse(error: string, code?: string): CallToolResult { return { content: [ { type: 'text', text: JSON.stringify( { success: false, error, code: code || TestRailErrorCodes.INTERNAL_ERROR, timestamp: new Date().toISOString(), }, null, 2 ), }, ], isError: true, }; } // Additional helper methods (simplified for brevity) private async analyzeTopFailures(_projectId: number, _runs: any[]): Promise<any> { return null; } private async analyzeRunFailures(_runId: number, _tests: any[]): Promise<any> { return null; } private async calculatePerformanceMetrics(_tests: any[]): Promise<any> { return null; } private generatePlanSummary(_plan: any): any { return null; } private async calculateFlakiness(_caseId: number, _runs: any[]): Promise<number> { return 0; } private analyzeRequirementCoverage(_cases: any[], _field: string): any { return null; } private analyzeComponentCoverage(_cases: any[], _field: string): any { return null; } private identifyCoverageGaps(_priority: any, _type: any, _automation: any): any[] { return []; } private generateCoverageRecommendations(_priority: any, _type: any, _automation: any): any[] { return []; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/samuelvinay91/testrail-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server