forked from github/issue-metrics
-
Notifications
You must be signed in to change notification settings - Fork 0
/
time_to_first_response.py
177 lines (148 loc) · 6.21 KB
/
time_to_first_response.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""A module for measuring the time it takes to get the first response to a GitHub issue.
This module provides functions for measuring the time it takes to get the first response
to a GitHub issue, as well as calculating the average time to first response for a list
of issues.
Functions:
measure_time_to_first_response(
issue: Union[github3.issues.Issue, None],
discussion: Union[dict, None]
pull_request: Union[github3.pulls.PullRequest, None],
) -> Union[timedelta, None]:
Measure the time to first response for a single issue or a discussion.
get_stats_time_to_first_response(
issues: List[IssueWithMetrics]
) -> Union[timedelta, None]:
Calculate stats describing time to first response for a list of issues with metrics.
"""
from datetime import datetime, timedelta
from typing import List, Union
import github3
import numpy
from classes import IssueWithMetrics
def measure_time_to_first_response(
issue: Union[github3.issues.Issue, None], # type: ignore
discussion: Union[dict, None],
pull_request: Union[github3.pulls.PullRequest, None] = None,
ready_for_review_at: Union[datetime, None] = None,
ignore_users: List[str] = None,
) -> Union[timedelta, None]:
"""Measure the time to first response for a single issue, pull request, or a discussion.
Args:
issue (Union[github3.issues.Issue, None]): A GitHub issue.
discussion (Union[dict, None]): A GitHub discussion.
pull_request (Union[github3.pulls.PullRequest, None]): A GitHub pull request.
ignore_users (List[str]): A list of GitHub usernames to ignore.
Returns:
Union[timedelta, None]: The time to first response for the issue/discussion.
"""
first_review_comment_time = None
first_comment_time = None
earliest_response = None
issue_time = None
if ignore_users is None:
ignore_users = []
# Get the first comment time
if issue:
comments = issue.issue.comments(
number=20, sort="created", direction="asc"
) # type: ignore
for comment in comments:
if ignore_comment(
issue.issue.user,
comment.user,
ignore_users,
comment.created_at,
ready_for_review_at,
):
continue
first_comment_time = comment.created_at
break
# Check if the issue is actually a pull request
# so we may also get the first review comment time
if pull_request:
review_comments = pull_request.reviews(number=50) # type: ignore
for review_comment in review_comments:
if ignore_comment(
issue.issue.user,
review_comment.user,
ignore_users,
review_comment.submitted_at,
ready_for_review_at,
):
continue
first_review_comment_time = review_comment.submitted_at
break
# Figure out the earliest response timestamp
if first_comment_time and first_review_comment_time:
earliest_response = min(first_comment_time, first_review_comment_time)
elif first_comment_time:
earliest_response = first_comment_time
elif first_review_comment_time:
earliest_response = first_review_comment_time
else:
return None
# Get the created_at time for the issue so we can calculate the time to first response
if ready_for_review_at:
issue_time = ready_for_review_at
else:
issue_time = datetime.fromisoformat(issue.created_at) # type: ignore
if discussion and len(discussion["comments"]["nodes"]) > 0:
earliest_response = datetime.fromisoformat(
discussion["comments"]["nodes"][0]["createdAt"]
)
issue_time = datetime.fromisoformat(discussion["createdAt"])
# Calculate the time between the issue and the first comment
if earliest_response and issue_time:
return earliest_response - issue_time
return None
def ignore_comment(
issue_user: github3.users.User,
comment_user: github3.users.User,
ignore_users: List[str],
comment_created_at: datetime,
ready_for_review_at: Union[datetime, None],
) -> bool:
"""Check if a comment should be ignored."""
return (
# ignore comments by IGNORE_USERS
comment_user.login in ignore_users
# ignore comments by bots
or comment_user.type == "Bot"
# ignore comments by the issue creator
or comment_user.login == issue_user.login
# ignore comments created before the issue was ready for review
or (ready_for_review_at and comment_created_at < ready_for_review_at)
)
def get_stats_time_to_first_response(
issues: List[IssueWithMetrics],
) -> Union[timedelta, None]:
"""Calculate the stats describing time to first response for a list of issues.
Args:
issues (List[IssueWithMetrics]): A list of GitHub issues with metrics attached.
Returns:
Union[Dict{String: datetime.timedelta}, None]: The stats describing time to first response for the issues in seconds.
"""
response_times = []
none_count = 0
for issue in issues:
if issue.time_to_first_response:
response_times.append(issue.time_to_first_response.total_seconds())
else:
none_count += 1
if len(issues) - none_count <= 0:
return None
average_seconds_to_first_response = numpy.round(numpy.average(response_times))
med_seconds_to_first_response = numpy.round(numpy.median(response_times))
ninety_percentile_seconds_to_first_response = numpy.round(
numpy.percentile(response_times, 90, axis=0)
)
stats = {
"avg": timedelta(seconds=average_seconds_to_first_response),
"med": timedelta(seconds=med_seconds_to_first_response),
"90p": timedelta(seconds=ninety_percentile_seconds_to_first_response),
}
# Print the average time to first response converting seconds to a readable time format
print(
f"Average time to first response: {timedelta(seconds=average_seconds_to_first_response)}"
)
return stats