Browse Source

feat(dashboardsv2): Add events-geo endpoint (#23272)

Alberto Leal 4 years ago
parent
commit
5ba9012668

+ 52 - 0
src/sentry/api/endpoints/organization_events.py

@@ -2,7 +2,9 @@ import logging
 
 from functools import partial
 from rest_framework.response import Response
+from rest_framework.exceptions import ParseError
 
+from sentry import features
 from sentry.api.bases import (
     OrganizationEventsEndpointBase,
     OrganizationEventsV2EndpointBase,
@@ -11,6 +13,7 @@ from sentry.api.bases import (
 from sentry.api.helpers.events import get_direct_hit_response
 from sentry.api.paginator import GenericOffsetPaginator
 from sentry.api.serializers import EventSerializer, serialize, SimpleEventSerializer
+from sentry.api.event_search import is_function
 from sentry import eventstore
 from sentry.snuba import discover
 from sentry.models.project import Project
@@ -124,3 +127,52 @@ class OrganizationEventsV2Endpoint(OrganizationEventsV2EndpointBase):
                         request, organization, params["project_id"], results
                     ),
                 )
+
+
+class OrganizationEventsGeoEndpoint(OrganizationEventsV2EndpointBase):
+    def has_feature(self, request, organization):
+        return features.has("organizations:dashboards-v2", organization, actor=request.user)
+
+    def get(self, request, organization):
+        if not self.has_feature(request, organization):
+            return Response(status=404)
+
+        try:
+            params = self.get_snuba_params(request, organization)
+        except NoProjects:
+            return Response([])
+
+        maybe_aggregate = request.GET.get("field")
+
+        if not maybe_aggregate:
+            raise ParseError(detail="No column selected")
+
+        if not is_function(maybe_aggregate):
+            raise ParseError(detail="Functions may only be given")
+
+        def data_fn(offset, limit):
+            return discover.query(
+                selected_columns=["geo.country_code", maybe_aggregate],
+                query=request.GET.get("query"),
+                params=params,
+                offset=offset,
+                limit=limit,
+                referrer=request.GET.get("referrer", "api.organization-events-geo"),
+                use_aggregate_conditions=True,
+            )
+
+        with self.handle_query_errors():
+            # We don't need pagination, so we don't include the cursor headers
+            return Response(
+                self.handle_results_with_meta(
+                    request,
+                    organization,
+                    params["project_id"],
+                    # Expect Discover query output to be at most 251 rows, which corresponds
+                    # to the number of possible two-letter country codes as defined in ISO 3166-1 alpha-2.
+                    #
+                    # There are 250 country codes from sentry/src/sentry/static/sentry/app/data/countryCodesMap.tsx
+                    # plus events with no assigned country code.
+                    data_fn(0, self.get_per_page(request, default_per_page=251, max_per_page=251)),
+                )
+            )

+ 10 - 1
src/sentry/api/urls.py

@@ -90,7 +90,11 @@ from .endpoints.organization_details import OrganizationDetailsEndpoint
 from .endpoints.organization_environments import OrganizationEnvironmentsEndpoint
 from .endpoints.organization_event_details import OrganizationEventDetailsEndpoint
 from .endpoints.organization_eventid import EventIdLookupEndpoint
-from .endpoints.organization_events import OrganizationEventsEndpoint, OrganizationEventsV2Endpoint
+from .endpoints.organization_events import (
+    OrganizationEventsEndpoint,
+    OrganizationEventsV2Endpoint,
+    OrganizationEventsGeoEndpoint,
+)
 from .endpoints.organization_events_histogram import OrganizationEventsHistogramEndpoint
 from .endpoints.organization_events_facets import OrganizationEventsFacetsEndpoint
 from .endpoints.organization_events_meta import (
@@ -838,6 +842,11 @@ urlpatterns = [
                     OrganizationEventsStatsEndpoint.as_view(),
                     name="sentry-api-0-organization-events-stats",
                 ),
+                url(
+                    r"^(?P<organization_slug>[^\/]+)/events-geo/$",
+                    OrganizationEventsGeoEndpoint.as_view(),
+                    name="sentry-api-0-organization-events-geo",
+                ),
                 url(
                     r"^(?P<organization_slug>[^\/]+)/events-facets/$",
                     OrganizationEventsFacetsEndpoint.as_view(),

+ 110 - 0
tests/snuba/api/endpoints/test_organization_events_geo.py

@@ -0,0 +1,110 @@
+from __future__ import absolute_import
+
+from django.core.urlresolvers import reverse
+
+from sentry.testutils import APITestCase, SnubaTestCase
+from sentry.testutils.helpers.datetime import before_now, iso_format
+
+
+class OrganizationEventsGeoEndpointTest(APITestCase, SnubaTestCase):
+    def setUp(self):
+        super(OrganizationEventsGeoEndpointTest, self).setUp()
+        self.min_ago = iso_format(before_now(minutes=1))
+
+    def do_request(self, query, features=None):
+        if features is None:
+            features = {"organizations:dashboards-v2": True}
+        self.login_as(user=self.user)
+        url = reverse(
+            "sentry-api-0-organization-events-geo",
+            kwargs={"organization_slug": self.organization.slug},
+        )
+        with self.feature(features):
+            return self.client.get(url, query, format="json")
+
+    def test_no_projects(self):
+        response = self.do_request({})
+
+        assert response.status_code == 200, response.data
+        assert len(response.data) == 0
+
+    def test_no_field(self):
+        query = {
+            "field": [],
+            "project": [self.project.id],
+        }
+
+        response = self.do_request(query)
+        assert response.status_code == 400
+        assert response.data["detail"] == "No column selected"
+
+    def test_require_aggregate_field(self):
+        query = {
+            "field": ["i_am_a_tag"],
+            "project": [self.project.id],
+        }
+
+        response = self.do_request(query)
+        assert response.status_code == 400
+        assert response.data["detail"] == "Functions may only be given"
+
+    def test_happy_path(self):
+        other_project = self.create_project()
+        self.store_event(
+            data={"event_id": "a" * 32, "environment": "staging", "timestamp": self.min_ago},
+            project_id=self.project.id,
+        )
+        self.store_event(
+            data={"event_id": "b" * 32, "environment": "staging", "timestamp": self.min_ago},
+            project_id=other_project.id,
+        )
+        self.store_event(
+            data={
+                "event_id": "c" * 32,
+                "environment": "production",
+                "timestamp": self.min_ago,
+                "user": {
+                    "email": "foo@example.com",
+                    "id": "123",
+                    "ip_address": "127.0.0.1",
+                    "username": "foo",
+                    "geo": {"country_code": "CA", "region": "Canada"},
+                },
+            },
+            project_id=self.project.id,
+        )
+
+        query = {
+            "project": [self.project.id],
+            "field": ["count()"],
+            "statsPeriod": "24h",
+        }
+
+        response = self.do_request(query)
+        assert response.status_code == 200, response.data
+        assert len(response.data["data"]) == 2
+        assert response.data["data"] == [
+            {"count": 1, "geo.country_code": None},
+            {"count": 1, "geo.country_code": "CA"},
+        ]
+        # Expect no pagination
+        assert "Link" not in response
+
+    def test_only_use_last_field(self):
+        self.store_event(
+            data={"event_id": "a" * 32, "environment": "staging", "timestamp": self.min_ago},
+            project_id=self.project.id,
+        )
+
+        query = {
+            "project": [self.project.id],
+            "field": ["p75()", "count()"],
+            "statsPeriod": "24h",
+        }
+
+        response = self.do_request(query)
+        assert response.status_code == 200, response.data
+        assert len(response.data["data"]) == 1
+        assert response.data["data"] == [
+            {"count": 1, "geo.country_code": None},
+        ]