From e34e4123f9600b083ad6d03372d1f47ee6fd0b9b Mon Sep 17 00:00:00 2001 From: Armen Zambrano G <44410+armenzg@users.noreply.github.com> Date: Wed, 15 May 2024 11:58:20 -0400 Subject: [PATCH] feat(related_issues): Support passing an event_id to the endpoint (#70878) This enables showing trace connected issues from the issue details page for the event we're currently viewing. --- .../api/endpoints/issues/related_issues.py | 11 ++++- src/sentry/issues/related/same_root_cause.py | 4 +- src/sentry/issues/related/trace_connected.py | 47 +++++++++++++++++-- src/sentry/testutils/cases.py | 8 +++- .../endpoints/issues/test_related_issues.py | 37 +++++++++++++-- 5 files changed, 92 insertions(+), 15 deletions(-) diff --git a/src/sentry/api/endpoints/issues/related_issues.py b/src/sentry/api/endpoints/issues/related_issues.py index d5606ab2a9a35d..17282b40991338 100644 --- a/src/sentry/api/endpoints/issues/related_issues.py +++ b/src/sentry/api/endpoints/issues/related_issues.py @@ -35,5 +35,12 @@ def get(self, request: Request, group: Group) -> Response: """ # The type of related issues to retrieve. Can be either `same_root_cause` or `trace_connected`. related_type = request.query_params["type"] - data, meta = RELATED_ISSUES_ALGORITHMS[related_type](group) - return Response({"type": related_type, "data": data, "meta": meta}) + extra_args = { + "event_id": request.query_params.get("event_id"), + "project_id": request.query_params.get("project_id"), + } + try: + data, meta = RELATED_ISSUES_ALGORITHMS[related_type](group, extra_args) + return Response({"type": related_type, "data": data, "meta": meta}) + except AssertionError: + return Response({}, status=400) diff --git a/src/sentry/issues/related/same_root_cause.py b/src/sentry/issues/related/same_root_cause.py index aa822488bc80b3..c0fd2104b8f088 100644 --- a/src/sentry/issues/related/same_root_cause.py +++ b/src/sentry/issues/related/same_root_cause.py @@ -7,7 +7,9 @@ from sentry.utils.query import RangeQuerySetWrapper -def same_root_cause_analysis(group: Group) -> tuple[list[int], dict[str, str]]: +def same_root_cause_analysis( + group: Group, _extra_args: dict[str, str | None] | None = None +) -> tuple[list[int], dict[str, str]]: """Analyze and create a group set if the group was caused by the same root cause.""" # Querying the data field (which is a GzippedDictField) cannot be done via # Django's ORM, thus, we do so via compare_groups diff --git a/src/sentry/issues/related/trace_connected.py b/src/sentry/issues/related/trace_connected.py index 1fdf88876cd176..98c2ae098f1f56 100644 --- a/src/sentry/issues/related/trace_connected.py +++ b/src/sentry/issues/related/trace_connected.py @@ -1,7 +1,9 @@ # Module to evaluate if other errors happened in the same trace. # # Refer to README in module for more details. +from sentry import eventstore from sentry.api.utils import default_start_end_dates +from sentry.eventstore.models import GroupEvent from sentry.models.group import Group from sentry.models.project import Project from sentry.search.events.builder import QueryBuilder @@ -11,12 +13,47 @@ from sentry.utils.snuba import bulk_snuba_queries -def trace_connected_analysis(group: Group) -> tuple[list[int], dict[str, str]]: +# If we drop trace connected issues from similar issues we can stop using the group +def trace_connected_analysis( + group: Group, extra_args: dict[str, str | None] | None = None +) -> tuple[list[int], dict[str, str]]: """Determine if the group has a trace connected to it and return other issues that were part of it.""" - event = group.get_recommended_event_for_environments() - if not event or event.trace_id is None: - return [], {} + if extra_args is None: + extra_args = {} + issues: list[int] = [] + meta: dict[str, str] = {} + event_id = extra_args.get("event_id") + project_id = extra_args.get("project_id") + if event_id: + # If we are passing an specific event_id, we need to get the project_id + assert project_id is not None + event = eventstore.backend.get_event_by_id(project_id, event_id, group_id=group.id) + # If we are requesting an specific event, we want to be notified with an error + assert event is not None + # This ensures that the event is actually part of the group and we are notified + assert event.group_id == group.id + else: + # If we drop trace connected issues from similar issues we can remove this + event = group.get_recommended_event_for_environments() + + if event: + issues, meta = trace_connected_issues(event) + else: + meta["error"] = "No event found for group." + + return issues, meta + + +def trace_connected_issues(event: GroupEvent) -> tuple[list[int], dict[str, str]]: + meta = {"event_id": event.event_id} + if event.trace_id: + meta["trace_id"] = event.trace_id + else: + meta["error"] = "No trace_id found in event." + return [], meta + + group = event.group org_id = group.project.organization_id # XXX: Test without a list and validate the data type project_ids = list(Project.objects.filter(organization_id=org_id).values_list("id", flat=True)) @@ -41,4 +78,4 @@ def trace_connected_analysis(group: Group) -> tuple[list[int], dict[str, str]]: if datum["issue.id"] != group.id # Exclude itself } ) - return transformed_results, {"event_id": event.event_id, "trace_id": event.trace_id} + return transformed_results, meta diff --git a/src/sentry/testutils/cases.py b/src/sentry/testutils/cases.py index f05fc9b02b137b..c686d2aecd7677 100644 --- a/src/sentry/testutils/cases.py +++ b/src/sentry/testutils/cases.py @@ -3406,7 +3406,11 @@ def convert_event_data_to_span(self, event: Event) -> dict[str, Any]: return span_data - def load_errors(self, project: Project, span_id: str) -> list[Event]: + def load_errors( + self, + project: Project, + span_id: str | None = None, + ) -> list[Event]: """Generates trace with errors across two projects.""" start, _ = self.get_start_end_from_day_ago(1000) error_data = load_data( @@ -3416,7 +3420,7 @@ def load_errors(self, project: Project, span_id: str) -> list[Event]: error_data["contexts"]["trace"] = { "type": "trace", "trace_id": self.trace_id, - "span_id": span_id, + "span_id": span_id or uuid4().hex[:16], } error_data["level"] = "fatal" error = self.store_event(error_data, project_id=project.id) diff --git a/tests/sentry/api/endpoints/issues/test_related_issues.py b/tests/sentry/api/endpoints/issues/test_related_issues.py index 1d2a31acf56549..811e757fd590dd 100644 --- a/tests/sentry/api/endpoints/issues/test_related_issues.py +++ b/tests/sentry/api/endpoints/issues/test_related_issues.py @@ -1,5 +1,3 @@ -from uuid import uuid4 - from django.urls import reverse from sentry.testutils.cases import APITestCase, SnubaTestCase, TraceTestCase @@ -47,20 +45,49 @@ def test_same_root_related_issues(self) -> None: assert response.json() == {"type": "same_root_cause", "data": [5], "meta": {}} def test_trace_connected_errors(self) -> None: - error_event, _, another_proj_event = self.load_errors(self.project, uuid4().hex[:16]) + error_event, _, another_proj_event = self.load_errors(self.project) group = error_event.group - self.group_id = error_event.group_id # type: ignore[assignment] recommended_event = group.get_recommended_event_for_environments() # type: ignore[union-attr] assert recommended_event is not None # It helps with typing + # This assertion ensures that the behaviour is different from the next test + assert recommended_event.event_id != another_proj_event.event_id + # This asserts that there are two issues which belong to the same trace assert error_event.group_id != another_proj_event.group_id assert error_event.project.id != another_proj_event.project.id assert error_event.trace_id == another_proj_event.trace_id + # This sets the group_id to the one we want to query about + self.group_id = error_event.group_id # type: ignore[assignment] response = self.get_success_response(qs_params={"type": "trace_connected"}) assert response.json() == { "type": "trace_connected", # This is the other issue in the trace that it is not itself "data": [another_proj_event.group_id], - "meta": {"event_id": recommended_event.event_id, "trace_id": error_event.trace_id}, + "meta": { + "event_id": recommended_event.event_id, + "trace_id": error_event.trace_id, + }, + } + + def test_trace_connected_errors_specific_event(self) -> None: + error_event, _, another_proj_event = self.load_errors(self.project) + + # This sets the group_id to the one we want to query about + self.group_id = another_proj_event.group_id # type: ignore[assignment] + response = self.get_success_response( + qs_params={ + "type": "trace_connected", + "event_id": another_proj_event.event_id, + "project_id": another_proj_event.project_id, + } + ) + assert response.json() == { + "type": "trace_connected", + # This is the other issue in the trace that it is not itself + "data": [error_event.group_id], + "meta": { + "event_id": another_proj_event.event_id, + "trace_id": another_proj_event.trace_id, + }, }