Browse Source

feat(workflow): 'Team' Release Count API (#28836)

This endpoint will be used on the project reports page to power a graph (and table) of releases created over time for projects your team belongs to.

The table also wants the 12w average.
Chris Fuller 3 years ago
parent
commit
34116495c3

+ 58 - 0
src/sentry/api/endpoints/team_release_count.py

@@ -0,0 +1,58 @@
+from datetime import timedelta
+
+from django.db.models import Count
+from django.db.models.functions import TruncDay
+from django.utils.timezone import now
+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 Project, Release, ReleaseProject
+
+
+class TeamReleaseCountEndpoint(TeamEndpoint, EnvironmentMixin):
+    def get(self, request, team):
+        """
+        Returns a dict of team projects, and a time-series list of release counts for each.
+        """
+        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)
+
+        per_project_average_releases = (
+            Release.objects.filter(
+                id__in=ReleaseProject.objects.filter(project__in=project_list).values("release_id"),
+                date_added__gte=now() - timedelta(days=84),
+                date_added__lte=now(),
+            )
+            .values("projects")
+            .annotate(count=Count("id"))
+        )
+        # TODO: Also need "this week" count for each project. Should i just bucket by week and average in python?
+        project_avgs = {}
+        for row in per_project_average_releases:
+            project_avgs[row["projects"]] = row["count"] / 12
+
+        bucketed_total_releases = (
+            Release.objects.filter(
+                id__in=ReleaseProject.objects.filter(project__in=project_list).values("release_id"),
+                date_added__gte=start,
+                date_added__lte=end,
+            )
+            .annotate(bucket=TruncDay("date_added"))
+            .order_by("bucket")
+            .values("bucket")
+            .annotate(count=Count("id"))
+        )
+
+        current_day, agg_project_counts = start, {}
+        while current_day < end:
+            agg_project_counts[str(current_day)] = 0
+            current_day += timedelta(days=1)
+
+        for bucket in bucketed_total_releases:
+            agg_project_counts[str(bucket["bucket"].date())] = bucket["count"]
+
+        return Response({"release_counts": agg_project_counts, "project_avgs": project_avgs})

+ 6 - 0
src/sentry/api/urls.py

@@ -405,6 +405,7 @@ from .endpoints.team_issue_breakdown import TeamIssueBreakdownEndpoint
 from .endpoints.team_members import TeamMembersEndpoint
 from .endpoints.team_notification_settings_details import TeamNotificationSettingsDetailsEndpoint
 from .endpoints.team_projects import TeamProjectsEndpoint
+from .endpoints.team_release_count import TeamReleaseCountEndpoint
 from .endpoints.team_stats import TeamStatsEndpoint
 from .endpoints.team_time_to_resolution import TeamTimeToResolutionEndpoint
 from .endpoints.user_authenticator_details import UserAuthenticatorDetailsEndpoint
@@ -1455,6 +1456,11 @@ urlpatterns = [
                     TeamGroupsNewEndpoint.as_view(),
                     name="sentry-api-0-team-groups-new",
                 ),
+                url(
+                    r"^(?P<organization_slug>[^\/]+)/(?P<team_slug>[^\/]+)/release-count/$",
+                    TeamReleaseCountEndpoint.as_view(),
+                    name="sentry-api-0-team-release-count",
+                ),
                 url(
                     r"^(?P<organization_slug>[^\/]+)/(?P<team_slug>[^\/]+)/time-to-resolution/$",
                     TeamTimeToResolutionEndpoint.as_view(),

+ 114 - 0
tests/sentry/api/endpoints/test_team_release_count.py

@@ -0,0 +1,114 @@
+from sentry.models import Release
+from sentry.testutils import APITestCase
+from sentry.testutils.helpers.datetime import before_now
+
+
+class TeamReleaseCountTest(APITestCase):
+    endpoint = "sentry-api-0-team-release-count"
+
+    def test_simple(self):
+        user = self.create_user(is_staff=False, is_superuser=False)
+        org = self.organization
+        org2 = self.create_organization()
+        org.flags.allow_joinleave = False
+        org.save()
+
+        team1 = self.create_team(organization=org)
+        team2 = self.create_team(organization=org)
+
+        project1 = self.create_project(teams=[team1], organization=org)
+        project2 = self.create_project(teams=[team2], organization=org2)
+        project3 = self.create_project(teams=[team1], organization=org)
+
+        self.create_member(teams=[team1], user=user, organization=org)
+        self.login_as(user=user)
+        release1 = Release.objects.create(
+            organization_id=org.id, version="1", date_added=before_now(days=15)
+        )
+        release1.add_project(project1)
+
+        release2 = Release.objects.create(
+            organization_id=org2.id, version="2", date_added=before_now(days=12)
+        )  # This release isn't returned, its in another org
+        release2.add_project(project2)
+
+        release3 = Release.objects.create(
+            organization_id=org.id,
+            version="3",
+            date_added=before_now(days=10),
+            date_released=before_now(days=10),
+        )
+        release3.add_project(project1)
+
+        release4 = Release.objects.create(
+            organization_id=org.id, version="4", date_added=before_now(days=5)
+        )
+        release4.add_project(project3)
+        release5 = Release.objects.create(
+            organization_id=org.id, version="5", date_added=before_now(days=5)
+        )
+        release5.add_project(project3)
+        response = self.get_valid_response(org.slug, team1.slug)
+
+        assert len(response.data) == 2
+        assert len(response.data["release_counts"]) == 90
+        assert len(response.data["project_avgs"]) == 2
+
+        assert response.data["release_counts"][str(before_now(days=0).date())] == 0
+        assert response.data["release_counts"][str(before_now(days=5).date())] == 2
+        assert response.data["release_counts"][str(before_now(days=10).date())] == 1
+        assert response.data["release_counts"][str(before_now(days=15).date())] == 1
+
+    def test_multi_project_release(self):
+        user = self.create_user(is_staff=False, is_superuser=False)
+        org = self.organization
+        org2 = self.create_organization()
+        org.flags.allow_joinleave = False
+        org.save()
+
+        team1 = self.create_team(organization=org)
+        team2 = self.create_team(organization=org)
+
+        project1 = self.create_project(teams=[team1], organization=org)
+        project2 = self.create_project(teams=[team2], organization=org2)
+        project3 = self.create_project(teams=[team1], organization=org)
+
+        self.create_member(teams=[team1], user=user, organization=org)
+        self.login_as(user=user)
+        release1 = Release.objects.create(
+            organization_id=org.id, version="1", date_added=before_now(days=15)
+        )
+        release1.add_project(project1)
+        release1.add_project(project3)
+
+        release2 = Release.objects.create(
+            organization_id=org2.id, version="2", date_added=before_now(days=12)
+        )  # This release isn't returned, its in another org
+        release2.add_project(project2)
+
+        release3 = Release.objects.create(
+            organization_id=org.id,
+            version="3",
+            date_added=before_now(days=10),
+            date_released=before_now(days=10),
+        )
+        release3.add_project(project1)
+
+        release4 = Release.objects.create(
+            organization_id=org.id, version="4", date_added=before_now(days=5)
+        )
+        release4.add_project(project3)
+        release5 = Release.objects.create(
+            organization_id=org.id, version="5", date_added=before_now(days=5)
+        )
+        release5.add_project(project3)
+        response = self.get_valid_response(org.slug, team1.slug)
+
+        assert len(response.data) == 2
+        assert len(response.data["release_counts"]) == 90
+        assert len(response.data["project_avgs"]) == 2
+
+        assert response.data["release_counts"][str(before_now(days=0).date())] == 0
+        assert response.data["release_counts"][str(before_now(days=5).date())] == 2
+        assert response.data["release_counts"][str(before_now(days=10).date())] == 1
+        assert response.data["release_counts"][str(before_now(days=15).date())] == 1