diff --git a/src/sentry/api/endpoints/organization_events_meta.py b/src/sentry/api/endpoints/organization_events_meta.py index 41e37ebfd3fe23..486c82c78ca5a9 100644 --- a/src/sentry/api/endpoints/organization_events_meta.py +++ b/src/sentry/api/endpoints/organization_events_meta.py @@ -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): @@ -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('(?[^\/]+)/related-issues/$", + OrganizationEventsRelatedIssuesEndpoint.as_view(), + name="sentry-api-0-organization-related-issues", + ), # Dashboards url( r"^(?P[^\/]+)/dashboards/$", diff --git a/tests/snuba/api/endpoints/test_organization_events_meta.py b/tests/snuba/api/endpoints/test_organization_events_meta.py index 8b141746f0eca3..aa65e86797bc4b 100644 --- a/tests/snuba/api/endpoints/test_organization_events_meta.py +++ b/tests/snuba/api/endpoints/test_organization_events_meta.py @@ -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