Browse Source

feat(api): Implement new search parser in group events endpoint

This endpoint was using the old parser when searching snuba, and so we weren't able to use new
search features. This converts the endpoint to use the new parser.

We change one piece of existing behaviour here: Previously, if a query looked like an event id we'd
search for it in the message and as an exact id match. I've changed this to match our existing
behaviour elsewhere, which just does an exact match and if we find a result we set
`X-Sentry-Direct-Hit`.
Dan Fuller 6 years ago
parent
commit
66f11628fa

+ 17 - 22
src/sentry/api/endpoints/group_events.py

@@ -12,8 +12,10 @@ from functools import partial
 from sentry import options, quotas, tagstore
 from sentry.api.base import DocSection, EnvironmentMixin
 from sentry.api.bases import GroupEndpoint
+from sentry.api.event_search import get_snuba_query_args
 from sentry.api.exceptions import ResourceDoesNotExist
 from sentry.api.helpers.environments import get_environments
+from sentry.api.helpers.events import get_direct_hit_response
 from sentry.api.serializers import serialize, SimpleEventSerializer
 from sentry.api.paginator import DateTimePaginator, GenericOffsetPaginator
 from sentry.api.utils import get_date_range_from_params
@@ -73,37 +75,30 @@ class GroupEventsEndpoint(GroupEndpoint, EnvironmentMixin):
         return backend(request, group, environments, query, tags, start, end)
 
     def _get_events_snuba(self, request, group, environments, query, tags, start, end):
-        conditions = []
-        if query:
-            msg_substr = ['positionCaseInsensitive', ['message', "'%s'" % (query,)]]
-            message_condition = [msg_substr, '!=', 0]
-            if is_event_id(query):
-                or_condition = [message_condition, ['event_id', '=', query]]
-                conditions.append(or_condition)
-            else:
-                conditions.append(message_condition)
-
-        if tags:
-            for tag_name, tag_val in tags.items():
-                operator = 'IN' if isinstance(tag_val, list) else '='
-                conditions.append([u'tags[{}]'.format(tag_name), operator, tag_val])
-
         default_end = timezone.now()
         default_start = default_end - timedelta(days=90)
+        params = {
+            'issue.id': [group.id],
+            'project_id': [group.project_id],
+            'start': start if start else default_start,
+            'end': end if end else default_end
+        }
+        direct_hit_resp = get_direct_hit_response(request, query, params, 'api.group-events')
+        if direct_hit_resp:
+            return direct_hit_resp
+
+        if environments:
+            params['environment'] = [env.name for env in environments]
+
+        snuba_args = get_snuba_query_args(request.GET.get('query', None), params)
 
         data_fn = partial(
             # extract 'data' from raw_query result
             lambda *args, **kwargs: raw_query(*args, **kwargs)['data'],
-            start=max(start, default_start) if start else default_start,
-            end=min(end, default_end) if end else default_end,
-            conditions=conditions,
-            filter_keys={
-                'project_id': [group.project_id],
-                'issue': [group.id]
-            },
             selected_columns=SnubaEvent.selected_columns,
             orderby='-timestamp',
             referrer='api.group-events',
+            **snuba_args
         )
 
         serializer = SimpleEventSerializer()

+ 14 - 22
src/sentry/api/endpoints/organization_events.py

@@ -7,14 +7,13 @@ from functools import partial
 from rest_framework.response import Response
 
 from sentry.api.bases import OrganizationEventsEndpointBase, OrganizationEventsError, NoProjects
+from sentry.api.helpers.events import get_direct_hit_response
 from sentry.api.paginator import GenericOffsetPaginator
 from sentry.api.serializers import serialize, SimpleEventSerializer
 from sentry.api.serializers.snuba import SnubaTSResultSerializer
 from sentry.models import SnubaEvent
 from sentry.utils.dates import parse_stats_period
 from sentry.utils.snuba import raw_query
-from sentry.utils.validators import is_event_id
-from sentry.api.event_search import get_snuba_query_args
 
 SnubaTSResult = namedtuple('SnubaTSResult', ('data', 'start', 'end', 'rollup'))
 
@@ -24,26 +23,19 @@ class OrganizationEventsEndpoint(OrganizationEventsEndpointBase):
     def get(self, request, organization):
         # Check for a direct hit on event ID
         query = request.GET.get('query', '').strip()
-        if is_event_id(query):
-            try:
-                snuba_args = get_snuba_query_args(
-                    query=u'id:{}'.format(query),
-                    params=self.get_filter_params(request, organization))
-
-                results = raw_query(
-                    selected_columns=SnubaEvent.selected_columns,
-                    referrer='api.organization-events',
-                    **snuba_args
-                )['data']
-
-                if len(results) == 1:
-                    response = Response(
-                        serialize([SnubaEvent(row) for row in results], request.user)
-                    )
-                    response['X-Sentry-Direct-Hit'] = '1'
-                    return response
-            except (OrganizationEventsError, NoProjects):
-                pass
+
+        try:
+            direct_hit_resp = get_direct_hit_response(
+                request,
+                query,
+                self.get_filter_params(request, organization),
+                'api.organization-events'
+            )
+        except (OrganizationEventsError, NoProjects):
+            pass
+        else:
+            if direct_hit_resp:
+                return direct_hit_resp
 
         try:
             snuba_args = self.get_snuba_query_args(request, organization)

+ 1 - 1
src/sentry/api/event_search.py

@@ -513,7 +513,7 @@ def get_snuba_query_args(query=None, params=None):
 
         if snuba_name in ('start', 'end'):
             kwargs[snuba_name] = _filter.value.value
-        elif snuba_name == 'project_id':
+        elif snuba_name in ('project_id', 'issue'):
             kwargs['filter_keys'][snuba_name] = _filter.value.value
         else:
             converted_filter = convert_search_filter_to_snuba_query(_filter)

+ 33 - 0
src/sentry/api/helpers/events.py

@@ -0,0 +1,33 @@
+from __future__ import absolute_import
+
+from rest_framework.response import Response
+
+from sentry.api.event_search import get_snuba_query_args
+from sentry.models import SnubaEvent
+from sentry.utils.snuba import raw_query
+from sentry.utils.validators import is_event_id
+from sentry.api.serializers import serialize
+
+
+def get_direct_hit_response(request, query, snuba_params, referrer):
+    """
+    Checks whether a query is a direct hit for an event, and if so returns
+    a response. Otherwise returns None
+    """
+    if is_event_id(query):
+        snuba_args = get_snuba_query_args(
+            query=u'id:{}'.format(query),
+            params=snuba_params)
+
+        results = raw_query(
+            selected_columns=SnubaEvent.selected_columns,
+            referrer=referrer,
+            **snuba_args
+        )['data']
+
+        if len(results) == 1:
+            response = Response(
+                serialize([SnubaEvent(row) for row in results], request.user)
+            )
+            response['X-Sentry-Direct-Hit'] = '1'
+            return response

+ 45 - 9
tests/snuba/api/endpoints/test_group_events.py

@@ -67,7 +67,7 @@ class GroupEventsTest(APITestCase, SnubaTestCase):
         )
         event_2 = self.create_event(
             event_id='b' * 32,
-            datetime=self.min_ago,
+            datetime=self.min_ago - timedelta(minutes=1),
             group=group,
             tags={
                 'bar': 'biz',
@@ -76,48 +76,51 @@ class GroupEventsTest(APITestCase, SnubaTestCase):
 
         url = u'/api/0/issues/{}/events/'.format(group.id)
         response = self.client.get(url + '?query=foo:baz', format='json')
-
         assert response.status_code == 200, response.content
         assert len(response.data) == 1
         assert response.data[0]['eventID'] == six.text_type(event_1.event_id)
 
-        response = self.client.get(url + '?query=bar:biz', format='json')
+        response = self.client.get(url + '?query=!foo:baz', format='json')
+        assert response.status_code == 200, response.content
+        assert len(response.data) == 1
+        assert response.data[0]['eventID'] == six.text_type(event_2.event_id)
 
+        response = self.client.get(url + '?query=bar:biz', format='json')
         assert response.status_code == 200, response.content
         assert len(response.data) == 1
         assert response.data[0]['eventID'] == six.text_type(event_2.event_id)
 
         response = self.client.get(url + '?query=bar:biz%20foo:baz', format='json')
-
         assert response.status_code == 200, response.content
         assert len(response.data) == 0
 
         response = self.client.get(url + '?query=bar:buz%20foo:baz', format='json')
-
         assert response.status_code == 200, response.content
         assert len(response.data) == 1
         assert response.data[0]['eventID'] == six.text_type(event_1.event_id)
 
         response = self.client.get(url + '?query=bar:baz', format='json')
-
         assert response.status_code == 200, response.content
         assert len(response.data) == 0
 
         response = self.client.get(url + '?query=a:b', format='json')
-
         assert response.status_code == 200, response.content
         assert len(response.data) == 0
 
         response = self.client.get(url + '?query=bar:b', format='json')
-
         assert response.status_code == 200, response.content
         assert len(response.data) == 0
 
         response = self.client.get(url + '?query=bar:baz', format='json')
-
         assert response.status_code == 200, response.content
         assert len(response.data) == 0
 
+        response = self.client.get(url + '?query=!bar:baz', format='json')
+        assert response.status_code == 200, response.content
+        assert len(response.data) == 2
+        assert response.data[0]['eventID'] == six.text_type(event_1.event_id)
+        assert response.data[1]['eventID'] == six.text_type(event_2.event_id)
+
     def test_search_event_by_id(self):
         self.login_as(user=self.user)
 
@@ -327,3 +330,36 @@ class GroupEventsTest(APITestCase, SnubaTestCase):
         assert response.status_code == 200, response.content
         assert len(response.data) == 1
         assert response.data[0]['eventID'] == six.text_type(event_2.event_id)
+
+    def test_multiple_group(self):
+        self.login_as(user=self.user)
+
+        event_1 = self.store_event(
+            data={
+                'fingerprint': ['group_1'],
+                'event_id': 'a' * 32,
+                'message': 'foo',
+                'timestamp': self.min_ago.isoformat()[:19],
+            },
+            project_id=self.project.id,
+        )
+        event_2 = self.store_event(
+            data={
+                'fingerprint': ['group_2'],
+                'event_id': 'b' * 32,
+                'message': 'group2',
+                'timestamp': self.min_ago.isoformat()[:19],
+            },
+            project_id=self.project.id,
+        )
+
+        for event in (event_1, event_2):
+            url = u'/api/0/issues/{}/events/'.format(event.group.id)
+            response = self.client.get(url, format='json')
+            assert response.status_code == 200, response.content
+            assert len(response.data) == 1, response.data
+            assert sorted(map(lambda x: x['eventID'], response.data)) == sorted(
+                [
+                    six.text_type(event.event_id),
+                ]
+            )