├── .gitignore ├── package.json ├── action.yml ├── LICENSE ├── README.md └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | package-lock.json 4 | yarn.lock 5 | 6 | # Build output 7 | dist/ 8 | 9 | # Logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # IDE 16 | .vscode/ 17 | .idea/ 18 | *.swp 19 | *.swo 20 | *~ 21 | 22 | # OS 23 | .DS_Store 24 | Thumbs.db 25 | 26 | # Testing 27 | coverage/ 28 | .nyc_output/ 29 | 30 | # Environment 31 | .env 32 | .env.local 33 | 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pr-metrics-dashboard", 3 | "version": "1.0.0", 4 | "description": "GitHub Action tool", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "ncc build src/index.js -o dist --source-map --license licenses.txt" 8 | }, 9 | "keywords": ["github-action", "automation"], 10 | "author": "rkneela0912", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@actions/core": "^1.10.1", 14 | "@actions/github": "^6.0.0" 15 | }, 16 | "devDependencies": { 17 | "@vercel/ncc": "^0.38.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'PR Metrics Dashboard' 2 | description: '📊 Generate comprehensive PR activity metrics, charts, and insights' 3 | author: 'rkneela0912' 4 | 5 | inputs: 6 | github_token: 7 | description: 'GitHub token for API access' 8 | required: true 9 | days: 10 | description: 'Number of days to analyze' 11 | required: false 12 | default: '30' 13 | output_file: 14 | description: 'Output file path for metrics report' 15 | required: false 16 | default: 'PR_METRICS.md' 17 | include_charts: 18 | description: 'Include ASCII charts in report' 19 | required: false 20 | default: 'true' 21 | 22 | outputs: 23 | total_prs: 24 | description: 'Total number of PRs' 25 | merged_prs: 26 | description: 'Number of merged PRs' 27 | avg_time_to_merge: 28 | description: 'Average time to merge (hours)' 29 | report_file: 30 | description: 'Generated report file path' 31 | 32 | runs: 33 | using: 'node20' 34 | main: 'dist/index.js' 35 | 36 | branding: 37 | icon: 'bar-chart-2' 38 | color: 'blue' 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 PR Size Labeler Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PR Metrics Dashboard 📊 2 | 3 | [![GitHub release](https://img.shields.io/github/v/release/rkneela0912/pr-metrics-dashboard)](https://github.com/rkneela0912/pr-metrics-dashboard/releases) [![MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 4 | 5 | Generate comprehensive PR activity metrics, charts, and insights for your repository. Track team performance, identify bottlenecks, and improve your development workflow with data-driven insights. 6 | 7 | ## ✨ Features 8 | 9 | - **📈 Comprehensive Metrics:** Total PRs, merge rates, time to merge, and more 10 | - **👥 Top Contributors:** Track who's contributing and their merge rates 11 | - **📏 PR Size Analysis:** Distribution of PR sizes (XS, S, M, L, XL) 12 | - **📅 Activity Patterns:** See which days are most active 13 | - **📊 ASCII Charts:** Visual representations right in markdown 14 | - **💡 Automated Insights:** Get actionable recommendations 15 | - **⏱️ Time Tracking:** Average and median time to merge 16 | 17 | ## 🚀 Quick Start 18 | 19 | Create `.github/workflows/pr-metrics.yml`: 20 | 21 | ```yaml 22 | name: PR Metrics Dashboard 23 | 24 | on: 25 | schedule: 26 | - cron: '0 0 * * 0' # Weekly on Sunday 27 | workflow_dispatch: 28 | 29 | jobs: 30 | metrics: 31 | runs-on: ubuntu-latest 32 | permissions: 33 | contents: write 34 | pull-requests: read 35 | steps: 36 | - uses: actions/checkout@v4 37 | 38 | - name: Generate metrics 39 | uses: rkneela0912/pr-metrics-dashboard@v1 40 | with: 41 | github_token: ${{ secrets.GITHUB_TOKEN }} 42 | days: 30 43 | 44 | - name: Commit report 45 | run: | 46 | git config user.name github-actions 47 | git config user.email github-actions@github.com 48 | git add PR_METRICS.md 49 | git commit -m "docs: update PR metrics" || exit 0 50 | git push 51 | ``` 52 | 53 | ## 📊 Example Output 54 | 55 | The action generates a comprehensive markdown report with: 56 | 57 | - Overview metrics (total PRs, merge rate, avg time to merge) 58 | - Top contributors ranking 59 | - PR size distribution 60 | - Activity by day of week 61 | - ASCII charts for visualization 62 | - Automated insights and recommendations 63 | 64 | ## ⚙️ Configuration 65 | 66 | ### Inputs 67 | 68 | | Input | Description | Default | Required | 69 | |-------|-------------|---------|----------| 70 | | `github_token` | GitHub token for API access | - | ✅ Yes | 71 | | `days` | Number of days to analyze | `30` | No | 72 | | `output_file` | Output file path | `PR_METRICS.md` | No | 73 | | `include_charts` | Include ASCII charts | `true` | No | 74 | 75 | ### Outputs 76 | 77 | | Output | Description | 78 | |--------|-------------| 79 | | `total_prs` | Total number of PRs | 80 | | `merged_prs` | Number of merged PRs | 81 | | `avg_time_to_merge` | Average time to merge (hours) | 82 | | `report_file` | Generated report file path | 83 | 84 | ## 📋 Usage Examples 85 | 86 | ### Weekly Report 87 | 88 | ```yaml 89 | on: 90 | schedule: 91 | - cron: '0 0 * * 0' # Every Sunday 92 | 93 | jobs: 94 | metrics: 95 | runs-on: ubuntu-latest 96 | permissions: 97 | contents: write 98 | pull-requests: read 99 | steps: 100 | - uses: actions/checkout@v4 101 | - uses: rkneela0912/pr-metrics-dashboard@v1 102 | with: 103 | github_token: ${{ secrets.GITHUB_TOKEN }} 104 | days: 7 105 | ``` 106 | 107 | ### Monthly Report with Custom Location 108 | 109 | ```yaml 110 | - uses: rkneela0912/pr-metrics-dashboard@v1 111 | with: 112 | github_token: ${{ secrets.GITHUB_TOKEN }} 113 | days: 30 114 | output_file: 'docs/metrics/monthly-report.md' 115 | ``` 116 | 117 | ### Quarterly Report 118 | 119 | ```yaml 120 | - uses: rkneela0912/pr-metrics-dashboard@v1 121 | with: 122 | github_token: ${{ secrets.GITHUB_TOKEN }} 123 | days: 90 124 | output_file: 'reports/Q1-2025.md' 125 | ``` 126 | 127 | ## 🎯 What You Get 128 | 129 | ### Metrics Tracked 130 | 131 | - Total PRs (open, merged, closed) 132 | - Merge rate percentage 133 | - Average time to merge 134 | - Median time to merge 135 | - Top 10 contributors 136 | - PR size distribution (XS/S/M/L/XL) 137 | - Activity by day of week 138 | 139 | ### Automated Insights 140 | 141 | The dashboard provides intelligent insights such as: 142 | - ✅ Fast merge times detected 143 | - ⚠️ Slow review cycles identified 144 | - ✅ High merge rate achievements 145 | - ⚠️ Large PR warnings 146 | - 📅 Weekend activity patterns 147 | 148 | ## 🔧 Deployment Steps 149 | 150 | 1. **Create workflow file** in `.github/workflows/` 151 | 2. **Configure schedule** (weekly, monthly, etc.) 152 | 3. **Set permissions** (contents: write, pull-requests: read) 153 | 4. **Add commit step** to save the report 154 | 5. **Test manually** using "Run workflow" button 155 | 156 | ## 📈 Best Practices 157 | 158 | - Run weekly for regular tracking 159 | - Commit reports to track trends over time 160 | - Share reports with the team 161 | - Use insights to improve processes 162 | - Adjust `days` parameter based on your sprint length 163 | 164 | ## 🐛 Troubleshooting 165 | 166 | **No PRs found?** 167 | - Check the `days` parameter covers a period with PR activity 168 | 169 | **Report not committed?** 170 | - Ensure `contents: write` permission is set 171 | - Check git config is set correctly in commit step 172 | 173 | ## 📚 Permissions 174 | 175 | ```yaml 176 | permissions: 177 | contents: write # To commit the report 178 | pull-requests: read # To read PR data 179 | ``` 180 | 181 | ## 📖 Learn More 182 | 183 | - [Complete Deployment Guide](https://github.com/rkneela0912/pr-metrics-dashboard#readme) 184 | - [Example Reports](https://github.com/rkneela0912/pr-metrics-dashboard/tree/main/examples) 185 | - [GitHub Actions Documentation](https://docs.github.com/en/actions) 186 | 187 | ## 🤝 Contributing 188 | 189 | Contributions welcome! Open an issue or submit a PR. 190 | 191 | ## 📄 License 192 | 193 | [MIT License](LICENSE) 194 | 195 | ## ⭐ Support 196 | 197 | Star this repo if you find it helpful! 198 | 199 | For issues: [Open an issue](https://github.com/rkneela0912/pr-metrics-dashboard/issues) 200 | 201 | --- 202 | 203 | **Made with ❤️ to help teams improve their development workflow** 204 | 205 | ## 💡 📊 Data-driven insights 206 | 207 | Make your workflow more efficient with automation! 208 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core'); 2 | const github = require('@actions/github'); 3 | const fs = require('fs'); 4 | 5 | async function run() { 6 | try { 7 | const token = core.getInput('github_token', { required: true }); 8 | const days = parseInt(core.getInput('days') || '30'); 9 | const outputFile = core.getInput('output_file') || 'PR_METRICS.md'; 10 | const includeCharts = core.getInput('include_charts') !== 'false'; 11 | 12 | const octokit = github.getOctokit(token); 13 | const { owner, repo } = github.context.repo; 14 | 15 | core.info(`📊 Generating PR metrics for the last ${days} days...`); 16 | 17 | const since = new Date(); 18 | since.setDate(since.getDate() - days); 19 | 20 | // Fetch all PRs (both open and closed) from the time period 21 | const allPRs = []; 22 | let page = 1; 23 | let hasMore = true; 24 | 25 | while (hasMore) { 26 | const { data: openPRs } = await octokit.rest.pulls.list({ 27 | owner, 28 | repo, 29 | state: 'open', 30 | per_page: 100, 31 | page, 32 | sort: 'created', 33 | direction: 'desc' 34 | }); 35 | 36 | const { data: closedPRs } = await octokit.rest.pulls.list({ 37 | owner, 38 | repo, 39 | state: 'closed', 40 | per_page: 100, 41 | page, 42 | sort: 'updated', 43 | direction: 'desc' 44 | }); 45 | 46 | const prs = [...openPRs, ...closedPRs].filter(pr => { 47 | const createdAt = new Date(pr.created_at); 48 | return createdAt >= since; 49 | }); 50 | 51 | allPRs.push(...prs); 52 | 53 | if (prs.length < 100) { 54 | hasMore = false; 55 | } else { 56 | page++; 57 | } 58 | } 59 | 60 | core.info(`Found ${allPRs.length} PRs in the last ${days} days`); 61 | 62 | // Calculate metrics 63 | const metrics = calculateMetrics(allPRs, since); 64 | 65 | // Generate report 66 | const report = generateReport(metrics, days, owner, repo, includeCharts); 67 | 68 | // Write to file 69 | fs.writeFileSync(outputFile, report); 70 | core.info(`✅ Metrics report written to ${outputFile}`); 71 | 72 | // Set outputs 73 | core.setOutput('total_prs', metrics.totalPRs.toString()); 74 | core.setOutput('merged_prs', metrics.mergedPRs.toString()); 75 | core.setOutput('avg_time_to_merge', metrics.avgTimeToMerge.toFixed(2)); 76 | core.setOutput('report_file', outputFile); 77 | 78 | // Create summary 79 | core.summary 80 | .addHeading('📊 PR Metrics Summary') 81 | .addTable([ 82 | [{data: 'Metric', header: true}, {data: 'Value', header: true}], 83 | ['Total PRs', metrics.totalPRs.toString()], 84 | ['Merged PRs', metrics.mergedPRs.toString()], 85 | ['Open PRs', metrics.openPRs.toString()], 86 | ['Closed (Not Merged)', metrics.closedNotMerged.toString()], 87 | ['Avg Time to Merge', `${metrics.avgTimeToMerge.toFixed(1)} hours`], 88 | ['Merge Rate', `${metrics.mergeRate.toFixed(1)}%`] 89 | ]) 90 | .write(); 91 | 92 | } catch (error) { 93 | core.setFailed(`Action failed: ${error.message}\n${error.stack}`); 94 | } 95 | } 96 | 97 | function calculateMetrics(prs, since) { 98 | const now = new Date(); 99 | 100 | const totalPRs = prs.length; 101 | const openPRs = prs.filter(pr => pr.state === 'open').length; 102 | const closedPRs = prs.filter(pr => pr.state === 'closed').length; 103 | const mergedPRs = prs.filter(pr => pr.merged_at).length; 104 | const closedNotMerged = closedPRs - mergedPRs; 105 | 106 | // Calculate time to merge for merged PRs 107 | const mergedPRsWithTime = prs 108 | .filter(pr => pr.merged_at) 109 | .map(pr => { 110 | const created = new Date(pr.created_at); 111 | const merged = new Date(pr.merged_at); 112 | const hours = (merged - created) / (1000 * 60 * 60); 113 | return { pr, hours }; 114 | }); 115 | 116 | const avgTimeToMerge = mergedPRsWithTime.length > 0 117 | ? mergedPRsWithTime.reduce((sum, item) => sum + item.hours, 0) / mergedPRsWithTime.length 118 | : 0; 119 | 120 | const medianTimeToMerge = mergedPRsWithTime.length > 0 121 | ? calculateMedian(mergedPRsWithTime.map(item => item.hours)) 122 | : 0; 123 | 124 | // Calculate merge rate 125 | const mergeRate = totalPRs > 0 ? (mergedPRs / totalPRs) * 100 : 0; 126 | 127 | // Top contributors 128 | const contributorMap = new Map(); 129 | prs.forEach(pr => { 130 | const author = pr.user.login; 131 | if (!contributorMap.has(author)) { 132 | contributorMap.set(author, { prs: 0, merged: 0 }); 133 | } 134 | const stats = contributorMap.get(author); 135 | stats.prs++; 136 | if (pr.merged_at) stats.merged++; 137 | }); 138 | 139 | const topContributors = Array.from(contributorMap.entries()) 140 | .map(([author, stats]) => ({ author, ...stats })) 141 | .sort((a, b) => b.prs - a.prs) 142 | .slice(0, 10); 143 | 144 | // PRs by size (lines changed) 145 | const prsBySizeRanges = { 146 | xs: 0, // 0-10 147 | s: 0, // 11-100 148 | m: 0, // 101-500 149 | l: 0, // 501-1000 150 | xl: 0 // 1000+ 151 | }; 152 | 153 | prs.forEach(pr => { 154 | const changes = (pr.additions || 0) + (pr.deletions || 0); 155 | if (changes <= 10) prsBySizeRanges.xs++; 156 | else if (changes <= 100) prsBySizeRanges.s++; 157 | else if (changes <= 500) prsBySizeRanges.m++; 158 | else if (changes <= 1000) prsBySizeRanges.l++; 159 | else prsBySizeRanges.xl++; 160 | }); 161 | 162 | // PRs by day of week 163 | const prsByDay = { Mon: 0, Tue: 0, Wed: 0, Thu: 0, Fri: 0, Sat: 0, Sun: 0 }; 164 | const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; 165 | prs.forEach(pr => { 166 | const day = dayNames[new Date(pr.created_at).getDay()]; 167 | prsByDay[day]++; 168 | }); 169 | 170 | return { 171 | totalPRs, 172 | openPRs, 173 | closedPRs, 174 | mergedPRs, 175 | closedNotMerged, 176 | avgTimeToMerge, 177 | medianTimeToMerge, 178 | mergeRate, 179 | topContributors, 180 | prsBySizeRanges, 181 | prsByDay, 182 | mergedPRsWithTime 183 | }; 184 | } 185 | 186 | function calculateMedian(numbers) { 187 | if (numbers.length === 0) return 0; 188 | const sorted = numbers.slice().sort((a, b) => a - b); 189 | const mid = Math.floor(sorted.length / 2); 190 | return sorted.length % 2 === 0 191 | ? (sorted[mid - 1] + sorted[mid]) / 2 192 | : sorted[mid]; 193 | } 194 | 195 | function generateReport(metrics, days, owner, repo, includeCharts) { 196 | const now = new Date(); 197 | 198 | let report = `# 📊 PR Metrics Dashboard 199 | 200 | **Repository:** ${owner}/${repo} 201 | **Period:** Last ${days} days 202 | **Generated:** ${now.toISOString().split('T')[0]} 203 | 204 | --- 205 | 206 | ## 📈 Overview 207 | 208 | | Metric | Value | 209 | |--------|-------| 210 | | **Total PRs** | ${metrics.totalPRs} | 211 | | **Merged PRs** | ${metrics.mergedPRs} ✅ | 212 | | **Open PRs** | ${metrics.openPRs} 🔄 | 213 | | **Closed (Not Merged)** | ${metrics.closedNotMerged} ❌ | 214 | | **Merge Rate** | ${metrics.mergeRate.toFixed(1)}% | 215 | | **Avg Time to Merge** | ${metrics.avgTimeToMerge.toFixed(1)} hours | 216 | | **Median Time to Merge** | ${metrics.medianTimeToMerge.toFixed(1)} hours | 217 | 218 | --- 219 | 220 | ## 👥 Top Contributors 221 | 222 | | Rank | Author | PRs | Merged | Merge Rate | 223 | |------|--------|-----|--------|------------| 224 | `; 225 | 226 | metrics.topContributors.forEach((contributor, index) => { 227 | const rate = contributor.prs > 0 ? (contributor.merged / contributor.prs * 100).toFixed(0) : 0; 228 | report += `| ${index + 1} | @${contributor.author} | ${contributor.prs} | ${contributor.merged} | ${rate}% |\n`; 229 | }); 230 | 231 | report += `\n--- 232 | 233 | ## 📏 PRs by Size 234 | 235 | | Size | Count | Percentage | 236 | |------|-------|------------| 237 | | **XS** (0-10 lines) | ${metrics.prsBySizeRanges.xs} | ${(metrics.prsBySizeRanges.xs / metrics.totalPRs * 100).toFixed(1)}% | 238 | | **S** (11-100 lines) | ${metrics.prsBySizeRanges.s} | ${(metrics.prsBySizeRanges.s / metrics.totalPRs * 100).toFixed(1)}% | 239 | | **M** (101-500 lines) | ${metrics.prsBySizeRanges.m} | ${(metrics.prsBySizeRanges.m / metrics.totalPRs * 100).toFixed(1)}% | 240 | | **L** (501-1000 lines) | ${metrics.prsBySizeRanges.l} | ${(metrics.prsBySizeRanges.l / metrics.totalPRs * 100).toFixed(1)}% | 241 | | **XL** (1000+ lines) | ${metrics.prsBySizeRanges.xl} | ${(metrics.prsBySizeRanges.xl / metrics.totalPRs * 100).toFixed(1)}% | 242 | 243 | --- 244 | 245 | ## 📅 PRs by Day of Week 246 | 247 | | Day | Count | Percentage | 248 | |-----|-------|------------| 249 | `; 250 | 251 | const dayOrder = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; 252 | dayOrder.forEach(day => { 253 | const count = metrics.prsByDay[day]; 254 | const pct = (count / metrics.totalPRs * 100).toFixed(1); 255 | report += `| ${day} | ${count} | ${pct}% |\n`; 256 | }); 257 | 258 | if (includeCharts) { 259 | report += `\n--- 260 | 261 | ## 📊 Visual Charts 262 | 263 | ### Merge Rate 264 | \`\`\` 265 | Merged: ${'█'.repeat(Math.round(metrics.mergeRate / 2))} ${metrics.mergeRate.toFixed(1)}% 266 | Not Merged: ${'█'.repeat(Math.round((100 - metrics.mergeRate) / 2))} ${(100 - metrics.mergeRate).toFixed(1)}% 267 | \`\`\` 268 | 269 | ### PR Size Distribution 270 | \`\`\` 271 | XS: ${'█'.repeat(Math.round(metrics.prsBySizeRanges.xs / metrics.totalPRs * 50))} ${metrics.prsBySizeRanges.xs} 272 | S: ${'█'.repeat(Math.round(metrics.prsBySizeRanges.s / metrics.totalPRs * 50))} ${metrics.prsBySizeRanges.s} 273 | M: ${'█'.repeat(Math.round(metrics.prsBySizeRanges.m / metrics.totalPRs * 50))} ${metrics.prsBySizeRanges.m} 274 | L: ${'█'.repeat(Math.round(metrics.prsBySizeRanges.l / metrics.totalPRs * 50))} ${metrics.prsBySizeRanges.l} 275 | XL: ${'█'.repeat(Math.round(metrics.prsBySizeRanges.xl / metrics.totalPRs * 50))} ${metrics.prsBySizeRanges.xl} 276 | \`\`\` 277 | 278 | ### Activity by Day 279 | \`\`\` 280 | Mon: ${'█'.repeat(Math.round(metrics.prsByDay.Mon / metrics.totalPRs * 50))} ${metrics.prsByDay.Mon} 281 | Tue: ${'█'.repeat(Math.round(metrics.prsByDay.Tue / metrics.totalPRs * 50))} ${metrics.prsByDay.Tue} 282 | Wed: ${'█'.repeat(Math.round(metrics.prsByDay.Wed / metrics.totalPRs * 50))} ${metrics.prsByDay.Wed} 283 | Thu: ${'█'.repeat(Math.round(metrics.prsByDay.Thu / metrics.totalPRs * 50))} ${metrics.prsByDay.Thu} 284 | Fri: ${'█'.repeat(Math.round(metrics.prsByDay.Fri / metrics.totalPRs * 50))} ${metrics.prsByDay.Fri} 285 | Sat: ${'█'.repeat(Math.round(metrics.prsByDay.Sat / metrics.totalPRs * 50))} ${metrics.prsByDay.Sat} 286 | Sun: ${'█'.repeat(Math.round(metrics.prsByDay.Sun / metrics.totalPRs * 50))} ${metrics.prsByDay.Sun} 287 | \`\`\` 288 | `; 289 | } 290 | 291 | report += `\n--- 292 | 293 | ## 💡 Insights 294 | 295 | `; 296 | 297 | // Generate insights 298 | if (metrics.avgTimeToMerge < 24) { 299 | report += `- ✅ **Fast merge times!** Average time to merge is under 24 hours.\n`; 300 | } else if (metrics.avgTimeToMerge > 72) { 301 | report += `- ⚠️ **Slow merge times.** Consider reviewing PR review processes.\n`; 302 | } 303 | 304 | if (metrics.mergeRate > 80) { 305 | report += `- ✅ **High merge rate!** ${metrics.mergeRate.toFixed(0)}% of PRs are being merged.\n`; 306 | } else if (metrics.mergeRate < 50) { 307 | report += `- ⚠️ **Low merge rate.** Many PRs are being closed without merging.\n`; 308 | } 309 | 310 | const smallPRs = metrics.prsBySizeRanges.xs + metrics.prsBySizeRanges.s; 311 | const smallPRPct = (smallPRs / metrics.totalPRs * 100); 312 | if (smallPRPct > 60) { 313 | report += `- ✅ **Good PR sizes!** ${smallPRPct.toFixed(0)}% of PRs are small (< 100 lines).\n`; 314 | } else if (smallPRPct < 30) { 315 | report += `- ⚠️ **Large PRs.** Consider breaking down PRs into smaller chunks.\n`; 316 | } 317 | 318 | const weekendPRs = metrics.prsByDay.Sat + metrics.prsByDay.Sun; 319 | const weekendPct = (weekendPRs / metrics.totalPRs * 100); 320 | if (weekendPct > 20) { 321 | report += `- 📅 **Weekend activity:** ${weekendPct.toFixed(0)}% of PRs created on weekends.\n`; 322 | } 323 | 324 | report += `\n--- 325 | 326 | *Generated by [PR Metrics Dashboard](https://github.com/rkneela0912/pr-metrics-dashboard)* 327 | `; 328 | 329 | return report; 330 | } 331 | 332 | run(); 333 | 334 | --------------------------------------------------------------------------------