Skip to content

Commit

Permalink
Merge pull request #78 from Okabe-Junya/junya/feat/add-ignore-users
Browse files Browse the repository at this point in the history
feat: Filter Out CI User Responses from Time to First Response
  • Loading branch information
zkoppert authored Aug 1, 2023
2 parents 7296d9d + fd029d1 commit 92b849a
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 8 deletions.
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

0 comments on commit 92b849a

Please sign in to comment.