Skip to content

Commit 9b23ae8

Browse files
authored
fix(trace): Handle missing groups (#109942)
- Groups being missing shouldn't break the entire trace view. Adding None to the result type so we can handle errors this way and then continue rendering the trace as best we can
1 parent 9101dc1 commit 9b23ae8

File tree

2 files changed

+76
-12
lines changed

2 files changed

+76
-12
lines changed

src/sentry/snuba/trace.py

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
)
1919
from sentry_protos.snuba.v1.request_common_pb2 import RequestMeta, TraceItemType
2020
from sentry_protos.snuba.v1.trace_item_attribute_pb2 import AttributeKey, AttributeValue
21-
from sentry_protos.snuba.v1.trace_item_filter_pb2 import ComparisonFilter, TraceItemFilter
21+
from sentry_protos.snuba.v1.trace_item_filter_pb2 import (
22+
ComparisonFilter,
23+
TraceItemFilter,
24+
)
2225
from snuba_sdk import Column as SnubaColumn
2326
from snuba_sdk import Function
2427

@@ -111,7 +114,9 @@ class TraceOccurrenceEvent(TypedDict):
111114
issue_data: TraceIssueOccurrenceData
112115

113116

114-
def _serialize_rpc_issue(event: dict[str, Any], group_cache: dict[int, Group]) -> SerializedIssue:
117+
def _serialize_rpc_issue(
118+
event: dict[str, Any], group_cache: dict[int, Group]
119+
) -> SerializedIssue | None:
115120
def _qualify_short_id(project: str, short_id: int | None) -> str | None:
116121
"""Logic for qualified_short_id is copied from property on the Group model
117122
to prevent an N+1 query from accessing project.slug everytime"""
@@ -127,7 +132,11 @@ def _qualify_short_id(project: str, short_id: int | None) -> str | None:
127132
if issue_id in group_cache:
128133
issue = group_cache[issue_id]
129134
else:
130-
issue = Group.objects.get(id=issue_id, project__id=occurrence.project_id)
135+
try:
136+
issue = Group.objects.get(id=issue_id, project__id=occurrence.project_id)
137+
except Group.DoesNotExist as e:
138+
logger.error(e)
139+
return None
131140
group_cache[issue_id] = issue
132141
return SerializedIssue(
133142
event_id=occurrence.event_id,
@@ -154,7 +163,11 @@ def _qualify_short_id(project: str, short_id: int | None) -> str | None:
154163
if issue_id in group_cache:
155164
issue = group_cache[issue_id]
156165
else:
157-
issue = Group.objects.get(id=issue_id, project__id=event["project.id"])
166+
try:
167+
issue = Group.objects.get(id=issue_id, project__id=event["project.id"])
168+
except Group.DoesNotExist as e:
169+
logger.error(e)
170+
return None
158171
group_cache[issue_id] = issue
159172

160173
return SerializedIssue(
@@ -179,7 +192,7 @@ def _serialize_rpc_event(
179192
event: dict[str, Any],
180193
group_cache: dict[int, Group],
181194
additional_attributes: list[str] | None = None,
182-
) -> SerializedEvent | SerializedIssue | SerializedUptimeCheck:
195+
) -> SerializedEvent | SerializedIssue | SerializedUptimeCheck | None:
183196
if event.get("event_type") not in ("span", "uptime_check"):
184197
return _serialize_rpc_issue(event, group_cache)
185198

@@ -189,11 +202,25 @@ def _serialize_rpc_event(
189202
if attribute in event
190203
}
191204
children = [
192-
_serialize_rpc_event(child, group_cache, additional_attributes)
193-
for child in event["children"]
205+
child
206+
for child in [
207+
_serialize_rpc_event(child, group_cache, additional_attributes)
208+
for child in event["children"]
209+
]
210+
if child is not None
211+
]
212+
errors = [
213+
error
214+
for error in [_serialize_rpc_issue(error, group_cache) for error in event["errors"]]
215+
if error is not None
216+
]
217+
occurrences = [
218+
occurrence
219+
for occurrence in [
220+
_serialize_rpc_issue(error, group_cache) for error in event["occurrences"]
221+
]
222+
if occurrence is not None
194223
]
195-
errors = [_serialize_rpc_issue(error, group_cache) for error in event["errors"]]
196-
occurrences = [_serialize_rpc_issue(error, group_cache) for error in event["occurrences"]]
197224

198225
if event.get("event_type") == "uptime_check":
199226
return SerializedUptimeCheck(
@@ -389,7 +416,9 @@ def _uptime_results_query(
389416
)
390417

391418

392-
def _run_uptime_results_query(uptime_query: TraceItemTableRequest) -> list[TraceItemTableResponse]:
419+
def _run_uptime_results_query(
420+
uptime_query: TraceItemTableRequest,
421+
) -> list[TraceItemTableResponse]:
393422
return table_rpc([uptime_query])
394423

395424

@@ -651,4 +680,10 @@ def query_trace_data(
651680
result.extend(errors)
652681
group_cache: dict[int, Group] = {}
653682
with sentry_sdk.start_span(op="serializing_data"):
654-
return [_serialize_rpc_event(root, group_cache, additional_attributes) for root in result]
683+
return [
684+
event
685+
for event in [
686+
_serialize_rpc_event(root, group_cache, additional_attributes) for root in result
687+
]
688+
if event is not None
689+
]

tests/snuba/api/endpoints/test_organization_trace.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from sentry.conf.types.uptime import UptimeRegionConfig
1212
from sentry.issues.ingest import save_issue_occurrence
1313
from sentry.issues.issue_occurrence import IssueOccurrence
14+
from sentry.models.group import Group
1415
from sentry.search.events.types import SnubaParams
1516
from sentry.testutils.cases import UptimeResultEAPTestCase
1617
from sentry.testutils.helpers.datetime import before_now
@@ -23,7 +24,9 @@
2324

2425
logger = logging.getLogger(__name__)
2526

26-
from sentry_protos.snuba.v1.trace_item_attribute_pb2 import AttributeValue as ProtoAttributeValue
27+
from sentry_protos.snuba.v1.trace_item_attribute_pb2 import (
28+
AttributeValue as ProtoAttributeValue,
29+
)
2730

2831
from sentry.snuba.trace import _serialize_columnar_uptime_item
2932
from sentry.testutils.cases import TestCase
@@ -350,6 +353,32 @@ def test_with_errors_data(self) -> None:
350353
assert error_event["issue_id"] == error.group_id
351354
assert error_event["start_timestamp"] == error_data["timestamp"]
352355

356+
def test_with_missing_group(self) -> None:
357+
self.load_trace()
358+
_, start = self.get_start_end_from_day_ago(123)
359+
error_data = load_data(
360+
"javascript",
361+
timestamp=start,
362+
)
363+
error_data["contexts"]["trace"] = {
364+
"type": "trace",
365+
"trace_id": self.trace_id,
366+
"span_id": self.root_event.data["contexts"]["trace"]["span_id"],
367+
}
368+
error_data["tags"] = [["transaction", "/transaction/gen1-0"]]
369+
self.store_event(error_data, project_id=self.gen1_project.id)
370+
371+
with self.feature(self.FEATURES):
372+
with mock.patch("sentry.snuba.trace.Group.objects.get", side_effect=Group.DoesNotExist):
373+
response = self.client_get(
374+
data={"timestamp": self.day_ago},
375+
)
376+
assert response.status_code == 200, response.content
377+
data = response.data
378+
assert len(data) == 1
379+
self.assert_trace_data(data[0])
380+
assert len(data[0]["errors"]) == 0
381+
353382
def test_with_errors_data_with_overlapping_span_id(self) -> None:
354383
self.load_trace()
355384
_, start = self.get_start_end_from_day_ago(123)

0 commit comments

Comments
 (0)