Skip to content

feat(events-v2): Add organization event details endpoint #13620

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 59 additions & 7 deletions src/sentry/api/bases/organization_events.py
Original file line number Diff line number Diff line change
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 @@ -25,7 +28,10 @@
},
}

ALLOWED_GROUPINGS = frozenset(('issue.id', 'project.id'))

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


class OrganizationEventsEndpointBase(OrganizationEndpoint):
Expand Down Expand Up @@ -77,12 +83,6 @@ def get_snuba_query_args_v2(self, request, organization, params):
aggregations = []
groupby = request.GET.getlist('groupby')

if not fields and not groupby:
raise OrganizationEventsError('No fields or groupings provided')

if any(field for field in groupby if field not in ALLOWED_GROUPINGS):
raise OrganizationEventsError('Invalid groupby value requested')

if fields:
# If project.name is requested, get the project.id from Snuba so we
# can use this to look up the name in Sentry
Expand Down Expand Up @@ -137,3 +137,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
Original file line number Diff line number Diff line change
@@ -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)
14 changes: 14 additions & 0 deletions src/sentry/api/endpoints/organization_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from sentry import features
from sentry.models.project import Project

ALLOWED_GROUPINGS = frozenset(('issue.id', 'project.id'))


class OrganizationEventsEndpoint(OrganizationEventsEndpointBase):

Expand Down Expand Up @@ -78,6 +80,18 @@ def get_v2(self, request, organization):
try:
params = self.get_filter_params(request, organization)
snuba_args = self.get_snuba_query_args_v2(request, organization, params)

fields = snuba_args.get('selected_columns')
groupby = snuba_args.get('groupby', [])

if not fields and not groupby:
return Response({'detail': 'No fields or groupings provided'}, status=400)

if any(field for field in groupby if field not in ALLOWED_GROUPINGS):
message = ('Invalid groupby value requested. Allowed values are ' +
', '.join(ALLOWED_GROUPINGS))
return Response({'detail': message}, status=400)

except OrganizationEventsError as exc:
return Response({'detail': exc.message}, status=400)
except NoProjects:
Expand Down
16 changes: 16 additions & 0 deletions src/sentry/api/urls.py
Original file line number Diff line number Diff line change
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
Loading