Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
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
1 change: 1 addition & 0 deletions classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,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: 6 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class EnvVars:
hide_time_to_close (bool): If true, the time to close metric is hidden in the output
hide_time_to_first_response (bool): If true, the time to first response metric is hidden
in the output
hide_time_to_first_review (bool): If true, the time to first review metric is hidden in the output
hide_created_at (bool): If true, the created at timestamp is hidden in the output
hide_status (bool): If true, the status column is hidden in the output
ignore_users (List[str]): List of usernames to ignore when calculating metrics
Expand Down Expand Up @@ -79,6 +80,7 @@ def __init__(
hide_time_to_answer: bool,
hide_time_to_close: bool,
hide_time_to_first_response: bool,
hide_time_to_first_review: bool,
hide_created_at: bool,
hide_status: bool,
ignore_user: List[str],
Expand Down Expand Up @@ -114,6 +116,7 @@ def __init__(
self.hide_time_to_answer = hide_time_to_answer
self.hide_time_to_close = hide_time_to_close
self.hide_time_to_first_response = hide_time_to_first_response
self.hide_time_to_first_review = hide_time_to_first_review
self.hide_created_at = hide_created_at
self.hide_status = hide_status
self.enable_mentor_count = enable_mentor_count
Expand Down Expand Up @@ -148,6 +151,7 @@ def __repr__(self):
f"{self.hide_time_to_answer}, "
f"{self.hide_time_to_close}, "
f"{self.hide_time_to_first_response}, "
f"{self.hide_time_to_first_review}, "
f"{self.hide_created_at}, "
f"{self.hide_status}, "
f"{self.ignore_users}, "
Expand Down Expand Up @@ -269,6 +273,7 @@ def get_env_vars(test: bool = False) -> EnvVars:
hide_time_to_answer = get_bool_env_var("HIDE_TIME_TO_ANSWER", False)
hide_time_to_close = get_bool_env_var("HIDE_TIME_TO_CLOSE", False)
hide_time_to_first_response = get_bool_env_var("HIDE_TIME_TO_FIRST_RESPONSE", False)
hide_time_to_first_review = get_bool_env_var("HIDE_TIME_TO_FIRST_REVIEW", False)
hide_created_at = get_bool_env_var("HIDE_CREATED_AT", True)
hide_status = get_bool_env_var("HIDE_STATUS", True)
hide_pr_statistics = get_bool_env_var("HIDE_PR_STATISTICS", True)
Expand All @@ -293,6 +298,7 @@ def get_env_vars(test: bool = False) -> EnvVars:
hide_time_to_answer,
hide_time_to_close,
hide_time_to_first_response,
hide_time_to_first_review,
hide_created_at,
hide_status,
ignore_users_list,
Expand Down
17 changes: 16 additions & 1 deletion issue_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
get_stats_time_to_first_response,
measure_time_to_first_response,
)
from time_to_first_review import (
get_stats_time_to_first_review,
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 +163,13 @@ def get_per_issue_metrics(
issue_with_metrics.pr_comment_count = count_pr_comments(
issue, pull_request, ignore_users
)

if not env_vars.hide_time_to_first_review and 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 Expand Up @@ -305,6 +315,7 @@ def main(): # pragma: no cover
write_to_markdown(
issues_with_metrics=None,
average_time_to_first_response=None,
average_time_to_first_review=None,
average_time_to_close=None,
average_time_to_answer=None,
average_time_in_draft=None,
Expand Down Expand Up @@ -333,6 +344,7 @@ def main(): # pragma: no cover
write_to_markdown(
issues_with_metrics=None,
average_time_to_first_response=None,
average_time_to_first_review=None,
average_time_to_close=None,
average_time_to_answer=None,
average_time_in_draft=None,
Expand Down Expand Up @@ -365,6 +377,7 @@ def main(): # pragma: no cover
)

stats_time_to_first_response = get_stats_time_to_first_response(issues_with_metrics)
stats_time_to_first_review = get_stats_time_to_first_review(issues_with_metrics)
stats_time_to_close = None
if num_issues_closed > 0:
stats_time_to_close = get_stats_time_to_close(issues_with_metrics)
Expand All @@ -385,6 +398,7 @@ def main(): # pragma: no cover
write_to_json(
issues_with_metrics=issues_with_metrics,
stats_time_to_first_response=stats_time_to_first_response,
stats_time_to_first_review=stats_time_to_first_review,
stats_time_to_close=stats_time_to_close,
stats_time_to_answer=stats_time_to_answer,
stats_time_in_draft=stats_time_in_draft,
Expand All @@ -400,6 +414,7 @@ def main(): # pragma: no cover
write_to_markdown(
issues_with_metrics=issues_with_metrics,
average_time_to_first_response=stats_time_to_first_response,
average_time_to_first_review=stats_time_to_first_review,
average_time_to_close=stats_time_to_close,
average_time_to_answer=stats_time_to_answer,
average_time_in_draft=stats_time_in_draft,
Expand Down
15 changes: 15 additions & 0 deletions json_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
write_to_json(
issues_with_metrics: Union[List[IssueWithMetrics], None],
stats_time_to_first_response: Union[dict[str, timedelta], None],
stats_time_to_first_review: Union[dict[str, timedelta], None],
stats_time_to_close: Union[dict[str, timedelta], None],
stats_time_to_answer: Union[dict[str, timedelta], None],
stats_time_in_draft: Union[dict[str, timedelta], None],
Expand All @@ -29,6 +30,7 @@
def write_to_json(
issues_with_metrics: Union[List[IssueWithMetrics], None],
stats_time_to_first_response: Union[dict[str, timedelta], None],
stats_time_to_first_review: Union[dict[str, timedelta], None],
stats_time_to_close: Union[dict[str, timedelta], None],
stats_time_to_answer: Union[dict[str, timedelta], None],
stats_time_in_draft: Union[dict[str, timedelta], None],
Expand Down Expand Up @@ -104,6 +106,15 @@ def write_to_json(
med_time_to_first_response = stats_time_to_first_response["med"]
p90_time_to_first_response = stats_time_to_first_response["90p"]

# time to first review
average_time_to_first_review = None
med_time_to_first_review = None
p90_time_to_first_review = None
if stats_time_to_first_review is not None:
average_time_to_first_review = stats_time_to_first_review["avg"]
med_time_to_first_review = stats_time_to_first_review["med"]
p90_time_to_first_review = stats_time_to_first_review["90p"]

# time to close
average_time_to_close = None
med_time_to_close = None
Expand Down Expand Up @@ -155,16 +166,19 @@ def write_to_json(
# Create a dictionary with the metrics
metrics: dict[str, Any] = {
"average_time_to_first_response": str(average_time_to_first_response),
"average_time_to_first_review": str(average_time_to_first_review),
"average_time_to_close": str(average_time_to_close),
"average_time_to_answer": str(average_time_to_answer),
"average_time_in_draft": str(average_time_in_draft),
"average_time_in_labels": average_time_in_labels,
"median_time_to_first_response": str(med_time_to_first_response),
"median_time_to_first_review": str(med_time_to_first_review),
"median_time_to_close": str(med_time_to_close),
"median_time_to_answer": str(med_time_to_answer),
"median_time_in_draft": str(med_time_in_draft),
"median_time_in_labels": med_time_in_labels,
"90_percentile_time_to_first_response": str(p90_time_to_first_response),
"90_percentile_time_to_first_review": str(p90_time_to_first_review),
"90_percentile_time_to_close": str(p90_time_to_close),
"90_percentile_time_to_answer": str(p90_time_to_answer),
"90_percentile_time_in_draft": str(p90_time_in_draft),
Expand Down Expand Up @@ -193,6 +207,7 @@ def write_to_json(
"assignee": issue.assignee,
"assignees": issue.assignees,
"time_to_first_response": str(issue.time_to_first_response),
"time_to_first_review": str(issue.time_to_first_review),
"time_to_close": str(issue.time_to_close),
"time_to_answer": str(issue.time_to_answer),
"time_in_draft": str(issue.time_in_draft),
Expand Down
21 changes: 21 additions & 0 deletions markdown_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ def get_non_hidden_columns(labels) -> List[str]:
if not hide_time_to_first_response:
columns.append("Time to first response")

hide_time_to_first_review = env_vars.hide_time_to_first_review
if not hide_time_to_first_review:
columns.append("Time to first review")

hide_time_to_close = env_vars.hide_time_to_close
if not hide_time_to_close:
columns.append("Time to close")
Expand Down Expand Up @@ -129,6 +133,7 @@ def sort_issues(
valid_fields = {
"time_to_close",
"time_to_first_response",
"time_to_first_review",
"time_to_answer",
"time_in_draft",
"created_at",
Expand Down Expand Up @@ -200,6 +205,7 @@ def group_issues(
def write_to_markdown(
issues_with_metrics: Union[List[IssueWithMetrics], None],
average_time_to_first_response: Union[dict[str, timedelta], None],
average_time_to_first_review: Union[dict[str, timedelta], None],
average_time_to_close: Union[dict[str, timedelta], None],
average_time_to_answer: Union[dict[str, timedelta], None],
average_time_in_draft: Union[dict[str, timedelta], None],
Expand Down Expand Up @@ -268,6 +274,7 @@ def write_to_markdown(
write_overall_metrics_tables(
issues_with_metrics,
average_time_to_first_response,
average_time_to_first_review,
average_time_to_close,
average_time_to_answer,
average_time_in_draft,
Expand Down Expand Up @@ -345,6 +352,8 @@ def write_to_markdown(
)
if "Time to first response" in columns:
file.write(f" {issue.time_to_first_response} |")
if "Time to first review" in columns:
file.write(f" {issue.time_to_first_review} |")
if "Time to close" in columns:
file.write(f" {issue.time_to_close} |")
if "Time to answer" in columns:
Expand Down Expand Up @@ -374,6 +383,7 @@ def write_to_markdown(
def write_overall_metrics_tables(
issues_with_metrics,
stats_time_to_first_response,
stats_time_to_first_review,
stats_time_to_close,
stats_time_to_answer,
average_time_in_draft,
Expand All @@ -397,6 +407,7 @@ def write_overall_metrics_tables(
column in columns
for column in [
"Time to first response",
"Time to first review",
"Time to close",
"Time to answer",
"Time in draft",
Expand All @@ -417,6 +428,16 @@ def write_overall_metrics_tables(
)
else:
file.write("| Time to first response | None | None | None |\n")
if "Time to first review" in columns:
if stats_time_to_first_review is not None:
file.write(
f"| Time to first review "
f"| {stats_time_to_first_review['avg']} "
f"| {stats_time_to_first_review['med']} "
f"| {stats_time_to_first_review['90p']} |\n"
)
else:
file.write("| Time to first review | None | None | None |\n")
if "Time to close" in columns:
if stats_time_to_close is not None:
file.write(
Expand Down
2 changes: 2 additions & 0 deletions test_assignee_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def test_assignee_in_markdown_output(self):
try:
write_to_markdown(
issues_with_metrics=issues_with_metrics,
average_time_to_first_review=None,
average_time_to_first_response={
"avg": timedelta(hours=3),
"med": timedelta(hours=3),
Expand Down Expand Up @@ -132,6 +133,7 @@ def test_assignee_in_json_output(self):
try:
json_output = write_to_json(
issues_with_metrics=issues_with_metrics,
stats_time_to_first_review=None,
stats_time_to_first_response={
"avg": timedelta(hours=3),
"med": timedelta(hours=3),
Expand Down
5 changes: 3 additions & 2 deletions test_column_order_fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def test_status_and_created_at_columns_alignment(self):
write_to_markdown(
issues_with_metrics=issues_with_metrics,
average_time_to_first_response=None,
average_time_to_first_review=None,
average_time_to_close=None,
average_time_to_answer=None,
average_time_in_draft=None,
Expand All @@ -80,7 +81,7 @@ def test_status_and_created_at_columns_alignment(self):
# The table should have the columns in the correct order
# and the data should be properly aligned
expected_header = (
"| Title | URL | Assignee | Author | Time to first response | "
"| Title | URL | Assignee | Author | Time to first response | Time to first review | "
"Time to close | Time to answer | Created At | Status |"
)
self.assertIn(expected_header, content)
Expand All @@ -92,7 +93,7 @@ def test_status_and_created_at_columns_alignment(self):
"| Test Issue | https://github.com/user/repo/issues/1 | "
"[assignee1](https://github.com/assignee1) | "
"[testuser](https://github.com/testuser) | 1 day, 0:00:00 | "
"2 days, 0:00:00 | 3 days, 0:00:00 | 2023-01-01T00:00:00Z | open |"
"None | 2 days, 0:00:00 | 3 days, 0:00:00 | 2023-01-01T00:00:00Z | open |"
)
self.assertIn(expected_row, content)

Expand Down
4 changes: 4 additions & 0 deletions test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def test_get_env_vars_with_github_app(self):
hide_time_to_answer=False,
hide_time_to_close=False,
hide_time_to_first_response=False,
hide_time_to_first_review=False,
hide_created_at=True,
hide_status=True,
ignore_user=[],
Expand Down Expand Up @@ -187,6 +188,7 @@ def test_get_env_vars_with_token(self):
hide_time_to_answer=False,
hide_time_to_close=False,
hide_time_to_first_response=False,
hide_time_to_first_review=False,
hide_created_at=True,
hide_status=True,
ignore_user=[],
Expand Down Expand Up @@ -292,6 +294,7 @@ def test_get_env_vars_optional_values(self):
hide_time_to_answer=True,
hide_time_to_close=True,
hide_time_to_first_response=True,
hide_time_to_first_review=False,
hide_created_at=True,
hide_status=True,
ignore_user=[],
Expand Down Expand Up @@ -339,6 +342,7 @@ def test_get_env_vars_optionals_are_defaulted(self):
hide_time_to_answer=False,
hide_time_to_close=False,
hide_time_to_first_response=False,
hide_time_to_first_review=False,
hide_created_at=True,
hide_status=True,
ignore_user=[],
Expand Down
Loading
Loading