Browse Source

work check in

David Burke 3 years ago
parent
commit
6a812ddfc5

+ 37 - 0
glitchtip/stats/serializers.py

@@ -0,0 +1,37 @@
+from rest_framework import serializers
+
+
+class StatsV2Serializer(serializers.Serializer):
+    category = serializers.ChoiceField(choices=("error", "error"))
+    interval = serializers.ChoiceField(
+        choices=(("1d", "1 day"), ("1h", "1 hour"), ("1m", "1 minute")),
+        default="1h",
+        required=False,
+    )
+    project = serializers.ListField(
+        child=serializers.IntegerField(min_value=-1), required=False
+    )
+    field = serializers.ChoiceField(
+        choices=(
+            ("sum(quantity)", "sum(quantity)"),
+            ("sum(times_seen", "sum(times_seen"),
+        ),
+    )
+    start = serializers.DateTimeField()
+    end = serializers.DateTimeField()
+
+    def validate(self, data):
+        start = data.get("start")
+        end = data.get("end")
+        interval = data.get("interval")
+
+        series_quantity = (end - start).days
+        if interval == "1h":
+            series_quantity *= 24
+        elif interval == "1m":
+            series_quantity *= 1440
+
+        if series_quantity > 1000:
+            raise serializers.ValidationError({"end": "Too many intervals"})
+        return data
+

+ 0 - 0
glitchtip/stats/tests/__init__.py


+ 20 - 0
glitchtip/stats/tests/test_api_permissions.py

@@ -0,0 +1,20 @@
+from django.shortcuts import reverse
+from model_bakery import baker
+from glitchtip.test_utils.test_case import APIPermissionTestCase
+
+
+class StatsAPIPermissionTests(APIPermissionTestCase):
+    def setUp(self):
+        self.create_user_org()
+        self.set_client_credentials(self.auth_token.token)
+        self.event = baker.make(
+            "events.Event", issue__project__organization=self.organization
+        )
+        self.url = reverse(
+            "stats-v2", kwargs={"organization_slug": self.organization.slug}
+        )
+
+    def test_get(self):
+        self.assertGetReqStatusCode(self.url, 403)
+        self.auth_token.add_permission("org:read")
+        self.assertGetReqStatusCode(self.url, 400)

+ 7 - 3
glitchtip/stats/tests.py → glitchtip/stats/tests/tests.py

@@ -7,13 +7,17 @@ from glitchtip.test_utils.test_case import GlitchTipTestCase
 class StatsV2APITestCase(GlitchTipTestCase):
     def setUp(self):
         self.create_user_and_project()
-        self.url = reverse("stats-v2", kwargs={"organization_slug": self.organization})
+        self.url = reverse(
+            "stats-v2", kwargs={"organization_slug": self.organization.slug}
+        )
 
     def test_get(self):
-        baker.make("events.Event")
+        baker.make("events.Event", issue__project=self.project)
         start = timezone.now() - timezone.timedelta(hours=2)
         end = timezone.now()
         res = self.client.get(
-            self.url, {"category": "error", "start": start, "end": end}
+            self.url,
+            {"category": "error", "start": start, "end": end, "field": "sum(quantity)"},
         )
         self.assertEqual(res.status_code, 200)
+

+ 51 - 34
glitchtip/stats/views.py

@@ -1,68 +1,85 @@
-# from django.db.models import
 from datetime import timedelta
-from dateutil import parser
 from django.db import connection
-from django.utils.timezone import make_aware
 from rest_framework import views
 from rest_framework.response import Response
-from rest_framework.status import HTTP_400_BAD_REQUEST
+from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND
+from projects.models import Project
+from organizations_ext.permissions import OrganizationPermission
+from .serializers import StatsV2Serializer
 
 
 EVENT_TIME_SERIES_SQL = """
 SELECT gs.ts, count(event.created)
-FROM generate_series(%s, %s, '1 hour'::interval) gs (ts)
+FROM generate_series(%s, %s, %s::interval) gs (ts)
 LEFT JOIN events_event event
 ON event.created >= gs.ts AND event.created < gs.ts +  interval '1 hour'
 RIGHT JOIN issues_issue issue
 ON event.issue_id = issue.id or event is null
+WHERE issue.project_id IN (%s)
+GROUP BY gs.ts ORDER BY gs.ts;
 """
 
-WHERE = "WHERE issue.project_id IN (%s) "
-
-GROUP_BY = "GROUP BY gs.ts ORDER BY gs.ts;"
-
 
 class StatsV2View(views.APIView):
     """
-    Reverse engineered stats v2 endpoint.
-    This endpoint in sentry is not open source and not documented, so good luck
-
+    Reverse engineered stats v2 endpoint. Endpoint in sentry not documented.
+    Appears similar to documented sessions endpoint.
     Used by the Sentry Grafana integration.
+
+    Used to return time series statistics.
+    Submit query params start, end, and interval (defaults to 1h)
+    Limits results to 1000 intervals. For example if using hours, max days would be 41
     """
 
+    permission_classes = [OrganizationPermission]
+
     def get(self, *args, **kwargs):
-        category = self.request.query_params.get("category")
-        start = parser.parse(self.request.query_params.get("start")).replace(
+        query_params = self.request.query_params
+        data = {
+            "category": query_params.get("category"),
+            "project": query_params.getlist("project"),
+            "field": query_params.get("field"),
+            "start": query_params.get("start"),
+            "end": query_params.get("end"),
+        }
+        if query_params.get("interval"):
+            data["interval"] = query_params.get("interval")
+        serializer = StatsV2Serializer(data=data)
+        serializer.is_valid(raise_exception=True)
+
+        category = serializer.validated_data["category"]
+        start = serializer.validated_data["start"].replace(
             microsecond=0, second=0, minute=0
         )
-        end = (
-            parser.parse(self.request.query_params.get("end")) + timedelta(hours=1)
-        ).replace(microsecond=0, second=0, minute=0)
-        field = self.request.query_params.get("field")
-        projects = self.request.query_params.getlist("project")
-        if projects:
-            projects = [int(project) for project in projects][0]
-        # ADD permissions next
-        print(projects)
+        end = (serializer.validated_data["end"] + timedelta(hours=1)).replace(
+            microsecond=0, second=0, minute=0
+        )
+        field = serializer.validated_data["field"]
+        interval = serializer.validated_data["interval"]
+        # Get projects that are authorized, filtered by organization, and selected by user
+        # Intentionally separate SQL call to simplify raw SQL
+        projects = Project.objects.filter(
+            organization__slug=self.kwargs.get("organization_slug"),
+            organization__users=self.request.user,
+        )
+        if serializer.validated_data.get("project"):
+            projects = projects.filter(pk__in=serializer.validated_data["project"])
+        project_ids = tuple(projects.values_list("id", flat=True))
+        if not project_ids:
+            return Response(status=HTTP_404_NOT_FOUND)
 
         if category == "error":
             with connection.cursor() as cursor:
-                if projects:
-                    cursor.execute(
-                        EVENT_TIME_SERIES_SQL + WHERE + GROUP_BY,
-                        [start, end, projects],
-                    )
-                else:
-                    cursor.execute(
-                        EVENT_TIME_SERIES_SQL + GROUP_BY, [start, end],
-                    )
+                cursor.execute(
+                    EVENT_TIME_SERIES_SQL, [start, end, interval, project_ids],
+                )
                 series = cursor.fetchall()
         else:
             return Response(status=HTTP_400_BAD_REQUEST)
 
         data = {
-            "intervals": [make_aware(row[0]) for row in series],
-            "groups": [{"series": {"sum(quantity)": [row[1] for row in series]},}],
+            "intervals": [row[0] for row in series],
+            "groups": [{"series": {field: [row[1] for row in series]},}],
         }
 
         return Response(data)