Skip to content

Commit 34d9222

Browse files
authored
feat(related_issues): Trace connected errors (#69237)
Given a group, we look for the recommended event and search for any other errors in the trace. These trace-connected groups will be shown under the Related Issues tab. See the current look (more UI changes will be needed). <img width="933" alt="image" src="https://github.com/getsentry/sentry/assets/44410/b3f774d7-dc71-40b9-b144-65f143b6e981">
1 parent 1766e91 commit 34d9222

File tree

8 files changed

+96
-22
lines changed

8 files changed

+96
-22
lines changed

src/sentry/eventstore/models.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ def data(self) -> NodeData:
9090
def data(self, value: NodeData | Mapping[str, Any]):
9191
pass
9292

93+
@property
94+
def trace_id(self) -> str | None:
95+
ret_value = None
96+
if self.data:
97+
ret_value = self.data.get("contexts", {}).get("trace", {}).get("trace_id")
98+
return ret_value
99+
93100
@property
94101
def platform(self) -> str | None:
95102
column = self._get_column_name(Columns.PLATFORM)
@@ -716,7 +723,7 @@ def __init__(
716723
data: NodeData,
717724
snuba_data: Mapping[str, Any] | None = None,
718725
occurrence: IssueOccurrence | None = None,
719-
):
726+
) -> None:
720727
super().__init__(project_id, event_id, snuba_data=snuba_data)
721728
self.group = group
722729
self.data = data

src/sentry/issues/related/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
from sentry.models.group import Group
44

55
from .same_root_cause import same_root_cause_analysis
6+
from .trace_connected import trace_connected_analysis
67

78
__all__ = ["find_related_issues"]
89

910
RELATED_ISSUES_ALGORITHMS = {
1011
"same_root_cause": same_root_cause_analysis,
12+
"trace_connected": trace_connected_analysis,
1113
}
1214

1315

src/sentry/issues/related/same_root_cause.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ def same_root_cause_analysis(group: Group) -> list[int]:
1111
"""Analyze and create a group set if the group was caused by the same root cause."""
1212
# Querying the data field (which is a GzippedDictField) cannot be done via
1313
# Django's ORM, thus, we do so via compare_groups
14-
project_groups = RangeQuerySetWrapper(Group.objects.filter(project=group.project_id), limit=100)
14+
project_groups = RangeQuerySetWrapper(
15+
Group.objects.filter(project=group.project_id).exclude(id=group.id),
16+
limit=100,
17+
)
1518
same_error_type_groups = [g.id for g in project_groups if compare_groups(g, group)]
1619
return same_error_type_groups or []
1720

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Module to evaluate if other errors happened in the same trace.
2+
#
3+
# Refer to README in module for more details.
4+
from sentry.api.utils import default_start_end_dates
5+
from sentry.models.group import Group
6+
from sentry.models.project import Project
7+
from sentry.search.events.builder import QueryBuilder
8+
from sentry.search.events.types import QueryBuilderConfig
9+
from sentry.snuba.dataset import Dataset
10+
from sentry.snuba.referrer import Referrer
11+
from sentry.utils.snuba import bulk_snuba_queries
12+
13+
14+
def trace_connected_analysis(group: Group) -> list[int]:
15+
event = group.get_recommended_event_for_environments()
16+
if not event or event.trace_id is None:
17+
return []
18+
19+
org_id = group.project.organization_id
20+
# XXX: Test without a list and validate the data type
21+
project_ids = list(Project.objects.filter(organization_id=org_id).values_list("id", flat=True))
22+
start, end = default_start_end_dates() # Today to 90 days back
23+
query = QueryBuilder(
24+
Dataset.Events,
25+
{"start": start, "end": end, "organization_id": org_id, "project_id": project_ids},
26+
query=f"trace:{event.trace_id}",
27+
selected_columns=["id", "issue.id"],
28+
# Don't add timestamp to this orderby as snuba will have to split the time range up and make multiple queries
29+
orderby=["id"],
30+
limit=100,
31+
config=QueryBuilderConfig(auto_fields=False),
32+
)
33+
results = bulk_snuba_queries(
34+
[query.get_snql_query()], referrer=Referrer.API_ISSUES_RELATED_ISSUES.value
35+
)
36+
transformed_results = list(
37+
{
38+
datum["issue.id"]
39+
for datum in query.process_results(results[0])["data"]
40+
if datum["issue.id"] != group.id # Exclude itself
41+
}
42+
)
43+
return transformed_results

src/sentry/snuba/referrer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class Referrer(Enum):
9898
API_GROUP_HASHES_LEVELS_GET_LEVELS_OVERVIEW = "api.group_hashes_levels.get_levels_overview"
9999
API_GROUP_HASHES = "api.group-hashes"
100100
API_ISSUES_ISSUE_EVENTS = "api.issues.issue_events"
101+
API_ISSUES_RELATED_ISSUES = "api.issues.related_issues"
101102
API_ORGANIZATION_EVENT_STATS_FIND_TOPN = "api.organization-event-stats.find-topn"
102103
API_ORGANIZATION_EVENT_STATS_METRICS_ENHANCED = "api.organization-event-stats.metrics-enhanced"
103104
API_ORGANIZATION_EVENT_STATS = "api.organization-event-stats"

src/sentry/testutils/cases.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3487,8 +3487,8 @@ def populate_project1(self) -> None:
34873487
)
34883488
]
34893489

3490-
def load_errors(self) -> tuple[Event, Event]:
3491-
"""Generates 2 events for gen1 projects."""
3490+
def load_errors(self) -> tuple[Event, Event, Event]:
3491+
"""Generates trace with errors across two projects."""
34923492
if not hasattr(self, "gen1_project"):
34933493
self.populate_project1()
34943494
start, _ = self.get_start_end_from_day_ago(1000)
@@ -3505,7 +3505,10 @@ def load_errors(self) -> tuple[Event, Event]:
35053505
error = self.store_event(error_data, project_id=self.gen1_project.id)
35063506
error_data["level"] = "warning"
35073507
error1 = self.store_event(error_data, project_id=self.gen1_project.id)
3508-
return error, error1
3508+
3509+
another_project = self.create_project(organization=self.organization)
3510+
another_project_error = self.store_event(error_data, project_id=another_project.id)
3511+
return error, error1, another_project_error
35093512

35103513
def load_default(self) -> Event:
35113514
start, _ = self.get_start_end_from_day_ago(1000)
Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,38 @@
1-
from typing import Any
2-
31
from django.urls import reverse
42

5-
from sentry.testutils.cases import APITestCase
3+
from sentry.testutils.cases import APITestCase, SnubaTestCase, TraceTestCase
64

75

8-
class RelatedIssuesTest(APITestCase):
6+
class RelatedIssuesTest(APITestCase, SnubaTestCase, TraceTestCase):
97
endpoint = "sentry-api-0-issues-related-issues"
8+
FEATURES: list[str] = []
109

1110
def setUp(self) -> None:
1211
super().setUp()
1312
self.login_as(user=self.user)
1413
self.organization = self.create_organization(owner=self.user)
15-
self.error_type = "ApiTimeoutError"
16-
self.error_value = "Timed out attempting to reach host: api.github.com"
1714
# You need to set this value in your test before calling the API
1815
self.group_id = None
1916

2017
def reverse_url(self) -> str:
2118
return reverse(self.endpoint, kwargs={"issue_id": self.group_id})
2219

23-
def _data(self, type: str, value: str) -> dict[str, Any]:
20+
def _data(self, type: str, value: str) -> dict[str, object]:
2421
return {"type": "error", "metadata": {"type": type, "value": value}}
2522

2623
def test_same_root_related_issues(self) -> None:
2724
# This is the group we're going to query about
28-
group = self.create_group(data=self._data(self.error_type, self.error_value))
25+
error_type = "ApiTimeoutError"
26+
error_value = "Timed out attempting to reach host: api.github.com"
27+
group = self.create_group(data=self._data(error_type, error_value))
2928
self.group_id = group.id
3029

3130
groups_data = [
32-
self._data("ApiError", self.error_value),
33-
self._data(self.error_type, "Unreacheable host: api.github.com"),
34-
self._data(self.error_type, ""),
31+
self._data("ApiError", error_value),
32+
self._data(error_type, "Unreacheable host: api.github.com"),
33+
self._data(error_type, ""),
3534
# Only this group will be related
36-
self._data(self.error_type, self.error_value),
35+
self._data(error_type, error_value),
3736
]
3837
# XXX: See if we can get this code to be closer to how save_event generates groups
3938
for datum in groups_data:
@@ -45,6 +44,22 @@ def test_same_root_related_issues(self) -> None:
4544
# https://us.sentry.io/api/0/organizations/sentry/issues-stats/?groups=4741828952&groups=4489703641&statsPeriod=24h
4645
assert response.json() == {
4746
"data": [
48-
{"type": "same_root_cause", "data": [1, 5]},
47+
{"type": "same_root_cause", "data": [5]},
48+
{"type": "trace_connected", "data": []},
4949
],
5050
}
51+
52+
def test_trace_connected_errors(self) -> None:
53+
error_event, _, another_proj_event = self.load_errors()
54+
self.group_id = error_event.group_id # type: ignore[assignment]
55+
assert error_event.group_id != another_proj_event.group_id
56+
assert error_event.project.id != another_proj_event.project.id
57+
assert error_event.trace_id == another_proj_event.trace_id
58+
59+
response = self.get_success_response()
60+
assert response.json() == {
61+
"data": [
62+
{"type": "same_root_cause", "data": []},
63+
{"type": "trace_connected", "data": [another_proj_event.group_id]},
64+
]
65+
}

tests/snuba/api/endpoints/test_organization_events_trace.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,7 +1002,7 @@ def test_with_orphan_trace(self):
10021002

10031003
def test_with_errors(self):
10041004
self.load_trace()
1005-
error, error1 = self.load_errors()
1005+
error, error1, _ = self.load_errors()
10061006

10071007
with self.feature(self.FEATURES):
10081008
response = self.client_get(
@@ -1012,7 +1012,7 @@ def test_with_errors(self):
10121012
assert response.status_code == 200, response.content
10131013
self.assert_trace_data(response.data["transactions"][0])
10141014
gen1_event = response.data["transactions"][0]["children"][0]
1015-
assert len(gen1_event["errors"]) == 2
1015+
assert len(gen1_event["errors"]) == 3
10161016
data = {
10171017
"event_id": error.event_id,
10181018
"issue_id": error.group_id,
@@ -1537,9 +1537,9 @@ def test_with_errors(self):
15371537
)
15381538
assert response.status_code == 200, response.content
15391539
data = response.data
1540-
assert data["projects"] == 4
1540+
assert data["projects"] == 5
15411541
assert data["transactions"] == 8
1542-
assert data["errors"] == 2
1542+
assert data["errors"] == 3
15431543
assert data["performance_issues"] == 2
15441544

15451545
def test_with_default(self):

0 commit comments

Comments
 (0)