From 197cbb0375c9259b422c85704578763a40384a92 Mon Sep 17 00:00:00 2001 From: Chris Fuller Date: Fri, 1 Oct 2021 12:09:06 -0400 Subject: [PATCH] feat(workflow): Time To Resolution API (#28910) --- .../api/endpoints/team_time_to_resolution.py | 54 +++++++++ src/sentry/api/urls.py | 6 + .../endpoints/test_team_time_to_resolution.py | 111 ++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 src/sentry/api/endpoints/team_time_to_resolution.py create mode 100644 tests/sentry/api/endpoints/test_team_time_to_resolution.py diff --git a/src/sentry/api/endpoints/team_time_to_resolution.py b/src/sentry/api/endpoints/team_time_to_resolution.py new file mode 100644 index 00000000000000..c34317d1556d3b --- /dev/null +++ b/src/sentry/api/endpoints/team_time_to_resolution.py @@ -0,0 +1,54 @@ +from collections import defaultdict +from datetime import timedelta + +from django.db.models import Avg, F +from django.db.models.functions import TruncDay +from rest_framework.response import Response + +from sentry.api.base import EnvironmentMixin +from sentry.api.bases.team import TeamEndpoint +from sentry.api.utils import get_date_range_from_params +from sentry.models import GroupHistory, GroupHistoryStatus, Project + + +class TeamTimeToResolutionEndpoint(TeamEndpoint, EnvironmentMixin): + def get(self, request, team): + """ + Return a a time bucketed list of mean group resolution times for a given team. + """ + project_list = Project.objects.get_for_team_ids(team_ids=[team.id]) + start, end = get_date_range_from_params(request.GET) + end = end.date() + timedelta(days=1) + start = start.date() + timedelta(days=1) + history_list = ( + GroupHistory.objects.filter( + status=GroupHistoryStatus.RESOLVED, + project__in=project_list, + date_added__gte=start, + date_added__lte=end, + ) + .annotate(bucket=TruncDay("date_added")) + .values("bucket", "prev_history_date") + .annotate(ttr=F("date_added") - F("prev_history_date")) + .annotate(avg_ttr=Avg("ttr")) + ) + sums = defaultdict(lambda: {"sum": timedelta(), "count": 0}) + for gh in history_list: + key = str(gh["bucket"].date()) + sums[key]["sum"] += gh["ttr"] + sums[key]["count"] += 1 + + avgs = {} + current_day = start + while current_day < end: + key = str(current_day) + if key in sums: + avg = int((sums[key]["sum"] / sums[key]["count"]).total_seconds()) + count = sums[key]["count"] + else: + avg = count = 0 + + avgs[key] = {"avg": avg, "count": count} + current_day += timedelta(days=1) + + return Response(avgs) diff --git a/src/sentry/api/urls.py b/src/sentry/api/urls.py index 30964e1e63a73f..3403638aa7f02e 100644 --- a/src/sentry/api/urls.py +++ b/src/sentry/api/urls.py @@ -405,6 +405,7 @@ from .endpoints.team_notification_settings_details import TeamNotificationSettingsDetailsEndpoint from .endpoints.team_projects import TeamProjectsEndpoint from .endpoints.team_stats import TeamStatsEndpoint +from .endpoints.team_time_to_resolution import TeamTimeToResolutionEndpoint from .endpoints.user_authenticator_details import UserAuthenticatorDetailsEndpoint from .endpoints.user_authenticator_enroll import UserAuthenticatorEnrollEndpoint from .endpoints.user_authenticator_index import UserAuthenticatorIndexEndpoint @@ -1453,6 +1454,11 @@ TeamGroupsNewEndpoint.as_view(), name="sentry-api-0-team-groups-new", ), + url( + r"^(?P[^\/]+)/(?P[^\/]+)/time-to-resolution/$", + TeamTimeToResolutionEndpoint.as_view(), + name="sentry-api-0-team-time-to-resolution", + ), url( r"^(?P[^\/]+)/(?P[^\/]+)/alerts-triggered/$", TeamAlertsTriggeredEndpoint.as_view(), diff --git a/tests/sentry/api/endpoints/test_team_time_to_resolution.py b/tests/sentry/api/endpoints/test_team_time_to_resolution.py new file mode 100644 index 00000000000000..8cdb23af60112f --- /dev/null +++ b/tests/sentry/api/endpoints/test_team_time_to_resolution.py @@ -0,0 +1,111 @@ +from datetime import timedelta + +from django.utils.timezone import now +from freezegun import freeze_time + +from sentry.models import GroupHistory, GroupHistoryStatus +from sentry.testutils import APITestCase +from sentry.testutils.helpers.datetime import before_now + + +@freeze_time() +class TeamTimeToResolutionTest(APITestCase): + endpoint = "sentry-api-0-team-time-to-resolution" + + def test_simple(self): + project1 = self.create_project(teams=[self.team], slug="foo") + project2 = self.create_project(teams=[self.team], slug="bar") + group1 = self.create_group(checksum="a" * 32, project=project1, times_seen=10) + group2 = self.create_group(checksum="b" * 32, project=project2, times_seen=5) + + gh1 = GroupHistory.objects.create( + organization=self.organization, + group=group1, + project=project1, + actor=self.user.actor, + date_added=before_now(days=5), + status=GroupHistoryStatus.UNRESOLVED, + prev_history=None, + prev_history_date=None, + ) + + GroupHistory.objects.create( + organization=self.organization, + group=group1, + project=project1, + actor=self.user.actor, + status=GroupHistoryStatus.RESOLVED, + prev_history=gh1, + prev_history_date=gh1.date_added, + date_added=before_now(days=2), + ) + + gh2 = GroupHistory.objects.create( + organization=self.organization, + group=group2, + project=project2, + actor=self.user.actor, + date_added=before_now(days=10), + status=GroupHistoryStatus.UNRESOLVED, + prev_history=None, + prev_history_date=None, + ) + + GroupHistory.objects.create( + organization=self.organization, + group=group2, + project=project2, + actor=self.user.actor, + status=GroupHistoryStatus.RESOLVED, + prev_history=gh2, + prev_history_date=gh2.date_added, + ) + today = str(now().date()) + yesterday = str((now() - timedelta(days=1)).date()) + two_days_ago = str((now() - timedelta(days=2)).date()) + self.login_as(user=self.user) + response = self.get_success_response( + self.team.organization.slug, self.team.slug, statsPeriod="14d" + ) + assert len(response.data) == 14 + assert response.data[today]["avg"] == timedelta(days=10).total_seconds() + assert response.data[two_days_ago]["avg"] == timedelta(days=3).total_seconds() + assert response.data[yesterday]["avg"] == 0 + + # Lower "todays" average by adding another resolution, but this time 5 days instead of 10 (avg is 7.5 now) + gh2 = GroupHistory.objects.create( + organization=self.organization, + group=group2, + project=project2, + actor=self.user.actor, + date_added=before_now(days=5), + status=GroupHistoryStatus.UNRESOLVED, + prev_history=None, + prev_history_date=None, + ) + GroupHistory.objects.create( + organization=self.organization, + group=group2, + project=project2, + actor=self.user.actor, + status=GroupHistoryStatus.RESOLVED, + prev_history=gh2, + prev_history_date=gh2.date_added, + ) + + # making sure it doesnt bork anything + GroupHistory.objects.create( + organization=self.organization, + group=group2, + project=project2, + actor=self.user.actor, + status=GroupHistoryStatus.DELETED, + prev_history=gh2, + prev_history_date=gh2.date_added, + ) + + response = self.get_success_response(self.team.organization.slug, self.team.slug) + assert len(response.data) == 90 + assert response.data[today]["avg"] == timedelta(days=7, hours=12).total_seconds() + assert response.data[two_days_ago]["avg"] == timedelta(days=3).total_seconds() + assert response.data[yesterday]["avg"] == 0