Skip to content

Commit

Permalink
feat(events-v2): Add an organization event details endpoint (#13553)
Browse files Browse the repository at this point in the history
This is a prototype of an organization event details endpoint
which adds support for finding a next and previous event based
on a custom list of conditions provided via the events v2 format.
  • Loading branch information
lynnagara committed Jun 10, 2019
1 parent 2ab2dc8 commit 822f85f
Show file tree
Hide file tree
Showing 4 changed files with 418 additions and 0 deletions.
60 changes: 60 additions & 0 deletions src/sentry/api/bases/organization_events.py
Expand Up @@ -2,11 +2,14 @@

from copy import deepcopy
from rest_framework.exceptions import PermissionDenied
import six
from enum import Enum

from sentry import features
from sentry.api.bases import OrganizationEndpoint, OrganizationEventsError
from sentry.api.event_search import get_snuba_query_args, InvalidSearchQuery
from sentry.models.project import Project
from sentry.utils import snuba

# We support 4 "special fields" on the v2 events API which perform some
# additional calculations over aggregated event data
Expand All @@ -28,6 +31,11 @@
ALLOWED_GROUPINGS = frozenset(('issue.id', 'project.id'))


class Direction(Enum):
NEXT = 0
PREV = 1


class OrganizationEventsEndpointBase(OrganizationEndpoint):

def get_snuba_query_args(self, request, organization):
Expand Down Expand Up @@ -137,3 +145,55 @@ def get_snuba_query_args_v2(self, request, organization, params):
raise OrganizationEventsError(
'Boolean search operator OR and AND not allowed in this search.')
return snuba_args

def next_event_id(self, *args):
"""
Returns the next event ID if there is a subsequent event matching the
conditions provided
"""
return self._get_next_or_prev_id(Direction.NEXT, *args)

def prev_event_id(self, *args):
"""
Returns the previous event ID if there is a previous event matching the
conditions provided
"""
return self._get_next_or_prev_id(Direction.PREV, *args)

def _get_next_or_prev_id(self, direction, request, organization, snuba_args, event):
if (direction == Direction.NEXT):
time_condition = [
['timestamp', '>=', event.timestamp],
[['timestamp', '>', event.timestamp], ['event_id', '>', event.event_id]]
]
orderby = ['timestamp', 'event_id']
start = max(event.datetime, snuba_args['start'])
end = snuba_args['end']

else:
time_condition = [
['timestamp', '<=', event.timestamp],
[['timestamp', '<', event.timestamp], ['event_id', '<', event.event_id]]
]
orderby = ['-timestamp', '-event_id']
start = snuba_args['start']
end = min(event.datetime, snuba_args['end'])

conditions = snuba_args['conditions'][:]
conditions.extend(time_condition)

result = snuba.raw_query(
start=start,
end=end,
selected_columns=['event_id'],
conditions=conditions,
filter_keys=snuba_args['filter_keys'],
orderby=orderby,
limit=1,
referrer='api.organization-events.next-or-prev-id',
)

if 'error' in result or len(result['data']) == 0:
return None

return six.text_type(result['data'][0]['event_id'])
119 changes: 119 additions & 0 deletions src/sentry/api/endpoints/organization_event_details.py
@@ -0,0 +1,119 @@
from __future__ import absolute_import

from rest_framework.response import Response
import six
from enum import Enum

from sentry.api.bases import OrganizationEventsEndpointBase, OrganizationEventsError, NoProjects
from sentry import features
from sentry.models import SnubaEvent
from sentry.models.project import Project
from sentry.api.serializers import serialize
from sentry.utils.snuba import raw_query


class EventOrdering(Enum):
LATEST = 0
OLDEST = 1


class OrganizationEventDetailsEndpoint(OrganizationEventsEndpointBase):
def get(self, request, organization, project_slug, event_id):
if not features.has('organizations:events-v2', organization, actor=request.user):
return Response(status=404)

try:
params = self.get_filter_params(request, organization)
snuba_args = self.get_snuba_query_args_v2(request, organization, params)
except OrganizationEventsError as exc:
return Response({'detail': exc.message}, status=400)
except NoProjects:
return Response(status=404)

try:
project = Project.objects.get(
slug=project_slug,
organization_id=organization.id
)
except Project.DoesNotExist:
return Response(status=404)

# We return the requested event if we find a match regardless of whether
# it occurred within the range specified
event = SnubaEvent.objects.from_event_id(event_id, project.id)

if event is None:
return Response({'detail': 'Event not found'}, status=404)

data = serialize(event)

data['nextEventID'] = self.next_event_id(request, organization, snuba_args, event)
data['previousEventID'] = self.prev_event_id(request, organization, snuba_args, event)
data['projectSlug'] = project_slug

return Response(data)


class OrganizationEventsLatestOrOldest(OrganizationEventsEndpointBase):
def get(self, latest_or_oldest, request, organization):
if not features.has('organizations:events-v2', organization, actor=request.user):
return Response(status=404)

try:
params = self.get_filter_params(request, organization)
snuba_args = self.get_snuba_query_args_v2(request, organization, params)
except OrganizationEventsError as exc:
return Response({'detail': exc.message}, status=400)
except NoProjects:
return Response(status=404)

if latest_or_oldest == EventOrdering.LATEST:
orderby = ['-timestamp', '-event_id']
else:
orderby = ['timestamp', 'event_id']

result = raw_query(
start=snuba_args['start'],
end=snuba_args['end'],
selected_columns=SnubaEvent.selected_columns,
conditions=snuba_args['conditions'],
filter_keys=snuba_args['filter_keys'],
orderby=orderby,
limit=2,
referrer='api.organization-event-details-latest-or-oldest',
)

if 'error' in result or len(result['data']) == 0:
return Response({'detail': 'Event not found'}, status=404)

try:
project_id = result['data'][0]['project_id']
project_slug = Project.objects.get(
organization=organization, id=project_id).slug
except Project.DoesNotExist:
project_slug = None

data = serialize(SnubaEvent(result['data'][0]))
data['previousEventID'] = None
data['nextEventID'] = None
data['projectSlug'] = project_slug

if latest_or_oldest == EventOrdering.LATEST and len(result['data']) == 2:
data['previousEventID'] = six.text_type(result['data'][1]['event_id'])

if latest_or_oldest == EventOrdering.OLDEST and len(result['data']) == 2:
data['nextEventID'] = six.text_type(result['data'][1]['event_id'])

return Response(data)


class OrganizationEventDetailsLatestEndpoint(OrganizationEventsLatestOrOldest):
def get(self, request, organization):
return super(OrganizationEventDetailsLatestEndpoint, self).get(
EventOrdering.LATEST, request, organization)


class OrganizationEventDetailsOldestEndpoint(OrganizationEventsLatestOrOldest):
def get(self, request, organization):
return super(OrganizationEventDetailsOldestEndpoint, self).get(
EventOrdering.OLDEST, request, organization)
16 changes: 16 additions & 0 deletions src/sentry/api/urls.py
Expand Up @@ -72,6 +72,7 @@
from .endpoints.organization_discover_saved_queries import OrganizationDiscoverSavedQueriesEndpoint
from .endpoints.organization_discover_saved_query_detail import OrganizationDiscoverSavedQueryDetailEndpoint
from .endpoints.organization_events import OrganizationEventsEndpoint, OrganizationEventsMetaEndpoint, OrganizationEventsStatsEndpoint, OrganizationEventsHeatmapEndpoint
from .endpoints.organization_event_details import OrganizationEventDetailsEndpoint, OrganizationEventDetailsLatestEndpoint, OrganizationEventDetailsOldestEndpoint
from .endpoints.organization_group_index import OrganizationGroupIndexEndpoint
from .endpoints.organization_dashboard_details import OrganizationDashboardDetailsEndpoint
from .endpoints.organization_dashboard_widget_details import OrganizationDashboardWidgetDetailsEndpoint
Expand Down Expand Up @@ -605,6 +606,21 @@
OrganizationEventsEndpoint.as_view(),
name='sentry-api-0-organization-events'
),
url(
r'^organizations/(?P<organization_slug>[^\/]+)/events/(?P<project_slug>[^\/]+):(?P<event_id>(?:\d+|[A-Fa-f0-9]{32}))/$',
OrganizationEventDetailsEndpoint.as_view(),
name='sentry-api-0-organization-event-details'
),
url(
r'^organizations/(?P<organization_slug>[^\/]+)/events/latest/$',
OrganizationEventDetailsLatestEndpoint.as_view(),
name='sentry-api-0-organization-event-details-latest'
),
url(
r'^organizations/(?P<organization_slug>[^\/]+)/events/oldest/$',
OrganizationEventDetailsOldestEndpoint.as_view(),
name='sentry-api-0-organization-event-details-oldest'
),
url(
r'^organizations/(?P<organization_slug>[^\/]+)/events-stats/$',
OrganizationEventsStatsEndpoint.as_view(),
Expand Down

0 comments on commit 822f85f

Please sign in to comment.