-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(workflow): Time To Resolution API (#28910)
- Loading branch information
1 parent
f52c157
commit 197cbb0
Showing
3 changed files
with
171 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
111 changes: 111 additions & 0 deletions
111
tests/sentry/api/endpoints/test_team_time_to_resolution.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |