Skip to content

Commit

Permalink
feat(performance) Add related issues endpoint (#18344)
Browse files Browse the repository at this point in the history
A first pass at a related issues endpoint to be used by the transaction summary
page to show issues related to that transaction.
  • Loading branch information
evanh authored Apr 24, 2020
1 parent 8df5a78 commit b458e8e
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 2 deletions.
65 changes: 64 additions & 1 deletion src/sentry/api/endpoints/organization_events_meta.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
from __future__ import absolute_import

import re
import six

from rest_framework.response import Response
from rest_framework.exceptions import ParseError

from sentry import search
from sentry.api.base import EnvironmentMixin
from sentry.api.bases import OrganizationEventsEndpointBase, OrganizationEventsError, NoProjects
from sentry.utils import snuba
from sentry.api.helpers.group_index import build_query_params_from_request
from sentry.api.event_search import parse_search_query
from sentry.api.serializers import serialize
from sentry.api.serializers.models.group import GroupSerializer
from sentry.snuba import discover
from sentry.utils import snuba


class OrganizationEventsMetaEndpoint(OrganizationEventsEndpointBase):
Expand All @@ -30,3 +37,59 @@ def get(self, request, organization):
raise ParseError(detail=six.text_type(error))

return Response({"count": result["data"][0]["count"]})


UNESCAPED_QUOTE_RE = re.compile('(?<!\\\\)"')


class OrganizationEventsRelatedIssuesEndpoint(OrganizationEventsEndpointBase, EnvironmentMixin):
def get(self, request, organization):
try:
params = self.get_filter_params(request, organization)
except OrganizationEventsError as e:
return Response({"detail": six.text_type(e)}, status=400)
except NoProjects:
return Response([])

possible_keys = ["transaction"]
lookup_keys = {key: request.query_params.get(key) for key in possible_keys}

if not any(lookup_keys.values()):
return Response(
{
"detail": "Must provide one of {} in order to find related events".format(
possible_keys
)
},
status=400,
)

try:
projects = self.get_projects(request, organization)
query_kwargs = build_query_params_from_request(
request, organization, projects, params.get("environment")
)
query_kwargs["limit"] = 5
try:
# Need to escape quotes in case some "joker" has a transaction with quotes
transaction_name = UNESCAPED_QUOTE_RE.sub('\\"', lookup_keys["transaction"])
parsed_terms = parse_search_query('transaction:"{}"'.format(transaction_name))
except ParseError:
return Response({"detail": "Invalid transaction search"}, status=400)

if query_kwargs.get("search_filters"):
query_kwargs["search_filters"].extend(parsed_terms)
else:
query_kwargs["search_filters"] = parsed_terms

results = search.query(**query_kwargs)
except discover.InvalidSearchQuery as err:
raise ParseError(detail=six.text_type(err))

context = serialize(
list(results),
request.user,
GroupSerializer(environment_func=self._get_environment_func(request, organization.id)),
)

return Response(context)
10 changes: 9 additions & 1 deletion src/sentry/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@
from .endpoints.organization_eventid import EventIdLookupEndpoint
from .endpoints.organization_events import OrganizationEventsEndpoint, OrganizationEventsV2Endpoint
from .endpoints.organization_events_facets import OrganizationEventsFacetsEndpoint
from .endpoints.organization_events_meta import OrganizationEventsMetaEndpoint
from .endpoints.organization_events_meta import (
OrganizationEventsMetaEndpoint,
OrganizationEventsRelatedIssuesEndpoint,
)
from .endpoints.organization_events_stats import OrganizationEventsStatsEndpoint
from .endpoints.organization_group_index import OrganizationGroupIndexEndpoint
from .endpoints.organization_incident_activity_index import (
Expand Down Expand Up @@ -675,6 +678,11 @@
KeyTransactionStatsEndpoint.as_view(),
name="sentry-api-0-organization-key-transactions-stats",
),
url(
r"^(?P<organization_slug>[^\/]+)/related-issues/$",
OrganizationEventsRelatedIssuesEndpoint.as_view(),
name="sentry-api-0-organization-related-issues",
),
# Dashboards
url(
r"^(?P<organization_slug>[^\/]+)/dashboards/$",
Expand Down
172 changes: 172 additions & 0 deletions tests/snuba/api/endpoints/test_organization_events_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,175 @@ def test_out_of_retention(self):
},
)
assert response.status_code == 400


class OrganizationEventsRelatedIssuesEndpoint(APITestCase, SnubaTestCase):
def setUp(self):
super(OrganizationEventsRelatedIssuesEndpoint, self).setUp()

def test_find_related_issue(self):
self.login_as(user=self.user)

project = self.create_project()
event1 = self.store_event(
data={"timestamp": iso_format(before_now(minutes=1)), "transaction": "/beth/sanchez"},
project_id=project.id,
)

url = reverse(
"sentry-api-0-organization-related-issues",
kwargs={"organization_slug": project.organization.slug},
)
response = self.client.get(url, {"transaction": "/beth/sanchez"}, format="json")

assert response.status_code == 200, response.content
assert len(response.data) == 1
assert response.data[0]["shortId"] == event1.group.qualified_short_id
assert int(response.data[0]["id"]) == event1.group_id

def test_related_issues_no_transaction(self):
self.login_as(user=self.user)

project = self.create_project()
self.store_event(
data={"timestamp": iso_format(before_now(minutes=1)), "transaction": "/beth/sanchez"},
project_id=project.id,
)

url = reverse(
"sentry-api-0-organization-related-issues",
kwargs={"organization_slug": project.organization.slug},
)
response = self.client.get(url, format="json")

assert response.status_code == 400, response.content
assert (
response.data["detail"]
== "Must provide one of ['transaction'] in order to find related events"
)

def test_related_issues_no_matching_groups(self):
self.login_as(user=self.user)

project = self.create_project()
self.store_event(
data={"timestamp": iso_format(before_now(minutes=1)), "transaction": "/beth/sanchez"},
project_id=project.id,
)

url = reverse(
"sentry-api-0-organization-related-issues",
kwargs={"organization_slug": project.organization.slug},
)
response = self.client.get(url, {"transaction": "/morty/sanchez"}, format="json")

assert response.status_code == 200, response.content
assert len(response.data) == 0

def test_related_issues_only_issues_in_date(self):
self.login_as(user=self.user)

project = self.create_project()
self.store_event(
data={
"event_id": "a" * 32,
"timestamp": iso_format(before_now(days=2)),
"transaction": "/beth/sanchez",
},
project_id=project.id,
)
event2 = self.store_event(
data={
"event_id": "b" * 32,
"timestamp": iso_format(before_now(minutes=1)),
"transaction": "/beth/sanchez",
},
project_id=project.id,
)

url = reverse(
"sentry-api-0-organization-related-issues",
kwargs={"organization_slug": project.organization.slug},
)
response = self.client.get(
url, {"transaction": "/beth/sanchez", "statsPeriod": "24h"}, format="json"
)

assert response.status_code == 200, response.content
assert len(response.data) == 1
assert response.data[0]["shortId"] == event2.group.qualified_short_id
assert int(response.data[0]["id"]) == event2.group_id

def test_related_issues_transactions_from_different_projects(self):
self.login_as(user=self.user)

project1 = self.create_project()
project2 = self.create_project()
event1 = self.store_event(
data={
"event_id": "a" * 32,
"timestamp": iso_format(before_now(minutes=1)),
"transaction": "/beth/sanchez",
},
project_id=project1.id,
)
self.store_event(
data={
"event_id": "b" * 32,
"timestamp": iso_format(before_now(minutes=1)),
"transaction": "/beth/sanchez",
},
project_id=project2.id,
)

url = reverse(
"sentry-api-0-organization-related-issues",
kwargs={"organization_slug": project1.organization.slug},
)
response = self.client.get(
url, {"transaction": "/beth/sanchez", "project": project1.id}, format="json",
)

assert response.status_code == 200, response.content
assert len(response.data) == 1
assert response.data[0]["shortId"] == event1.group.qualified_short_id
assert int(response.data[0]["id"]) == event1.group_id

def test_related_issues_transactions_with_quotes(self):
self.login_as(user=self.user)

project = self.create_project()
event = self.store_event(
data={
"event_id": "a" * 32,
"timestamp": iso_format(before_now(minutes=1)),
"transaction": '/beth/"sanchez"',
},
project_id=project.id,
)

url = reverse(
"sentry-api-0-organization-related-issues",
kwargs={"organization_slug": project.organization.slug},
)
response = self.client.get(
url, {"transaction": '/beth/"sanchez"', "project": project.id}, format="json",
)

assert response.status_code == 200, response.content
assert len(response.data) == 1
assert response.data[0]["shortId"] == event.group.qualified_short_id
assert int(response.data[0]["id"]) == event.group_id

url = reverse(
"sentry-api-0-organization-related-issues",
kwargs={"organization_slug": project.organization.slug},
)
response = self.client.get(
url, {"transaction": '/beth/\\"sanchez\\"', "project": project.id}, format="json",
)

assert response.status_code == 200, response.content
assert len(response.data) == 1
assert response.data[0]["shortId"] == event.group.qualified_short_id
assert int(response.data[0]["id"]) == event.group_id

0 comments on commit b458e8e

Please sign in to comment.