Skip to content

feat(event-search): event-tags/heatmap api #13350

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 8 commits into from
May 30, 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
37 changes: 37 additions & 0 deletions src/sentry/api/endpoints/organization_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

from rest_framework.response import Response

from sentry import tagstore
from sentry.api.bases import OrganizationEventsEndpointBase, OrganizationEventsError, NoProjects
from sentry.api.exceptions import ResourceDoesNotExist
from sentry.api.helpers.events import get_direct_hit_response
from sentry.api.paginator import GenericOffsetPaginator
from sentry.api.serializers import EventSerializer, serialize, SimpleEventSerializer
Expand Down Expand Up @@ -137,6 +139,41 @@ def get(self, request, organization):
)


class OrganizationEventsHeatmapEndpoint(OrganizationEventsEndpointBase):
def get(self, request, organization):
try:
snuba_args = self.get_snuba_query_args(request, organization)
except OrganizationEventsError as exc:
return Response({'detail': exc.message}, status=400)
except NoProjects:
return Response({'detail': 'A valid project must be included.'}, status=400)

lookup_keys = [tagstore.prefix_reserved_key(key) for key in request.GET.getlist('keys')]

if not lookup_keys:
return Response({'detail': 'Tag keys must be specified.'}, status=400)
project_ids = snuba_args['filter_keys']['project_id']
Copy link
Member

Choose a reason for hiding this comment

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

Should we also ensure that orgs without global-views don't do multi-project queries here too? Like in #13365

environment_ids = snuba_args['filter_keys'].get('environment_id')

has_global_views = features.has(
'organizations:global-views',
organization,
actor=request.user)

if not has_global_views and len(project_ids) > 1:
return Response({
'detail': 'You cannot view events from multiple projects.'
}, status=400)

try:
tag_key = tagstore.get_group_tag_keys_and_top_values(
Copy link
Member

Choose a reason for hiding this comment

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

I think you mentioned when we were discussing this yesterday. I didn't notice at the time that this has the word group in the name, it's a bit of a misnomer that we then pass None into it. I don't think it's a blocker for this PR, but maybe we should share the core code and have a different method that doesn't require group.

project_ids, None, environment_ids, keys=lookup_keys, get_excluded_tags=True, **snuba_args)
except tagstore.TagKeyNotFound:
raise ResourceDoesNotExist

return Response(serialize(tag_key, request.user))


class OrganizationEventsMetaEndpoint(OrganizationEventsEndpointBase):

def get(self, request, organization):
Expand Down
7 changes: 6 additions & 1 deletion src/sentry/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
from .endpoints.organization_discover_query import OrganizationDiscoverQueryEndpoint
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
from .endpoints.organization_events import OrganizationEventsEndpoint, OrganizationEventsMetaEndpoint, OrganizationEventsStatsEndpoint, OrganizationEventsHeatmapEndpoint
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 @@ -595,6 +595,11 @@
OrganizationEventsStatsEndpoint.as_view(),
name='sentry-api-0-organization-events-stats'
),
url(
r'^organizations/(?P<organization_slug>[^\/]+)/events-heatmap/$',
OrganizationEventsHeatmapEndpoint.as_view(),
name='sentry-api-0-organization-events-heatmap'
),
url(
r'^organizations/(?P<organization_slug>[^\/]+)/events-meta/$',
OrganizationEventsMetaEndpoint.as_view(),
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/tagstore/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ def update_group_tag_key_values_seen(self, project_id, group_ids):
raise NotImplementedError

def get_group_tag_keys_and_top_values(
self, project_id, group_id, environment_ids, keys=None, value_limit=TOP_VALUES_DEFAULT_LIMIT):
self, project_id, group_id, environment_ids, keys=None, value_limit=TOP_VALUES_DEFAULT_LIMIT, **kwargs):

# only the snuba backend supports multi env, and that overrides this method
if environment_ids and len(environment_ids) > 1:
Expand Down
77 changes: 50 additions & 27 deletions src/sentry/tagstore/snuba/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from __future__ import absolute_import

import functools
from collections import defaultdict
from collections import defaultdict, Iterable
from datetime import timedelta
from dateutil.parser import parse as parse_datetime
from django.utils import timezone
Expand Down Expand Up @@ -47,6 +47,10 @@ def fix_tag_value_data(data):
return data


def get_project_list(project_id):
return project_id if isinstance(project_id, Iterable) else [project_id]


class SnubaTagStorage(TagStorage):

# These keys correspond to tags that are typically prefixed with `sentry:`
Expand All @@ -72,7 +76,7 @@ def __get_tag_key(self, project_id, group_id, environment_id, key):
start, end = self.get_time_range()
tag = u'tags[{}]'.format(key)
filters = {
'project_id': [project_id],
'project_id': get_project_list(project_id),
}
if environment_id:
filters['environment'] = [environment_id]
Expand Down Expand Up @@ -100,18 +104,25 @@ def __get_tag_key(self, project_id, group_id, environment_id, key):
return GroupTagKey(group_id=group_id, **data)

def __get_tag_key_and_top_values(self, project_id, group_id, environment_id,
key, limit=3, raise_on_empty=True):
start, end = self.get_time_range()
key, limit=3, raise_on_empty=True, **kwargs):

default_start, default_end = self.get_time_range()
start = kwargs.get('start', default_start)
end = kwargs.get('end', default_end)

tag = u'tags[{}]'.format(key)
filters = {
'project_id': [project_id],
'project_id': get_project_list(project_id),
}
if environment_id:
filters['environment'] = [environment_id]
if group_id is not None:
filters['issue'] = [group_id]
conditions = [[tag, '!=', '']]
aggregations = [
conditions = kwargs.get('conditions', [])
aggregations = kwargs.get('aggregations', [])

conditions.append([tag, '!=', ''])
aggregations += [
['uniq', tag, 'values_seen'],
['count()', '', 'count'],
['min', SEEN_COLUMN, 'first_seen'],
Expand Down Expand Up @@ -153,11 +164,14 @@ def __get_tag_key_and_top_values(self, project_id, group_id, environment_id,

def __get_tag_keys(
self, project_id, group_id, environment_ids, limit=1000, keys=None,
include_values_seen=True,
include_values_seen=True, **kwargs
):
start, end = self.get_time_range()
default_start, default_end = self.get_time_range()
start = kwargs.get('start', default_start)
end = kwargs.get('end', default_end)

return self.__get_tag_keys_for_projects(
[project_id],
get_project_list(project_id),
group_id,
environment_ids,
start,
Expand Down Expand Up @@ -217,7 +231,7 @@ def __get_tag_value(self, project_id, group_id, environment_id, key, value):
start, end = self.get_time_range()
tag = u'tags[{}]'.format(key)
filters = {
'project_id': [project_id],
'project_id': get_project_list(project_id),
}
if environment_id:
filters['environment'] = [environment_id]
Expand All @@ -244,9 +258,11 @@ def __get_tag_value(self, project_id, group_id, environment_id, key, value):
else:
return GroupTagValue(group_id=group_id, **fix_tag_value_data(data))

def get_tag_key(self, project_id, environment_id, key, status=TagKeyStatus.VISIBLE):
def get_tag_key(self, project_id, environment_id, key, status=TagKeyStatus.VISIBLE,
**kwargs):
assert status is TagKeyStatus.VISIBLE
return self.__get_tag_key_and_top_values(project_id, None, environment_id, key)
return self.__get_tag_key_and_top_values(
project_id, None, environment_id, key, **kwargs)

def get_tag_keys(
self, project_id, environment_id, status=TagKeyStatus.VISIBLE,
Expand Down Expand Up @@ -283,10 +299,11 @@ def get_group_tag_key(self, project_id, group_id, environment_id, key):
return self.__get_tag_key_and_top_values(
project_id, group_id, environment_id, key, limit=TOP_VALUES_DEFAULT_LIMIT)

def get_group_tag_keys(self, project_id, group_id, environment_ids, limit=None, keys=None):
def get_group_tag_keys(self, project_id, group_id, environment_ids,
limit=None, keys=None, **kwargs):
return self.__get_tag_keys(
project_id, group_id, environment_ids, limit=limit, keys=keys,
include_values_seen=False,
include_values_seen=False, **kwargs
)

def get_group_tag_value(self, project_id, group_id, environment_id, key, value):
Expand Down Expand Up @@ -359,7 +376,7 @@ def get_group_tag_value_count(self, project_id, group_id, environment_id, key):
start, end = self.get_time_range()
tag = u'tags[{}]'.format(key)
filters = {
'project_id': [project_id],
'project_id': get_project_list(project_id),
'issue': [group_id],
}
if environment_id:
Expand All @@ -376,33 +393,39 @@ def get_top_group_tag_values(self, project_id, group_id,
return tag.top_values

def get_group_tag_keys_and_top_values(
self, project_id, group_id, environment_ids, user=None, keys=None, value_limit=TOP_VALUES_DEFAULT_LIMIT):
self, project_id, group_id, environment_ids, user=None, keys=None, value_limit=TOP_VALUES_DEFAULT_LIMIT,
**kwargs):
# Similar to __get_tag_key_and_top_values except we get the top values
# for all the keys provided. value_limit in this case means the number
# of top values for each key, so the total rows returned should be
# num_keys * limit.
start, end = self.get_time_range()
default_start, default_end = self.get_time_range()
start = kwargs.get('start', default_start)
end = kwargs.get('end', default_end)

# First get totals and unique counts by key.
keys_with_counts = self.get_group_tag_keys(project_id, group_id, environment_ids, keys=keys)
keys_with_counts = self.get_group_tag_keys(
project_id, group_id, environment_ids, keys=keys, start=start, end=end)

# Then get the top values with first_seen/last_seen/count for each
filters = {
'project_id': [project_id],
'project_id': get_project_list(project_id),
}
if environment_ids:
filters['environment'] = environment_ids
if keys is not None:
filters['tags_key'] = keys
if group_id is not None:
filters['issue'] = [group_id]

aggregations = [
conditions = kwargs.get('conditions', [])
aggregations = kwargs.get('aggregations', [])
aggregations += [
['count()', '', 'count'],
['min', SEEN_COLUMN, 'first_seen'],
['max', SEEN_COLUMN, 'last_seen'],
]
conditions = [['tags_key', 'NOT IN', self.EXCLUDE_TAG_KEYS]]
if not kwargs.get('get_excluded_tags'):
conditions.append(['tags_key', 'NOT IN', self.EXCLUDE_TAG_KEYS])

values_by_key = snuba.query(
start, end, ['tags_key', 'tags_value'], conditions, filters, aggregations,
Expand Down Expand Up @@ -434,7 +457,7 @@ def get_group_tag_keys_and_top_values(
def __get_release(self, project_id, group_id, first=True):
start, end = self.get_time_range()
filters = {
'project_id': [project_id],
'project_id': get_project_list(project_id),
}
conditions = [['tags[sentry:release]', 'IS NOT NULL', None]]
if group_id is not None:
Expand Down Expand Up @@ -557,7 +580,7 @@ def get_tag_value_paginator(self, project_id, environment_id, key, query=None,
order_by='-last_seen'):
start, end = self.get_time_range()
return self.get_tag_value_paginator_for_projects(
[project_id],
get_project_list(project_id),
[environment_id] if environment_id else None,
key,
start,
Expand Down Expand Up @@ -627,7 +650,7 @@ def get_tag_value_paginator_for_projects(self, projects, environments, key, star
def get_group_tag_value_iter(self, project_id, group_id, environment_id, key, callbacks=()):
start, end = self.get_time_range()
filters = {
'project_id': [project_id],
'project_id': get_project_list(project_id),
'tags_key': [key],
'issue': [group_id],
}
Expand Down Expand Up @@ -698,7 +721,7 @@ def get_group_event_filter(self, project_id, group_id, environment_ids, tags, st
end = min(end, default_end) if end else default_end

filters = {
'project_id': [project_id],
'project_id': get_project_list(project_id),
'issue': [group_id],
}
if environment_ids:
Expand Down
Loading