Skip to content

Commit

Permalink
First stab at figuring out the number of very active mentors (#206)
Browse files Browse the repository at this point in the history
* First stab at figuring out the number of very active mentors in a project.

* Fix format and linter errors.

* Add flake to requirements.

* Adds call to mentor counter and output in markdown.

This adds the call to mentor counter and displays the results in markdown
including first tests for this functionality.

* Make mentor counting configurable.

This adds two configuration options: One to enable mentor counting and one for
configuring how many comments a user needs to leave in discussions, PRs. and
issues to be counted as an active mentor.

* Adds mentor counting to json output and adds missing config.

This adds mentor counting output to json format. In addition this change makes
max number of comments to evaluate configurable as well as the cutoff for
heavily involved mentors.

* Fix merge conflicts.

* Fix linting errors.

* fix: linting fixes

Signed-off-by: Zack Koppert <[email protected]>

* 8 is reasonable number of attrs

Signed-off-by: Zack Koppert <[email protected]>

* Update test_most_active_mentors.py

Co-authored-by: Jason Meridth <[email protected]>

* Update config.py

Co-authored-by: Jason Meridth <[email protected]>

* Update config.py

Remove merge residual

* Update requirements.txt

Remove lib only needed for testing.

* Update issue_metrics.py

Co-authored-by: Jason Meridth <[email protected]>

* Update config.py

* Update config.py

set type of `enable_mentor_count` to `bool`

* Update test_config.py

change tests to handle boolean change of enable_mentor_count

---------

Signed-off-by: Zack Koppert <[email protected]>
Co-authored-by: Drost-Fromm <[email protected]>
Co-authored-by: Zack Koppert <[email protected]>
Co-authored-by: Jason Meridth <[email protected]>
Co-authored-by: Jason Meridth <[email protected]>
  • Loading branch information
5 people committed Apr 11, 2024
1 parent 0a896e9 commit eb98ac5
Show file tree
Hide file tree
Showing 12 changed files with 370 additions and 8 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ This action can be configured to authenticate with GitHub App Installation or Pe
| `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. |
| `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. |
| `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) |
| `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors |
| `MIN_MENTOR_COMMENTS` | False | 10 | Minimum number of comments to count as a mentor |
| `MAX_COMMENTS_EVAL` | False | 20 | Maximum number of comments per thread to evaluate for mentor stats |
| `HEAVILY_INVOLVED_CUTOFF` | False | 3 | Cutoff after which a mentor's comments in one issue are no longer counted against their total score |
| `LABELS_TO_MEASURE` | False | `""` | A comma separated list of labels to measure how much time the label is applied. If not provided, no labels durations will be measured. Not compatible with discussions at this time. |
| `SEARCH_QUERY` | True | `""` | The query by which you can filter issues/PRs which must contain a `repo:`, `org:`, `owner:`, or a `user:` entry. For discussions, include `type:discussions` in the query. |

Expand Down
5 changes: 5 additions & 0 deletions classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ class IssueWithMetrics:
time_to_answer (timedelta, optional): The time it took to answer the
discussions in the issue.
label_metrics (dict, optional): A dictionary containing the label metrics
mentor_activity (dict, optional): A dictionary containing active mentors
"""

# pylint: disable=too-many-instance-attributes

def __init__(
self,
title,
Expand All @@ -31,6 +34,7 @@ def __init__(
time_to_close=None,
time_to_answer=None,
labels_metrics=None,
mentor_activity=None,
):
self.title = title
self.html_url = html_url
Expand All @@ -39,3 +43,4 @@ def __init__(
self.time_to_close = time_to_close
self.time_to_answer = time_to_answer
self.label_metrics = labels_metrics
self.mentor_activity = mentor_activity
24 changes: 24 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class EnvVars:
hide_time_to_first_response (bool): If true, the time to first response metric is hidden in the output
ignore_users (List[str]): List of usernames to ignore when calculating metrics
labels_to_measure (List[str]): List of labels to measure how much time the lable is applied
enable_mentor_count (bool): If set to TRUE, compute number of mentors
min_mentor_comments (str): If set, defines the minimum number of comments for mentors
max_comments_eval (str): If set, defines the maximum number of comments to look at for mentor evaluation
heavily_involved_cutoff (str): If set, defines the cutoff after which heavily involved commentors in
search_query (str): Search query used to filter issues/prs/discussions on GitHub
"""

Expand All @@ -51,6 +55,10 @@ def __init__(
hide_time_to_first_response: bool,
ignore_user: List[str],
labels_to_measure: List[str],
enable_mentor_count: bool,
min_mentor_comments: str,
max_comments_eval: str,
heavily_involved_cutoff: str,
search_query: str,
):
self.gh_app_id = gh_app_id
Expand All @@ -65,6 +73,10 @@ 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.enable_mentor_count = enable_mentor_count
self.min_mentor_comments = min_mentor_comments
self.max_comments_eval = max_comments_eval
self.heavily_involved_cutoff = heavily_involved_cutoff
self.search_query = search_query

def __repr__(self):
Expand All @@ -82,6 +94,10 @@ def __repr__(self):
f"{self.hide_time_to_first_response},"
f"{self.ignore_users},"
f"{self.labels_to_measure},"
f"{self.enable_mentor_count},"
f"{self.min_mentor_comments},"
f"{self.max_comments_eval},"
f"{self.heavily_involved_cutoff},"
f"{self.search_query})"
)

Expand Down Expand Up @@ -166,6 +182,10 @@ def get_env_vars(test: bool = False) -> EnvVars:
hide_time_to_answer = get_bool_env_var("HIDE_TIME_TO_ANSWER")
hide_time_to_close = get_bool_env_var("HIDE_TIME_TO_CLOSE")
hide_time_to_first_response = get_bool_env_var("HIDE_TIME_TO_FIRST_RESPONSE")
enable_mentor_count = get_bool_env_var("ENABLE_MENTOR_COUNT")
min_mentor_comments = os.getenv("MIN_MENTOR_COMMENTS", "10")
max_comments_eval = os.getenv("MAX_COMMENTS_EVAL", "20")
heavily_involved_cutoff = os.getenv("HEAVILY_INVOLVED_CUTOFF", "3")

return EnvVars(
gh_app_id,
Expand All @@ -180,5 +200,9 @@ def get_env_vars(test: bool = False) -> EnvVars:
hide_time_to_first_response,
ignore_users_list,
labels_to_measure_list,
enable_mentor_count,
min_mentor_comments,
max_comments_eval,
heavily_involved_cutoff,
search_query,
)
40 changes: 36 additions & 4 deletions issue_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from json_writer import write_to_json
from labels import get_label_metrics, get_stats_time_in_labels
from markdown_writer import write_to_markdown
from most_active_mentors import count_comments_per_user, get_mentor_count
from time_to_answer import get_stats_time_to_answer, measure_time_to_answer
from time_to_close import get_stats_time_to_close, measure_time_to_close
from time_to_first_response import (
Expand All @@ -40,8 +41,6 @@
from time_to_merge import measure_time_to_merge
from time_to_ready_for_review import get_time_to_ready_for_review

GITHUB_BASE_URL = "https://github.com"


def search_issues(
search_query: str, github_connection: github3.GitHub
Expand Down Expand Up @@ -126,6 +125,8 @@ def get_per_issue_metrics(
discussions: bool = False,
labels: Union[List[str], None] = None,
ignore_users: Union[List[str], None] = None,
max_comments_to_eval: int = 20,
heavily_involved: int = 3,
) -> tuple[List, int, int]:
"""
Calculate the metrics for each issue/pr/discussion in a list provided.
Expand Down Expand Up @@ -158,10 +159,20 @@ def get_per_issue_metrics(
None,
None,
None,
None,
)
issue_with_metrics.time_to_first_response = measure_time_to_first_response(
None, issue, ignore_users
)
issue_with_metrics.mentor_activity = count_comments_per_user(
None,
issue,
ignore_users,
None,
None,
max_comments_to_eval,
heavily_involved,
)
issue_with_metrics.time_to_answer = measure_time_to_answer(issue)
if issue["closedAt"]:
issue_with_metrics.time_to_close = measure_time_to_close(None, issue)
Expand All @@ -188,6 +199,15 @@ def get_per_issue_metrics(
issue_with_metrics.time_to_first_response = measure_time_to_first_response(
issue, None, pull_request, ready_for_review_at, ignore_users
)
issue_with_metrics.mentor_activity = count_comments_per_user(
issue,
None,
pull_request,
ready_for_review_at,
ignore_users,
max_comments_to_eval,
heavily_involved,
)
if labels:
issue_with_metrics.label_metrics = get_label_metrics(issue, labels)
if issue.state == "closed": # type: ignore
Expand Down Expand Up @@ -259,6 +279,10 @@ def main():
token,
env_vars.ghe,
)
enable_mentor_count = env_vars.enable_mentor_count
min_mentor_count = int(env_vars.min_mentor_comments)
max_comments_eval = int(env_vars.max_comments_eval)
heavily_involved_cutoff = int(env_vars.heavily_involved_cutoff)

# Get the repository owner and name from the search query
owner = get_owner(search_query)
Expand All @@ -283,13 +307,13 @@ def main():
issues = get_discussions(token, search_query)
if len(issues) <= 0:
print("No discussions found")
write_to_markdown(None, None, None, None, None, None, None)
write_to_markdown(None, None, None, None, None, None, None, None)
return
else:
issues = search_issues(search_query, github_connection)
if len(issues) <= 0:
print("No issues found")
write_to_markdown(None, None, None, None, None, None, None)
write_to_markdown(None, None, None, None, None, None, None, None)
return

# Get all the metrics
Expand All @@ -298,6 +322,8 @@ def main():
discussions="type:discussions" in search_query,
labels=labels,
ignore_users=ignore_users,
max_comments_to_eval=max_comments_eval,
heavily_involved=heavily_involved_cutoff,
)

stats_time_to_first_response = get_stats_time_to_first_response(issues_with_metrics)
Expand All @@ -307,6 +333,10 @@ def main():

stats_time_to_answer = get_stats_time_to_answer(issues_with_metrics)

num_mentor_count = 0
if enable_mentor_count:
num_mentor_count = get_mentor_count(issues_with_metrics, min_mentor_count)

# Get stats describing the time in label for each label and store it in a dictionary
# where the key is the label and the value is the average time
stats_time_in_labels = get_stats_time_in_labels(issues_with_metrics, labels)
Expand All @@ -320,6 +350,7 @@ def main():
stats_time_in_labels,
num_issues_open,
num_issues_closed,
num_mentor_count,
search_query,
)
write_to_markdown(
Expand All @@ -330,6 +361,7 @@ def main():
stats_time_in_labels,
num_issues_open,
num_issues_closed,
num_mentor_count,
labels,
search_query,
)
Expand Down
3 changes: 3 additions & 0 deletions json_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def write_to_json(
stats_time_in_labels: Union[dict[str, dict[str, timedelta]], None],
num_issues_opened: Union[int, None],
num_issues_closed: Union[int, None],
num_mentor_count: Union[int, None],
search_query: str,
) -> str:
"""
Expand All @@ -42,6 +43,7 @@ def write_to_json(
"average_time_to_answer": "1 day, 0:00:00",
"num_items_opened": 2,
"num_items_closed": 1,
"num_mentor_count": 5,
"total_item_count": 2,
"issues": [
{
Expand Down Expand Up @@ -129,6 +131,7 @@ def write_to_json(
"90_percentile_time_in_labels": p90_time_in_labels,
"num_items_opened": num_issues_opened,
"num_items_closed": num_issues_closed,
"num_mentor_count": num_mentor_count,
"total_item_count": len(issues_with_metrics),
}

Expand Down
8 changes: 7 additions & 1 deletion markdown_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
average_time_to_answer: timedelta,
num_issues_opened: int,
num_issues_closed: int,
num_mentor_count: int,
file: file object = None
) -> None:
Write the issues with metrics to a markdown file.
Expand Down Expand Up @@ -79,6 +80,7 @@ def write_to_markdown(
average_time_in_labels: Union[dict, None],
num_issues_opened: Union[int, None],
num_issues_closed: Union[int, None],
num_mentor_count: Union[int, None],
labels=None,
search_query=None,
hide_label_metrics=False,
Expand All @@ -95,7 +97,8 @@ def write_to_markdown(
file (file object, optional): The file object to write to. If not provided,
a file named "issue_metrics.md" will be created.
num_issues_opened (int): The Number of items that remain opened.
num_issues_closed (int): The number of issues that were closed.
num_issues_closed (int): The number of issues that were closedi.
num_mentor_count (int): The number of very active commentors.
labels (List[str]): A list of the labels that are used in the issues.
search_query (str): The search query used to find the issues.
hide_label_metrics (bool): Represents whether the user has chosen to hide label metrics in the output
Expand Down Expand Up @@ -127,6 +130,7 @@ def write_to_markdown(
average_time_in_labels,
num_issues_opened,
num_issues_closed,
num_mentor_count,
labels,
columns,
file,
Expand Down Expand Up @@ -184,6 +188,7 @@ def write_overall_metrics_tables(
stats_time_in_labels,
num_issues_opened,
num_issues_closed,
num_mentor_count,
labels,
columns,
file,
Expand Down Expand Up @@ -246,4 +251,5 @@ def write_overall_metrics_tables(
file.write("| --- | ---: |\n")
file.write(f"| Number of items that remain open | {num_issues_opened} |\n")
file.write(f"| Number of items closed | {num_issues_closed} |\n")
file.write(f"| Number of most active mentors | {num_mentor_count} |\n")
file.write(f"| Total number of items created | {len(issues_with_metrics)} |\n\n")
Loading

0 comments on commit eb98ac5

Please sign in to comment.