Skip to content

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

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 10 commits into from
Jun 10, 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
60 changes: 60 additions & 0 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 @@ -26,6 +29,11 @@
}


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


class OrganizationEventsEndpointBase(OrganizationEndpoint):

def get_snuba_query_args(self, request, organization):
Expand Down Expand Up @@ -108,3 +116,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)
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}))/$',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markstory Is this the groupSlug value you were thinking about passing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do this, or we could use a / instead of a :. I was using : in the frontend eventSlug query parameter as it was a simple way to pack two values into a single query string argument making back/forward navigation simpler.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I switched this to a / in #13568

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to stick with the :. / implies a subresource to me and since we can't have an eventId without a projectSlug I think it might be a bit unexpected.

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