Browse Source

feat: Add environment filter support for latest and oldest event (#12440)

Allows fetching the oldest/latest event in a group by a filtered set of
environments. This will enable issue details view filtering in future.

Ref: SEN-238
Lyn Nagara 6 years ago
parent
commit
b05d3a521e

+ 5 - 1
src/sentry/api/endpoints/group_events_latest.py

@@ -7,6 +7,7 @@ from sentry.api.base import DocSection
 from sentry.api.bases.group import GroupEndpoint
 from sentry.models import Group
 from sentry.utils.apidocs import scenario, attach_scenarios
+from sentry.api.helpers.environments import get_environments
 
 
 @scenario('GetLatestGroupSample')
@@ -32,7 +33,10 @@ class GroupEventsLatestEndpoint(GroupEndpoint):
 
         :pparam string group_id: the ID of the issue
         """
-        event = group.get_latest_event()
+        environments = [e.name for e in get_environments(request, group.project.organization)]
+
+        event = group.get_latest_event_for_environments(environments)
+
         if not event:
             return Response({'detail': 'No events found for group'}, status=404)
 

+ 6 - 1
src/sentry/api/endpoints/group_events_oldest.py

@@ -7,6 +7,7 @@ from sentry.api.base import DocSection
 from sentry.api.bases.group import GroupEndpoint
 from sentry.models import Group
 from sentry.utils.apidocs import scenario, attach_scenarios
+from sentry.api.helpers.environments import get_environments
 
 
 @scenario('GetOldestGroupSample')
@@ -32,7 +33,11 @@ class GroupEventsOldestEndpoint(GroupEndpoint):
 
         :pparam string group_id: the ID of the issue
         """
-        event = group.get_oldest_event()
+
+        environments = [e.name for e in get_environments(request, group.project.organization)]
+
+        event = group.get_oldest_event_for_environments(environments)
+
         if not event:
             return Response({'detail': 'No events found for group'}, status=404)
 

+ 63 - 2
src/sentry/models/group.py

@@ -11,15 +11,16 @@ import logging
 import math
 import re
 import warnings
+from enum import Enum
 
-from datetime import timedelta
+from datetime import datetime, timedelta
 from django.core.urlresolvers import reverse
 from django.db import models
 from django.utils import timezone
 from django.utils.http import urlencode
 from django.utils.translation import ugettext_lazy as _
 
-from sentry import eventtypes, tagstore
+from sentry import eventtypes, tagstore, options
 from sentry.constants import (
     DEFAULT_LOGGER_NAME, EVENT_ORDERING_KEY, LOG_LEVELS, MAX_CULPRIT_LENGTH
 )
@@ -76,6 +77,40 @@ def get_group_with_redirect(id, queryset=None):
             raise error  # raise original `DoesNotExist`
 
 
+class EventOrdering(Enum):
+    LATEST = ['-timestamp', '-event_id']
+    OLDEST = ['timestamp', 'event_id']
+
+
+def get_oldest_or_latest_event_for_environments(
+        ordering, environments=[], issue_id=None, project_id=None):
+    from sentry.utils import snuba
+    from sentry.models import SnubaEvent
+
+    conditions = []
+
+    if len(environments) > 0:
+        conditions.append(['environment', 'IN', environments])
+
+    result = snuba.raw_query(
+        start=datetime.utcfromtimestamp(0),
+        end=datetime.utcnow(),
+        selected_columns=SnubaEvent.selected_columns,
+        conditions=conditions,
+        filter_keys={
+            'issue': [issue_id],
+            'project_id': [project_id],
+        },
+        orderby=ordering.value,
+        limit=1,
+    )
+
+    if 'error' not in result and len(result['data']) == 1:
+        return SnubaEvent(result['data'][0])
+
+    return None
+
+
 class GroupManager(BaseManager):
     use_for_related_fields = True
 
@@ -377,6 +412,19 @@ class Group(Model):
                 self._latest_event = None
         return self._latest_event
 
+    def get_latest_event_for_environments(self, environments=[]):
+        use_snuba = options.get('snuba.events-queries.enabled')
+
+        # Fetch without environment if Snuba is not enabled
+        if not use_snuba:
+            return self.get_latest_event()
+
+        return get_oldest_or_latest_event_for_environments(
+            EventOrdering.LATEST,
+            environments=environments,
+            issue_id=self.id,
+            project_id=self.project_id)
+
     def get_oldest_event(self):
         from sentry.models import Event
 
@@ -393,6 +441,19 @@ class Group(Model):
                 self._oldest_event = None
         return self._oldest_event
 
+    def get_oldest_event_for_environments(self, environments=[]):
+        use_snuba = options.get('snuba.events-queries.enabled')
+
+        # Fetch without environment if Snuba is not enabled
+        if not use_snuba:
+            return self.get_oldest_event()
+
+        return get_oldest_or_latest_event_for_environments(
+            EventOrdering.OLDEST,
+            environments=environments,
+            issue_id=self.id,
+            project_id=self.project_id)
+
     def get_first_release(self):
         if self.first_release_id is None:
             return tagstore.get_first_release(self.project_id, self.id)

+ 0 - 30
tests/sentry/api/endpoints/test_group_events_latest.py

@@ -1,30 +0,0 @@
-from __future__ import absolute_import
-
-import six
-
-from datetime import datetime
-
-from sentry.testutils import APITestCase
-
-
-class GroupEventsLatestTest(APITestCase):
-    def test_simple(self):
-        self.login_as(user=self.user)
-
-        group = self.create_group()
-        self.create_event(
-            event_id='a' * 32,
-            group=group,
-            datetime=datetime(2013, 8, 13, 3, 8, 25),
-        )
-        event_2 = self.create_event(
-            event_id='b' * 32,
-            group=group,
-            datetime=datetime(2013, 8, 13, 3, 8, 26),
-        )
-
-        url = u'/api/0/issues/{}/events/latest/'.format(group.id)
-        response = self.client.get(url, format='json')
-
-        assert response.status_code == 200
-        assert response.data['id'] == six.text_type(event_2.id)

+ 0 - 30
tests/sentry/api/endpoints/test_group_events_oldest.py

@@ -1,30 +0,0 @@
-from __future__ import absolute_import
-
-import six
-
-from datetime import datetime
-
-from sentry.testutils import APITestCase
-
-
-class GroupEventsOldestTest(APITestCase):
-    def test_simple(self):
-        self.login_as(user=self.user)
-
-        group = self.create_group()
-        event_1 = self.create_event(
-            event_id='a' * 32,
-            group=group,
-            datetime=datetime(2013, 8, 13, 3, 8, 25),
-        )
-        self.create_event(
-            event_id='b' * 32,
-            group=group,
-            datetime=datetime(2013, 8, 13, 3, 8, 26),
-        )
-
-        url = u'/api/0/issues/{}/events/oldest/'.format(group.id)
-        response = self.client.get(url, format='json')
-
-        assert response.status_code == 200
-        assert response.data['id'] == six.text_type(event_1.id)

+ 61 - 0
tests/snuba/api/endpoints/test_group_events_latest.py

@@ -0,0 +1,61 @@
+from __future__ import absolute_import
+
+import six
+
+from datetime import timedelta
+from django.utils import timezone
+
+from sentry import options
+from sentry.models import Group
+from sentry.testutils import APITestCase, SnubaTestCase
+
+
+class GroupEventsLatestTest(APITestCase, SnubaTestCase):
+    def setUp(self):
+        super(GroupEventsLatestTest, self).setUp()
+        self.login_as(user=self.user)
+
+        project = self.create_project()
+        min_ago = (timezone.now() - timedelta(minutes=1)).isoformat()[:19]
+        two_min_ago = (timezone.now() - timedelta(minutes=2)).isoformat()[:19]
+
+        self.event1 = self.store_event(
+            data={
+                'event_id': 'a' * 32,
+                'environment': 'staging',
+                'fingerprint': ['group_1'],
+                'timestamp': two_min_ago
+            },
+            project_id=project.id,
+        )
+
+        self.event2 = self.store_event(
+            data={
+                'event_id': 'b' * 32,
+                'environment': 'production',
+                'fingerprint': ['group_1'],
+                'timestamp': min_ago
+            },
+            project_id=project.id,
+        )
+
+        self.group = Group.objects.first()
+
+    def test_simple(self):
+        url = u'/api/0/issues/{}/events/latest/'.format(self.group.id)
+        response = self.client.get(url, format='json')
+
+        assert response.status_code == 200
+        assert response.data['id'] == six.text_type(self.event2.id)
+
+    def test_snuba_no_environment(self):
+        options.set('snuba.events-queries.enabled', True)
+        self.test_simple()
+
+    def test_environment(self):
+        options.set('snuba.events-queries.enabled', True)
+        url = u'/api/0/issues/{}/events/latest/'.format(self.group.id)
+        response = self.client.get(url, format='json', data={'environment': ['production']})
+
+        assert response.status_code == 200
+        assert response.data['id'] == six.text_type(self.event2.id)

+ 61 - 0
tests/snuba/api/endpoints/test_group_events_oldest.py

@@ -0,0 +1,61 @@
+from __future__ import absolute_import
+
+import six
+
+from datetime import timedelta
+from django.utils import timezone
+
+from sentry import options
+from sentry.models import Group
+from sentry.testutils import APITestCase, SnubaTestCase
+
+
+class GroupEventsOldestTest(APITestCase, SnubaTestCase):
+    def setUp(self):
+        super(GroupEventsOldestTest, self).setUp()
+        self.login_as(user=self.user)
+
+        project = self.create_project()
+        min_ago = (timezone.now() - timedelta(minutes=1)).isoformat()[:19]
+        two_min_ago = (timezone.now() - timedelta(minutes=2)).isoformat()[:19]
+
+        self.event1 = self.store_event(
+            data={
+                'event_id': 'a' * 32,
+                'environment': 'staging',
+                'fingerprint': ['group_1'],
+                'timestamp': two_min_ago
+            },
+            project_id=project.id,
+        )
+
+        self.event2 = self.store_event(
+            data={
+                'event_id': 'b' * 32,
+                'environment': 'production',
+                'fingerprint': ['group_1'],
+                'timestamp': min_ago
+            },
+            project_id=project.id,
+        )
+
+        self.group = Group.objects.first()
+
+    def test_simple(self):
+        url = u'/api/0/issues/{}/events/oldest/'.format(self.group.id)
+        response = self.client.get(url, format='json')
+
+        assert response.status_code == 200
+        assert response.data['id'] == six.text_type(self.event1.id)
+
+    def test_snuba_no_environment(self):
+        options.set('snuba.events-queries.enabled', True)
+        self.test_simple()
+
+    def test_environment(self):
+        options.set('snuba.events-queries.enabled', True)
+        url = u'/api/0/issues/{}/events/oldest/'.format(self.group.id)
+        response = self.client.get(url, format='json', data={'environment': ['production']})
+
+        assert response.status_code == 200
+        assert response.data['id'] == six.text_type(self.event2.id)

+ 52 - 0
tests/snuba/models/test_group.py

@@ -0,0 +1,52 @@
+from __future__ import absolute_import
+
+from datetime import timedelta
+from django.utils import timezone
+from sentry import options
+from sentry.models import Group
+from sentry.testutils import SnubaTestCase
+
+
+class GroupTestSnuba(SnubaTestCase):
+    def test_get_oldest_latest_for_environments(self):
+        options.set('snuba.events-queries.enabled', True)
+        project = self.create_project()
+
+        min_ago = (timezone.now() - timedelta(minutes=1)).isoformat()[:19]
+
+        self.store_event(
+            data={
+                'event_id': 'a' * 32,
+                'environment': 'production',
+                'timestamp': min_ago,
+                'fingerprint': ['group-1']
+            },
+            project_id=project.id
+        )
+        self.store_event(
+            data={
+                'event_id': 'b' * 32,
+                'environment': 'production',
+                'timestamp': min_ago,
+                'fingerprint': ['group-1']
+            },
+            project_id=project.id
+        )
+        self.store_event(
+            data={
+                'event_id': 'c' * 32,
+                'timestamp': min_ago,
+                'fingerprint': ['group-1']
+            },
+            project_id=project.id
+        )
+
+        group = Group.objects.first()
+
+        assert group.get_latest_event_for_environments().event_id == 'c' * 32
+        assert group.get_latest_event_for_environments(['staging']) is None
+        assert group.get_latest_event_for_environments(['production']).event_id == 'b' * 32
+        assert group.get_oldest_event_for_environments().event_id == 'a' * 32
+        assert group.get_oldest_event_for_environments(
+            ['staging', 'production']).event_id == 'a' * 32
+        assert group.get_oldest_event_for_environments(['staging']) is None