Browse Source

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.
Armen Zambrano G 10 months ago
parent
commit
e34e4123f9

+ 9 - 2
src/sentry/api/endpoints/issues/related_issues.py

@@ -35,5 +35,12 @@ class RelatedIssuesEndpoint(GroupEndpoint):
         """
         # 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)

+ 3 - 1
src/sentry/issues/related/same_root_cause.py

@@ -7,7 +7,9 @@ from sentry.models.group import Group
 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

+ 42 - 5
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.snuba.referrer import Referrer
 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

+ 6 - 2
src/sentry/testutils/cases.py

@@ -3406,7 +3406,11 @@ class TraceTestCase(SpanTestCase):
 
         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 @@ class TraceTestCase(SpanTestCase):
         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)

+ 32 - 5
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 @@ class RelatedIssuesTest(APITestCase, SnubaTestCase, TraceTestCase):
         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,
+            },
         }