Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Filter Out CI User Responses from Time to First Response #78

Merged
merged 10 commits into from
Aug 1, 2023
1 change: 1 addition & 0 deletions .env-example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
GH_TOKEN = " "
SEARCH_QUERY = "repo:owner/repo is:open is:issue"
LABELS_TO_MEASURE = "waiting-for-review,waiting-for-manager"
IGNORE_USERS = "user1,user2"
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Below are the allowed configuration options:
| `HIDE_TIME_TO_CLOSE` | False | | If set to any value, the time to close will not be displayed in the generated markdown file. |
| `HIDE_TIME_TO_ANSWER` | False | | If set to any value, the time to answer a discussion will not be displayed in the generated markdown file. |
| `HIDE_LABEL_METRICS` | False | | If set to any value, the time in label metrics will not be displayed in the generated markdown file. |
| `IGNORE_USERS` | False | | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`) |

### Example workflows

Expand Down Expand Up @@ -425,4 +426,4 @@ jobs:

## License

[MIT](LICENSE)
[MIT](LICENSE)
21 changes: 16 additions & 5 deletions issue_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
Searches for issues in a GitHub repository that match the given search query.
auth_to_github() -> github3.GitHub: Connect to GitHub API with token authentication.
get_per_issue_metrics(issues: Union[List[dict], List[github3.issues.Issue]],
discussions: bool = False) -> tuple[List, int, int]:
discussions: bool = False), labels: Union[List[str], None] = None, ignore_users: List[str] = [] -> tuple[List, int, int]:
Calculate the metrics for each issue in a list of GitHub issues.
get_owner(search_query: str) -> Union[str, None]]:
Get the owner from the search query.
Expand Down Expand Up @@ -41,13 +41,14 @@
)


def get_env_vars() -> tuple[str, str]:
def get_env_vars() -> tuple[str, str, List[str]]:
"""
Get the environment variables for use in the script.

Returns:
str: the search query used to filter issues, prs, and discussions
str: the github token used to authenticate to github.com
List[str]: a list of users to ignore when calculating metrics
"""
search_query = os.getenv("SEARCH_QUERY")
if not search_query:
Expand All @@ -57,7 +58,13 @@ def get_env_vars() -> tuple[str, str]:
if not token:
raise ValueError("GITHUB_TOKEN environment variable not set")

return search_query, token
ignore_users = os.getenv("IGNORE_USERS")
if ignore_users:
ignore_users = ignore_users.split(",")
else:
ignore_users = []

return search_query, token, ignore_users


def search_issues(
Expand Down Expand Up @@ -125,6 +132,7 @@ def get_per_issue_metrics(
issues: Union[List[dict], List[github3.search.IssueSearchResult]], # type: ignore
discussions: bool = False,
labels: Union[List[str], None] = None,
ignore_users: List[str] = [],
) -> tuple[List, int, int]:
"""
Calculate the metrics for each issue/pr/discussion in a list provided.
Expand All @@ -135,6 +143,7 @@ def get_per_issue_metrics(
discussions (bool, optional): Whether the issues are discussions or not.
Defaults to False.
labels (List[str]): A list of labels to measure time spent in. Defaults to empty list.
ignore_users (List[str]): A list of users to ignore when calculating metrics.

Returns:
tuple[List[IssueWithMetrics], int, int]: A tuple containing a
Expand All @@ -157,7 +166,7 @@ def get_per_issue_metrics(
None,
)
issue_with_metrics.time_to_first_response = measure_time_to_first_response(
None, issue
None, issue, ignore_users
)
issue_with_metrics.time_to_answer = measure_time_to_answer(issue)
if issue["closedAt"]:
Expand All @@ -175,7 +184,7 @@ def get_per_issue_metrics(
None,
)
issue_with_metrics.time_to_first_response = measure_time_to_first_response(
issue, None
issue, None, ignore_users
)
if labels:
issue_with_metrics.label_metrics = get_label_metrics(issue, labels)
Expand Down Expand Up @@ -238,6 +247,7 @@ def main():
env_vars = get_env_vars()
search_query = env_vars[0]
token = env_vars[1]
ignore_users = env_vars[2]

# Get the repository owner and name from the search query
owner = get_owner(search_query)
Expand Down Expand Up @@ -280,6 +290,7 @@ def main():
issues,
discussions="type:discussions" in search_query,
labels=labels,
ignore_users=ignore_users,
)

average_time_to_first_response = get_average_time_to_first_response(
Expand Down
50 changes: 50 additions & 0 deletions test_time_to_first_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,56 @@ def test_measure_time_to_first_response_no_comments(self):
# Check the results
self.assertEqual(result, expected_result)

def test_measure_time_to_first_response_ignore_users(self):
"""Test that measure_time_to_first_response ignores comments from ignored users."""
# Set up the mock GitHub issues
mock_issue1 = MagicMock()
mock_issue1.comments = 1
mock_issue1.created_at = "2023-01-01T00:00:00Z"

# Set up the mock GitHub comments (one ignored, one not ignored)
mock_comment1 = MagicMock()
mock_comment1.user.login = "ignored_user"
mock_comment1.created_at = datetime.fromisoformat("2023-01-02T00:00:00Z")

mock_comment2 = MagicMock()
mock_comment2.user.login = "not_ignored_user"
mock_comment2.created_at = datetime.fromisoformat("2023-01-03T00:00:00Z")

mock_issue1.issue.comments.return_value = [mock_comment1, mock_comment2]

# Call the function
result = measure_time_to_first_response(mock_issue1, None, ["ignored_user"])
expected_result = timedelta(days=2)

# Check the results
self.assertEqual(result, expected_result)

def test_measure_time_to_first_response_only_ignored_users(self):
"""Test that measure_time_to_first_response returns empty for an issue with only ignored users."""
# Set up the mock GitHub issues
mock_issue1 = MagicMock()
mock_issue1.comments = 1
mock_issue1.created_at = "2023-01-01T00:00:00Z"

# Set up the mock GitHub comments (all ignored)
mock_comment1 = MagicMock()
mock_comment1.user.login = "ignored_user"
mock_comment1.created_at = datetime.fromisoformat("2023-01-02T00:00:00Z")

mock_comment2 = MagicMock()
mock_comment2.user.login = "ignored_user2"
mock_comment2.created_at = datetime.fromisoformat("2023-01-03T00:00:00Z")

mock_issue1.issue.comments.return_value = [mock_comment1, mock_comment2]

# Call the function
result = measure_time_to_first_response(mock_issue1, None, ["ignored_user", "ignored_user2"])
expected_result = None

# Check the results
self.assertEqual(result, expected_result)


class TestGetAverageTimeToFirstResponse(unittest.TestCase):
"""Test the get_average_time_to_first_response function."""
Expand Down
10 changes: 8 additions & 2 deletions time_to_first_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@
def measure_time_to_first_response(
issue: Union[github3.issues.Issue, None], # type: ignore
discussion: Union[dict, None],
ignore_users: List[str] = [],
) -> Union[timedelta, None]:
"""Measure the time to first response for a single issue or a discussion.

Args:
issue (Union[github3.issues.Issue, None]): A GitHub issue.
discussion (Union[dict, None]): A GitHub discussion.
ignore_users (List[str]): A list of GitHub usernames to ignore.

Returns:
Union[timedelta, None]: The time to first response for the issue/discussion.
Expand All @@ -46,17 +48,21 @@ def measure_time_to_first_response(
# Get the first comment time
if issue:
comments = issue.issue.comments(
number=1, sort="created", direction="asc"
number=20, sort="created", direction="asc"
) # type: ignore
for comment in comments:
if comment.user.login in ignore_users:
continue
first_comment_time = comment.created_at

# Check if the issue is actually a pull request
# so we may also get the first review comment time
if issue.issue.pull_request_urls:
pull_request = issue.issue.pull_request()
review_comments = pull_request.reviews(number=1) # type: ignore
review_comments = pull_request.reviews(number=50) # type: ignore
for review_comment in review_comments:
if review_comment.user.login in ignore_users:
continue
first_review_comment_time = review_comment.submitted_at

# Figure out the earliest response timestamp
Expand Down