tech-hub-skills 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/bin/cli.js +241 -0
- package/bin/copilot.js +182 -0
- package/bin/postinstall.js +42 -0
- package/package.json +46 -0
- package/tech_hub_skills/roles/ai-engineer/skills/01-prompt-engineering/README.md +252 -0
- package/tech_hub_skills/roles/ai-engineer/skills/02-rag-pipeline/README.md +448 -0
- package/tech_hub_skills/roles/ai-engineer/skills/03-agent-orchestration/README.md +599 -0
- package/tech_hub_skills/roles/ai-engineer/skills/04-llm-guardrails/README.md +735 -0
- package/tech_hub_skills/roles/ai-engineer/skills/05-vector-embeddings/README.md +711 -0
- package/tech_hub_skills/roles/ai-engineer/skills/06-llm-evaluation/README.md +777 -0
- package/tech_hub_skills/roles/azure/skills/01-infrastructure-fundamentals/README.md +264 -0
- package/tech_hub_skills/roles/azure/skills/02-data-factory/README.md +264 -0
- package/tech_hub_skills/roles/azure/skills/03-synapse-analytics/README.md +264 -0
- package/tech_hub_skills/roles/azure/skills/04-databricks/README.md +264 -0
- package/tech_hub_skills/roles/azure/skills/05-functions/README.md +264 -0
- package/tech_hub_skills/roles/azure/skills/06-kubernetes-service/README.md +264 -0
- package/tech_hub_skills/roles/azure/skills/07-openai-service/README.md +264 -0
- package/tech_hub_skills/roles/azure/skills/08-machine-learning/README.md +264 -0
- package/tech_hub_skills/roles/azure/skills/09-storage-adls/README.md +264 -0
- package/tech_hub_skills/roles/azure/skills/10-networking/README.md +264 -0
- package/tech_hub_skills/roles/azure/skills/11-sql-cosmos/README.md +264 -0
- package/tech_hub_skills/roles/azure/skills/12-event-hubs/README.md +264 -0
- package/tech_hub_skills/roles/code-review/skills/01-automated-code-review/README.md +394 -0
- package/tech_hub_skills/roles/code-review/skills/02-pr-review-workflow/README.md +427 -0
- package/tech_hub_skills/roles/code-review/skills/03-code-quality-gates/README.md +518 -0
- package/tech_hub_skills/roles/code-review/skills/04-reviewer-assignment/README.md +504 -0
- package/tech_hub_skills/roles/code-review/skills/05-review-analytics/README.md +540 -0
- package/tech_hub_skills/roles/data-engineer/skills/01-lakehouse-architecture/README.md +550 -0
- package/tech_hub_skills/roles/data-engineer/skills/02-etl-pipeline/README.md +580 -0
- package/tech_hub_skills/roles/data-engineer/skills/03-data-quality/README.md +579 -0
- package/tech_hub_skills/roles/data-engineer/skills/04-streaming-pipelines/README.md +608 -0
- package/tech_hub_skills/roles/data-engineer/skills/05-performance-optimization/README.md +547 -0
- package/tech_hub_skills/roles/data-governance/skills/01-data-catalog/README.md +112 -0
- package/tech_hub_skills/roles/data-governance/skills/02-data-lineage/README.md +129 -0
- package/tech_hub_skills/roles/data-governance/skills/03-data-quality-framework/README.md +182 -0
- package/tech_hub_skills/roles/data-governance/skills/04-access-control/README.md +39 -0
- package/tech_hub_skills/roles/data-governance/skills/05-master-data-management/README.md +40 -0
- package/tech_hub_skills/roles/data-governance/skills/06-compliance-privacy/README.md +46 -0
- package/tech_hub_skills/roles/data-scientist/skills/01-eda-automation/README.md +230 -0
- package/tech_hub_skills/roles/data-scientist/skills/02-statistical-modeling/README.md +264 -0
- package/tech_hub_skills/roles/data-scientist/skills/03-feature-engineering/README.md +264 -0
- package/tech_hub_skills/roles/data-scientist/skills/04-predictive-modeling/README.md +264 -0
- package/tech_hub_skills/roles/data-scientist/skills/05-customer-analytics/README.md +264 -0
- package/tech_hub_skills/roles/data-scientist/skills/06-campaign-analysis/README.md +264 -0
- package/tech_hub_skills/roles/data-scientist/skills/07-experimentation/README.md +264 -0
- package/tech_hub_skills/roles/data-scientist/skills/08-data-visualization/README.md +264 -0
- package/tech_hub_skills/roles/devops/skills/01-cicd-pipeline/README.md +264 -0
- package/tech_hub_skills/roles/devops/skills/02-container-orchestration/README.md +264 -0
- package/tech_hub_skills/roles/devops/skills/03-infrastructure-as-code/README.md +264 -0
- package/tech_hub_skills/roles/devops/skills/04-gitops/README.md +264 -0
- package/tech_hub_skills/roles/devops/skills/05-environment-management/README.md +264 -0
- package/tech_hub_skills/roles/devops/skills/06-automated-testing/README.md +264 -0
- package/tech_hub_skills/roles/devops/skills/07-release-management/README.md +264 -0
- package/tech_hub_skills/roles/devops/skills/08-monitoring-alerting/README.md +264 -0
- package/tech_hub_skills/roles/devops/skills/09-devsecops/README.md +265 -0
- package/tech_hub_skills/roles/finops/skills/01-cost-visibility/README.md +264 -0
- package/tech_hub_skills/roles/finops/skills/02-resource-tagging/README.md +264 -0
- package/tech_hub_skills/roles/finops/skills/03-budget-management/README.md +264 -0
- package/tech_hub_skills/roles/finops/skills/04-reserved-instances/README.md +264 -0
- package/tech_hub_skills/roles/finops/skills/05-spot-optimization/README.md +264 -0
- package/tech_hub_skills/roles/finops/skills/06-storage-tiering/README.md +264 -0
- package/tech_hub_skills/roles/finops/skills/07-compute-rightsizing/README.md +264 -0
- package/tech_hub_skills/roles/finops/skills/08-chargeback/README.md +264 -0
- package/tech_hub_skills/roles/ml-engineer/skills/01-mlops-pipeline/README.md +566 -0
- package/tech_hub_skills/roles/ml-engineer/skills/02-feature-engineering/README.md +655 -0
- package/tech_hub_skills/roles/ml-engineer/skills/03-model-training/README.md +704 -0
- package/tech_hub_skills/roles/ml-engineer/skills/04-model-serving/README.md +845 -0
- package/tech_hub_skills/roles/ml-engineer/skills/05-model-monitoring/README.md +874 -0
- package/tech_hub_skills/roles/mlops/skills/01-ml-pipeline-orchestration/README.md +264 -0
- package/tech_hub_skills/roles/mlops/skills/02-experiment-tracking/README.md +264 -0
- package/tech_hub_skills/roles/mlops/skills/03-model-registry/README.md +264 -0
- package/tech_hub_skills/roles/mlops/skills/04-feature-store/README.md +264 -0
- package/tech_hub_skills/roles/mlops/skills/05-model-deployment/README.md +264 -0
- package/tech_hub_skills/roles/mlops/skills/06-model-observability/README.md +264 -0
- package/tech_hub_skills/roles/mlops/skills/07-data-versioning/README.md +264 -0
- package/tech_hub_skills/roles/mlops/skills/08-ab-testing/README.md +264 -0
- package/tech_hub_skills/roles/mlops/skills/09-automated-retraining/README.md +264 -0
- package/tech_hub_skills/roles/platform-engineer/skills/01-internal-developer-platform/README.md +153 -0
- package/tech_hub_skills/roles/platform-engineer/skills/02-self-service-infrastructure/README.md +57 -0
- package/tech_hub_skills/roles/platform-engineer/skills/03-slo-sli-management/README.md +59 -0
- package/tech_hub_skills/roles/platform-engineer/skills/04-developer-experience/README.md +57 -0
- package/tech_hub_skills/roles/platform-engineer/skills/05-incident-management/README.md +73 -0
- package/tech_hub_skills/roles/platform-engineer/skills/06-capacity-management/README.md +59 -0
- package/tech_hub_skills/roles/product-designer/skills/01-requirements-discovery/README.md +407 -0
- package/tech_hub_skills/roles/product-designer/skills/02-user-research/README.md +382 -0
- package/tech_hub_skills/roles/product-designer/skills/03-brainstorming-ideation/README.md +437 -0
- package/tech_hub_skills/roles/product-designer/skills/04-ux-design/README.md +496 -0
- package/tech_hub_skills/roles/product-designer/skills/05-product-market-fit/README.md +376 -0
- package/tech_hub_skills/roles/product-designer/skills/06-stakeholder-management/README.md +412 -0
- package/tech_hub_skills/roles/security-architect/skills/01-pii-detection/README.md +319 -0
- package/tech_hub_skills/roles/security-architect/skills/02-threat-modeling/README.md +264 -0
- package/tech_hub_skills/roles/security-architect/skills/03-infrastructure-security/README.md +264 -0
- package/tech_hub_skills/roles/security-architect/skills/04-iam/README.md +264 -0
- package/tech_hub_skills/roles/security-architect/skills/05-application-security/README.md +264 -0
- package/tech_hub_skills/roles/security-architect/skills/06-secrets-management/README.md +264 -0
- package/tech_hub_skills/roles/security-architect/skills/07-security-monitoring/README.md +264 -0
- package/tech_hub_skills/roles/system-design/skills/01-architecture-patterns/README.md +337 -0
- package/tech_hub_skills/roles/system-design/skills/02-requirements-engineering/README.md +264 -0
- package/tech_hub_skills/roles/system-design/skills/03-scalability/README.md +264 -0
- package/tech_hub_skills/roles/system-design/skills/04-high-availability/README.md +264 -0
- package/tech_hub_skills/roles/system-design/skills/05-cost-optimization-design/README.md +264 -0
- package/tech_hub_skills/roles/system-design/skills/06-api-design/README.md +264 -0
- package/tech_hub_skills/roles/system-design/skills/07-observability-architecture/README.md +264 -0
- package/tech_hub_skills/roles/system-design/skills/08-process-automation/PROCESS_TEMPLATE.md +336 -0
- package/tech_hub_skills/roles/system-design/skills/08-process-automation/README.md +521 -0
- package/tech_hub_skills/skills/README.md +336 -0
- package/tech_hub_skills/skills/ai-engineer.md +104 -0
- package/tech_hub_skills/skills/azure.md +149 -0
- package/tech_hub_skills/skills/code-review.md +399 -0
- package/tech_hub_skills/skills/compliance-automation.md +747 -0
- package/tech_hub_skills/skills/data-engineer.md +113 -0
- package/tech_hub_skills/skills/data-governance.md +102 -0
- package/tech_hub_skills/skills/data-scientist.md +123 -0
- package/tech_hub_skills/skills/devops.md +160 -0
- package/tech_hub_skills/skills/docker.md +160 -0
- package/tech_hub_skills/skills/enterprise-dashboard.md +613 -0
- package/tech_hub_skills/skills/finops.md +184 -0
- package/tech_hub_skills/skills/ml-engineer.md +115 -0
- package/tech_hub_skills/skills/mlops.md +187 -0
- package/tech_hub_skills/skills/optimization-advisor.md +329 -0
- package/tech_hub_skills/skills/orchestrator.md +497 -0
- package/tech_hub_skills/skills/platform-engineer.md +102 -0
- package/tech_hub_skills/skills/process-automation.md +226 -0
- package/tech_hub_skills/skills/process-changelog.md +184 -0
- package/tech_hub_skills/skills/process-documentation.md +484 -0
- package/tech_hub_skills/skills/process-kanban.md +324 -0
- package/tech_hub_skills/skills/process-versioning.md +214 -0
- package/tech_hub_skills/skills/product-designer.md +104 -0
- package/tech_hub_skills/skills/project-starter.md +443 -0
- package/tech_hub_skills/skills/security-architect.md +135 -0
- package/tech_hub_skills/skills/system-design.md +126 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
# cr-05: Review Analytics
|
|
2
|
+
|
|
3
|
+
Metrics and insights for review process optimization and bottleneck detection.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Review analytics provides visibility into your code review process, helping identify bottlenecks, balance workloads, and continuously improve team velocity. Track SLOs, measure cycle time, and make data-driven decisions.
|
|
8
|
+
|
|
9
|
+
## Capabilities
|
|
10
|
+
|
|
11
|
+
### Time Metrics
|
|
12
|
+
- Time to first review
|
|
13
|
+
- Time to approval
|
|
14
|
+
- Total cycle time (open to merge)
|
|
15
|
+
- Time in each review state
|
|
16
|
+
|
|
17
|
+
### Load Metrics
|
|
18
|
+
- Reviews per developer
|
|
19
|
+
- Review comments given/received
|
|
20
|
+
- Approval rate
|
|
21
|
+
- Change request rate
|
|
22
|
+
|
|
23
|
+
### Quality Metrics
|
|
24
|
+
- Defect escape rate
|
|
25
|
+
- Review iteration count
|
|
26
|
+
- PR size distribution
|
|
27
|
+
- Security findings per review
|
|
28
|
+
|
|
29
|
+
### Process Insights
|
|
30
|
+
- Bottleneck identification
|
|
31
|
+
- Trend analysis
|
|
32
|
+
- SLO compliance
|
|
33
|
+
- Prediction analytics
|
|
34
|
+
|
|
35
|
+
## Implementation
|
|
36
|
+
|
|
37
|
+
### Review Analytics Dashboard
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
#!/usr/bin/env python3
|
|
41
|
+
"""Review analytics and metrics collection."""
|
|
42
|
+
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from datetime import datetime, timedelta
|
|
45
|
+
from typing import List, Dict, Optional, Tuple
|
|
46
|
+
from collections import defaultdict
|
|
47
|
+
import json
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class ReviewEvent:
|
|
51
|
+
"""A single review event."""
|
|
52
|
+
pr_number: int
|
|
53
|
+
event_type: str # opened, review_requested, reviewed, approved, merged
|
|
54
|
+
timestamp: datetime
|
|
55
|
+
actor: str
|
|
56
|
+
details: Dict = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class PRMetrics:
|
|
60
|
+
"""Metrics for a single PR."""
|
|
61
|
+
pr_number: int
|
|
62
|
+
author: str
|
|
63
|
+
opened_at: datetime
|
|
64
|
+
merged_at: Optional[datetime] = None
|
|
65
|
+
first_review_at: Optional[datetime] = None
|
|
66
|
+
approved_at: Optional[datetime] = None
|
|
67
|
+
lines_added: int = 0
|
|
68
|
+
lines_removed: int = 0
|
|
69
|
+
files_changed: int = 0
|
|
70
|
+
review_iterations: int = 0
|
|
71
|
+
reviewers: List[str] = field(default_factory=list)
|
|
72
|
+
comments_count: int = 0
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def time_to_first_review(self) -> Optional[float]:
|
|
76
|
+
"""Hours until first review."""
|
|
77
|
+
if self.first_review_at:
|
|
78
|
+
return (self.first_review_at - self.opened_at).total_seconds() / 3600
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def time_to_approval(self) -> Optional[float]:
|
|
83
|
+
"""Hours until approval."""
|
|
84
|
+
if self.approved_at:
|
|
85
|
+
return (self.approved_at - self.opened_at).total_seconds() / 3600
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def cycle_time(self) -> Optional[float]:
|
|
90
|
+
"""Hours from open to merge."""
|
|
91
|
+
if self.merged_at:
|
|
92
|
+
return (self.merged_at - self.opened_at).total_seconds() / 3600
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def pr_size(self) -> str:
|
|
97
|
+
"""Categorize PR size."""
|
|
98
|
+
total_lines = self.lines_added + self.lines_removed
|
|
99
|
+
if total_lines < 50:
|
|
100
|
+
return "XS"
|
|
101
|
+
elif total_lines < 200:
|
|
102
|
+
return "S"
|
|
103
|
+
elif total_lines < 500:
|
|
104
|
+
return "M"
|
|
105
|
+
elif total_lines < 1000:
|
|
106
|
+
return "L"
|
|
107
|
+
else:
|
|
108
|
+
return "XL"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ReviewAnalytics:
|
|
112
|
+
"""Analyze review metrics and generate insights."""
|
|
113
|
+
|
|
114
|
+
# SLO definitions
|
|
115
|
+
SLOS = {
|
|
116
|
+
"time_to_first_review": 4, # hours
|
|
117
|
+
"time_to_approval": 24, # hours
|
|
118
|
+
"cycle_time": 48, # hours
|
|
119
|
+
"max_review_iterations": 3,
|
|
120
|
+
"max_pr_size": 400, # lines
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
def __init__(self):
|
|
124
|
+
self.prs: Dict[int, PRMetrics] = {}
|
|
125
|
+
self.events: List[ReviewEvent] = []
|
|
126
|
+
|
|
127
|
+
def add_pr(self, pr: PRMetrics) -> None:
|
|
128
|
+
"""Add PR metrics."""
|
|
129
|
+
self.prs[pr.pr_number] = pr
|
|
130
|
+
|
|
131
|
+
def add_event(self, event: ReviewEvent) -> None:
|
|
132
|
+
"""Add review event."""
|
|
133
|
+
self.events.append(event)
|
|
134
|
+
|
|
135
|
+
def calculate_team_metrics(
|
|
136
|
+
self,
|
|
137
|
+
start_date: datetime,
|
|
138
|
+
end_date: datetime
|
|
139
|
+
) -> Dict:
|
|
140
|
+
"""Calculate team-wide metrics for date range."""
|
|
141
|
+
prs_in_range = [
|
|
142
|
+
pr for pr in self.prs.values()
|
|
143
|
+
if start_date <= pr.opened_at <= end_date
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
if not prs_in_range:
|
|
147
|
+
return {"error": "No PRs in date range"}
|
|
148
|
+
|
|
149
|
+
merged_prs = [pr for pr in prs_in_range if pr.merged_at]
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
"summary": {
|
|
153
|
+
"total_prs": len(prs_in_range),
|
|
154
|
+
"merged_prs": len(merged_prs),
|
|
155
|
+
"merge_rate": f"{len(merged_prs) / len(prs_in_range) * 100:.1f}%",
|
|
156
|
+
},
|
|
157
|
+
"time_metrics": {
|
|
158
|
+
"avg_time_to_first_review": self._avg([
|
|
159
|
+
pr.time_to_first_review for pr in merged_prs
|
|
160
|
+
if pr.time_to_first_review
|
|
161
|
+
]),
|
|
162
|
+
"avg_time_to_approval": self._avg([
|
|
163
|
+
pr.time_to_approval for pr in merged_prs
|
|
164
|
+
if pr.time_to_approval
|
|
165
|
+
]),
|
|
166
|
+
"avg_cycle_time": self._avg([
|
|
167
|
+
pr.cycle_time for pr in merged_prs
|
|
168
|
+
if pr.cycle_time
|
|
169
|
+
]),
|
|
170
|
+
"p90_cycle_time": self._percentile([
|
|
171
|
+
pr.cycle_time for pr in merged_prs
|
|
172
|
+
if pr.cycle_time
|
|
173
|
+
], 90),
|
|
174
|
+
},
|
|
175
|
+
"quality_metrics": {
|
|
176
|
+
"avg_review_iterations": self._avg([
|
|
177
|
+
pr.review_iterations for pr in merged_prs
|
|
178
|
+
]),
|
|
179
|
+
"avg_comments_per_pr": self._avg([
|
|
180
|
+
pr.comments_count for pr in merged_prs
|
|
181
|
+
]),
|
|
182
|
+
},
|
|
183
|
+
"size_distribution": self._size_distribution(prs_in_range),
|
|
184
|
+
"slo_compliance": self._calculate_slo_compliance(merged_prs),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
def calculate_reviewer_metrics(self) -> Dict[str, Dict]:
|
|
188
|
+
"""Calculate per-reviewer metrics."""
|
|
189
|
+
reviewer_stats = defaultdict(lambda: {
|
|
190
|
+
"reviews_given": 0,
|
|
191
|
+
"comments_given": 0,
|
|
192
|
+
"approvals": 0,
|
|
193
|
+
"change_requests": 0,
|
|
194
|
+
"avg_response_time": [],
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
for event in self.events:
|
|
198
|
+
if event.event_type == "reviewed":
|
|
199
|
+
reviewer = event.actor
|
|
200
|
+
reviewer_stats[reviewer]["reviews_given"] += 1
|
|
201
|
+
|
|
202
|
+
state = event.details.get("state", "")
|
|
203
|
+
if state == "APPROVED":
|
|
204
|
+
reviewer_stats[reviewer]["approvals"] += 1
|
|
205
|
+
elif state == "CHANGES_REQUESTED":
|
|
206
|
+
reviewer_stats[reviewer]["change_requests"] += 1
|
|
207
|
+
|
|
208
|
+
# Convert to final format
|
|
209
|
+
return {
|
|
210
|
+
reviewer: {
|
|
211
|
+
"reviews_given": stats["reviews_given"],
|
|
212
|
+
"approval_rate": (
|
|
213
|
+
f"{stats['approvals'] / stats['reviews_given'] * 100:.1f}%"
|
|
214
|
+
if stats["reviews_given"] > 0 else "N/A"
|
|
215
|
+
),
|
|
216
|
+
"change_request_rate": (
|
|
217
|
+
f"{stats['change_requests'] / stats['reviews_given'] * 100:.1f}%"
|
|
218
|
+
if stats["reviews_given"] > 0 else "N/A"
|
|
219
|
+
),
|
|
220
|
+
}
|
|
221
|
+
for reviewer, stats in reviewer_stats.items()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
def identify_bottlenecks(self) -> List[Dict]:
|
|
225
|
+
"""Identify review process bottlenecks."""
|
|
226
|
+
bottlenecks = []
|
|
227
|
+
|
|
228
|
+
# Check for slow first reviews
|
|
229
|
+
slow_first_review = [
|
|
230
|
+
pr for pr in self.prs.values()
|
|
231
|
+
if pr.time_to_first_review and
|
|
232
|
+
pr.time_to_first_review > self.SLOS["time_to_first_review"]
|
|
233
|
+
]
|
|
234
|
+
if len(slow_first_review) > len(self.prs) * 0.2:
|
|
235
|
+
bottlenecks.append({
|
|
236
|
+
"type": "slow_first_review",
|
|
237
|
+
"severity": "high",
|
|
238
|
+
"message": f"{len(slow_first_review)} PRs ({len(slow_first_review)/len(self.prs)*100:.0f}%) "
|
|
239
|
+
f"exceed first review SLO of {self.SLOS['time_to_first_review']}h",
|
|
240
|
+
"recommendation": "Add more reviewers or enable auto-assignment"
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
# Check for too many iterations
|
|
244
|
+
high_iteration_prs = [
|
|
245
|
+
pr for pr in self.prs.values()
|
|
246
|
+
if pr.review_iterations > self.SLOS["max_review_iterations"]
|
|
247
|
+
]
|
|
248
|
+
if high_iteration_prs:
|
|
249
|
+
bottlenecks.append({
|
|
250
|
+
"type": "high_iterations",
|
|
251
|
+
"severity": "medium",
|
|
252
|
+
"message": f"{len(high_iteration_prs)} PRs required >{self.SLOS['max_review_iterations']} review iterations",
|
|
253
|
+
"recommendation": "Improve PR guidelines and automated checks"
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
# Check for oversized PRs
|
|
257
|
+
large_prs = [
|
|
258
|
+
pr for pr in self.prs.values()
|
|
259
|
+
if pr.lines_added + pr.lines_removed > self.SLOS["max_pr_size"]
|
|
260
|
+
]
|
|
261
|
+
if len(large_prs) > len(self.prs) * 0.3:
|
|
262
|
+
bottlenecks.append({
|
|
263
|
+
"type": "large_prs",
|
|
264
|
+
"severity": "medium",
|
|
265
|
+
"message": f"{len(large_prs)} PRs exceed {self.SLOS['max_pr_size']} lines",
|
|
266
|
+
"recommendation": "Encourage smaller, focused PRs"
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
# Check reviewer load imbalance
|
|
270
|
+
reviewer_loads = self.calculate_reviewer_metrics()
|
|
271
|
+
if reviewer_loads:
|
|
272
|
+
loads = [r["reviews_given"] for r in reviewer_loads.values()]
|
|
273
|
+
if max(loads) > 3 * min(loads) if min(loads) > 0 else False:
|
|
274
|
+
bottlenecks.append({
|
|
275
|
+
"type": "unbalanced_load",
|
|
276
|
+
"severity": "medium",
|
|
277
|
+
"message": "Review load is unbalanced across team",
|
|
278
|
+
"recommendation": "Enable load-balanced reviewer assignment"
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
return bottlenecks
|
|
282
|
+
|
|
283
|
+
def _calculate_slo_compliance(self, prs: List[PRMetrics]) -> Dict:
|
|
284
|
+
"""Calculate SLO compliance rates."""
|
|
285
|
+
if not prs:
|
|
286
|
+
return {}
|
|
287
|
+
|
|
288
|
+
first_review_compliant = sum(
|
|
289
|
+
1 for pr in prs
|
|
290
|
+
if pr.time_to_first_review and
|
|
291
|
+
pr.time_to_first_review <= self.SLOS["time_to_first_review"]
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
approval_compliant = sum(
|
|
295
|
+
1 for pr in prs
|
|
296
|
+
if pr.time_to_approval and
|
|
297
|
+
pr.time_to_approval <= self.SLOS["time_to_approval"]
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
cycle_compliant = sum(
|
|
301
|
+
1 for pr in prs
|
|
302
|
+
if pr.cycle_time and
|
|
303
|
+
pr.cycle_time <= self.SLOS["cycle_time"]
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
"first_review_slo": f"{first_review_compliant / len(prs) * 100:.1f}%",
|
|
308
|
+
"approval_slo": f"{approval_compliant / len(prs) * 100:.1f}%",
|
|
309
|
+
"cycle_time_slo": f"{cycle_compliant / len(prs) * 100:.1f}%",
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
def _size_distribution(self, prs: List[PRMetrics]) -> Dict[str, int]:
|
|
313
|
+
"""Calculate PR size distribution."""
|
|
314
|
+
dist = defaultdict(int)
|
|
315
|
+
for pr in prs:
|
|
316
|
+
dist[pr.pr_size] += 1
|
|
317
|
+
return dict(dist)
|
|
318
|
+
|
|
319
|
+
def _avg(self, values: List[float]) -> Optional[float]:
|
|
320
|
+
"""Calculate average, handling empty lists."""
|
|
321
|
+
if not values:
|
|
322
|
+
return None
|
|
323
|
+
return round(sum(values) / len(values), 2)
|
|
324
|
+
|
|
325
|
+
def _percentile(self, values: List[float], p: int) -> Optional[float]:
|
|
326
|
+
"""Calculate percentile."""
|
|
327
|
+
if not values:
|
|
328
|
+
return None
|
|
329
|
+
sorted_values = sorted(values)
|
|
330
|
+
idx = int(len(sorted_values) * p / 100)
|
|
331
|
+
return round(sorted_values[min(idx, len(sorted_values) - 1)], 2)
|
|
332
|
+
|
|
333
|
+
def generate_report(self, start_date: datetime, end_date: datetime) -> str:
|
|
334
|
+
"""Generate markdown analytics report."""
|
|
335
|
+
metrics = self.calculate_team_metrics(start_date, end_date)
|
|
336
|
+
bottlenecks = self.identify_bottlenecks()
|
|
337
|
+
|
|
338
|
+
report = f"""# Code Review Analytics Report
|
|
339
|
+
|
|
340
|
+
**Period**: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}
|
|
341
|
+
|
|
342
|
+
## Summary
|
|
343
|
+
- Total PRs: {metrics['summary']['total_prs']}
|
|
344
|
+
- Merged PRs: {metrics['summary']['merged_prs']}
|
|
345
|
+
- Merge Rate: {metrics['summary']['merge_rate']}
|
|
346
|
+
|
|
347
|
+
## Time Metrics
|
|
348
|
+
| Metric | Value | SLO | Status |
|
|
349
|
+
|--------|-------|-----|--------|
|
|
350
|
+
| Avg Time to First Review | {metrics['time_metrics']['avg_time_to_first_review']}h | {self.SLOS['time_to_first_review']}h | {'✅' if metrics['time_metrics']['avg_time_to_first_review'] and metrics['time_metrics']['avg_time_to_first_review'] <= self.SLOS['time_to_first_review'] else '❌'} |
|
|
351
|
+
| Avg Time to Approval | {metrics['time_metrics']['avg_time_to_approval']}h | {self.SLOS['time_to_approval']}h | {'✅' if metrics['time_metrics']['avg_time_to_approval'] and metrics['time_metrics']['avg_time_to_approval'] <= self.SLOS['time_to_approval'] else '❌'} |
|
|
352
|
+
| Avg Cycle Time | {metrics['time_metrics']['avg_cycle_time']}h | {self.SLOS['cycle_time']}h | {'✅' if metrics['time_metrics']['avg_cycle_time'] and metrics['time_metrics']['avg_cycle_time'] <= self.SLOS['cycle_time'] else '❌'} |
|
|
353
|
+
| P90 Cycle Time | {metrics['time_metrics']['p90_cycle_time']}h | - | - |
|
|
354
|
+
|
|
355
|
+
## SLO Compliance
|
|
356
|
+
{chr(10).join(f"- {k}: {v}" for k, v in metrics['slo_compliance'].items())}
|
|
357
|
+
|
|
358
|
+
## PR Size Distribution
|
|
359
|
+
{chr(10).join(f"- {size}: {count} PRs" for size, count in sorted(metrics['size_distribution'].items()))}
|
|
360
|
+
|
|
361
|
+
## Identified Bottlenecks
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
if bottlenecks:
|
|
365
|
+
for b in bottlenecks:
|
|
366
|
+
report += f"""
|
|
367
|
+
### {b['type'].replace('_', ' ').title()}
|
|
368
|
+
- **Severity**: {b['severity']}
|
|
369
|
+
- **Issue**: {b['message']}
|
|
370
|
+
- **Recommendation**: {b['recommendation']}
|
|
371
|
+
"""
|
|
372
|
+
else:
|
|
373
|
+
report += "\nNo significant bottlenecks identified.\n"
|
|
374
|
+
|
|
375
|
+
return report
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# GitHub Action for weekly metrics
|
|
379
|
+
WEEKLY_METRICS_ACTION = """
|
|
380
|
+
name: Weekly Review Metrics
|
|
381
|
+
on:
|
|
382
|
+
schedule:
|
|
383
|
+
- cron: '0 9 * * 1' # Monday 9 AM
|
|
384
|
+
workflow_dispatch:
|
|
385
|
+
|
|
386
|
+
jobs:
|
|
387
|
+
metrics:
|
|
388
|
+
runs-on: ubuntu-latest
|
|
389
|
+
steps:
|
|
390
|
+
- uses: actions/checkout@v4
|
|
391
|
+
|
|
392
|
+
- name: Collect PR metrics
|
|
393
|
+
uses: actions/github-script@v7
|
|
394
|
+
id: metrics
|
|
395
|
+
with:
|
|
396
|
+
script: |
|
|
397
|
+
const oneWeekAgo = new Date();
|
|
398
|
+
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
399
|
+
|
|
400
|
+
const { data: prs } = await github.rest.pulls.list({
|
|
401
|
+
owner: context.repo.owner,
|
|
402
|
+
repo: context.repo.repo,
|
|
403
|
+
state: 'all',
|
|
404
|
+
sort: 'created',
|
|
405
|
+
direction: 'desc',
|
|
406
|
+
per_page: 100
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const weeklyPRs = prs.filter(pr =>
|
|
410
|
+
new Date(pr.created_at) >= oneWeekAgo
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const metrics = {
|
|
414
|
+
total: weeklyPRs.length,
|
|
415
|
+
merged: weeklyPRs.filter(pr => pr.merged_at).length,
|
|
416
|
+
open: weeklyPRs.filter(pr => pr.state === 'open').length
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
return metrics;
|
|
420
|
+
|
|
421
|
+
- name: Post to Slack
|
|
422
|
+
uses: slackapi/slack-github-action@v1
|
|
423
|
+
with:
|
|
424
|
+
payload: |
|
|
425
|
+
{
|
|
426
|
+
"text": "Weekly Review Metrics",
|
|
427
|
+
"blocks": [
|
|
428
|
+
{
|
|
429
|
+
"type": "section",
|
|
430
|
+
"text": {
|
|
431
|
+
"type": "mrkdwn",
|
|
432
|
+
"text": "*Weekly Code Review Metrics*\\n• Total PRs: ${{ fromJSON(steps.metrics.outputs.result).total }}\\n• Merged: ${{ fromJSON(steps.metrics.outputs.result).merged }}\\n• Open: ${{ fromJSON(steps.metrics.outputs.result).open }}"
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
]
|
|
436
|
+
}
|
|
437
|
+
env:
|
|
438
|
+
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
|
|
439
|
+
"""
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Grafana Dashboard Configuration
|
|
443
|
+
|
|
444
|
+
```json
|
|
445
|
+
{
|
|
446
|
+
"dashboard": {
|
|
447
|
+
"title": "Code Review Analytics",
|
|
448
|
+
"panels": [
|
|
449
|
+
{
|
|
450
|
+
"title": "Time to First Review (hours)",
|
|
451
|
+
"type": "stat",
|
|
452
|
+
"targets": [
|
|
453
|
+
{
|
|
454
|
+
"expr": "avg(github_pr_time_to_first_review_hours)",
|
|
455
|
+
"legendFormat": "Avg"
|
|
456
|
+
}
|
|
457
|
+
],
|
|
458
|
+
"thresholds": {
|
|
459
|
+
"steps": [
|
|
460
|
+
{"color": "green", "value": null},
|
|
461
|
+
{"color": "yellow", "value": 4},
|
|
462
|
+
{"color": "red", "value": 8}
|
|
463
|
+
]
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
"title": "Cycle Time Trend",
|
|
468
|
+
"type": "timeseries",
|
|
469
|
+
"targets": [
|
|
470
|
+
{
|
|
471
|
+
"expr": "avg(github_pr_cycle_time_hours) by (week)",
|
|
472
|
+
"legendFormat": "Cycle Time"
|
|
473
|
+
}
|
|
474
|
+
]
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
"title": "Reviewer Load",
|
|
478
|
+
"type": "barchart",
|
|
479
|
+
"targets": [
|
|
480
|
+
{
|
|
481
|
+
"expr": "sum(github_reviews_given) by (reviewer)",
|
|
482
|
+
"legendFormat": "{{reviewer}}"
|
|
483
|
+
}
|
|
484
|
+
]
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
"title": "PR Size Distribution",
|
|
488
|
+
"type": "piechart",
|
|
489
|
+
"targets": [
|
|
490
|
+
{
|
|
491
|
+
"expr": "count(github_pr_lines_changed) by (size_category)"
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
"title": "SLO Compliance",
|
|
497
|
+
"type": "gauge",
|
|
498
|
+
"targets": [
|
|
499
|
+
{
|
|
500
|
+
"expr": "github_review_slo_compliance_percent"
|
|
501
|
+
}
|
|
502
|
+
],
|
|
503
|
+
"thresholds": {
|
|
504
|
+
"steps": [
|
|
505
|
+
{"color": "red", "value": null},
|
|
506
|
+
{"color": "yellow", "value": 80},
|
|
507
|
+
{"color": "green", "value": 95}
|
|
508
|
+
]
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
]
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## Key Metrics Reference
|
|
517
|
+
|
|
518
|
+
| Metric | Formula | Target | Why It Matters |
|
|
519
|
+
|--------|---------|--------|----------------|
|
|
520
|
+
| Time to First Review | first_review_at - opened_at | < 4 hours | Fast feedback enables iteration |
|
|
521
|
+
| Time to Approval | approved_at - opened_at | < 24 hours | Prevents context switching |
|
|
522
|
+
| Cycle Time | merged_at - opened_at | < 48 hours | Overall delivery speed |
|
|
523
|
+
| Review Iterations | count(change_requests) | < 3 | Quality of initial submission |
|
|
524
|
+
| Review Load | reviews_per_person / team_avg | 0.8 - 1.2 | Balanced workload |
|
|
525
|
+
| Defect Escape Rate | bugs_in_reviewed_code / total_bugs | < 1% | Review effectiveness |
|
|
526
|
+
|
|
527
|
+
## Connections
|
|
528
|
+
|
|
529
|
+
- **Inputs from**: All review activities (cr-01 to cr-04)
|
|
530
|
+
- **Outputs to**: Dashboards, reports, alerts
|
|
531
|
+
- **Integrates with**: Platform (pe-05 SLOs), FinOps (cost of delays)
|
|
532
|
+
|
|
533
|
+
## Best Practices
|
|
534
|
+
|
|
535
|
+
1. Track metrics weekly, analyze monthly, set goals quarterly
|
|
536
|
+
2. Focus on trends, not individual data points
|
|
537
|
+
3. Share metrics transparently with team
|
|
538
|
+
4. Use data to improve process, not blame individuals
|
|
539
|
+
5. Set realistic SLOs based on baseline, then improve
|
|
540
|
+
6. Correlate review metrics with production quality
|