Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6e0c7ea
feat: add time_to_first_review metric for pull requests
meoyushi Mar 4, 2026
98baab9
Merge branch 'github-community-projects:main' into feat-time-to-first…
meoyushi Mar 6, 2026
346e6b5
fix: add tests, remove comment and lint fixed
meoyushi Mar 6, 2026
fe0b84e
fix: initialize first_review_time to None
meoyushi Mar 6, 2026
694d390
fix: resolve lint issues and formatting
meoyushi Mar 6, 2026
5c98ea6
fix: resolve remaining lint issues
meoyushi Mar 6, 2026
f8b44d7
fix: resolve isort formatting errors by removing blank lines
meoyushi Mar 6, 2026
627921c
feat: add time_to_first_review in json and markdown
meoyushi Mar 7, 2026
7317684
feat: add time to first review metric and update tests likewise
meoyushi Mar 10, 2026
49e361c
Merge pull request #1 from github-community-projects/main
meoyushi Mar 13, 2026
319ca22
fix: style and lint fix
meoyushi Mar 13, 2026
1a23450
Merge branch 'main' into feat-time-to-first-review
meoyushi Mar 13, 2026
6b25327
test: implement all requested coverage for time_to_first_review
meoyushi Mar 21, 2026
04649aa
test: implement all requested coverage for time_to_first_review
meoyushi Mar 21, 2026
d5dbdb0
test: implement full coverage and fix all linting/formatting
meoyushi Mar 21, 2026
4c11a67
Merge branch 'main' into feat-time-to-first-review
meoyushi Mar 21, 2026
1038350
doc: README.md edited for time_to_first_review metric
meoyushi Mar 25, 2026
676b5cf
doc: README.md edited for time_to_first_review metric
meoyushi Mar 25, 2026
be94607
doc: mdformat for README.md
meoyushi Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def __init__(
html_url,
author,
time_to_first_response=None,
# time_to_first_review=None,
Comment thread
jmeridth marked this conversation as resolved.
Outdated
time_to_close=None,
time_to_answer=None,
time_in_draft=None,
Expand All @@ -53,6 +54,7 @@ def __init__(
self.assignee = assignee
self.assignees = assignees or []
self.time_to_first_response = time_to_first_response
self.time_to_first_review = None
self.time_to_close = time_to_close
self.time_to_answer = time_to_answer
self.time_in_draft = time_in_draft
Expand Down
6 changes: 5 additions & 1 deletion issue_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
get_stats_time_to_first_response,
measure_time_to_first_response,
)
from time_to_first_review import measure_time_to_first_review
from time_to_merge import measure_time_to_merge
from time_to_ready_for_review import get_time_to_ready_for_review

Expand Down Expand Up @@ -159,7 +160,10 @@ def get_per_issue_metrics(
issue_with_metrics.pr_comment_count = count_pr_comments(
issue, pull_request, ignore_users
)

if pull_request:
issue_with_metrics.time_to_first_review = measure_time_to_first_review(
issue, pull_request, ready_for_review_at, ignore_users
)
if env_vars.hide_time_to_first_response is False:
issue_with_metrics.time_to_first_response = (
measure_time_to_first_response(
Expand Down
84 changes: 84 additions & 0 deletions time_to_first_review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from datetime import datetime, timedelta
from typing import List, Union

import github3
import numpy
from classes import IssueWithMetrics
from time_to_first_response import ignore_comment


def measure_time_to_first_review(
issue: Union[github3.issues.Issue, None],
pull_request: Union[github3.pulls.PullRequest, None],
ready_for_review_at: Union[datetime, None] = None,
ignore_users: Union[List[str], None] = None,
) -> Union[timedelta, None]:
'''Measures duration between pull request creation time and the timestamp when the first review is submitted'''

if not issue or not pull_request:
return None

if ignore_users is None:
ignore_users = []

'''first_review_time = None'''

try:
reviews = pull_request.reviews(number=50)
for review in reviews:
if ignore_comment(
issue.issue.user,
review.user,
ignore_users,
review.submitted_at,
ready_for_review_at,
):
continue

first_review_time = review.submitted_at
break

except TypeError:
return None

if not first_review_time:
return None

if ready_for_review_at:
pr_created_time = ready_for_review_at
else:
pr_created_time = datetime.fromisoformat(issue.created_at)

return first_review_time - pr_created_time

def get_stats_time_to_first_review(
issues: List[IssueWithMetrics],
) -> Union[dict[str, timedelta], None]:

review_times = []
none_count = 0
for issue in issues:
if issue.time_to_first_review:
review_times.append(issue.time_to_first_review.total_seconds())
else:
none_count += 1

if len(issues) - none_count <= 0:
return None

average_seconds_to_first_review = numpy.round(numpy.average(review_times))
med_seconds_to_first_review = numpy.round(numpy.median(review_times))
ninety_percentile_seconds_to_first_review = numpy.round(numpy.percentile(review_times, 90, axis=0))

stats = {
"avg": timedelta(seconds=average_seconds_to_first_review),
"med": timedelta(seconds=med_seconds_to_first_review),
"90p": timedelta(seconds=ninety_percentile_seconds_to_first_review),
}

# Print the average time to first review converting seconds to a readable time format
print(
f"Average time to first review: {timedelta(seconds=average_seconds_to_first_review)}"
)

return stats
Loading