Skip to content

Commit 2e3a412

Browse files
authored
chore(similarity): Do not send > 30 system frames to seer (#81259)
Do not send stacktraces with > 30 system frames and no in-app frames to seer
1 parent 16c843f commit 2e3a412

File tree

7 files changed

+212
-15
lines changed

7 files changed

+212
-15
lines changed

src/sentry/grouping/ingest/seer.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
from sentry.seer.similarity.similar_issues import get_similarity_data_from_seer
1818
from sentry.seer.similarity.types import SimilarIssuesEmbeddingsRequest
1919
from sentry.seer.similarity.utils import (
20+
ReferrerOptions,
2021
event_content_is_seer_eligible,
2122
filter_null_from_string,
22-
get_stacktrace_string,
23+
get_stacktrace_string_with_metrics,
2324
killswitch_enabled,
2425
)
2526
from sentry.utils import metrics
@@ -182,7 +183,9 @@ def _circuit_breaker_broken(event: Event, project: Project) -> bool:
182183

183184

184185
def _has_empty_stacktrace_string(event: Event, variants: Mapping[str, BaseVariant]) -> bool:
185-
stacktrace_string = get_stacktrace_string(get_grouping_info_from_variants(variants))
186+
stacktrace_string = get_stacktrace_string_with_metrics(
187+
get_grouping_info_from_variants(variants), event.platform, ReferrerOptions.INGEST
188+
)
186189
if stacktrace_string == "":
187190
metrics.incr(
188191
"grouping.similarity.did_call_seer",
@@ -217,7 +220,10 @@ def get_seer_similar_issues(
217220
"hash": event_hash,
218221
"project_id": event.project.id,
219222
"stacktrace": event.data.get(
220-
"stacktrace_string", get_stacktrace_string(get_grouping_info_from_variants(variants))
223+
"stacktrace_string",
224+
get_stacktrace_string_with_metrics(
225+
get_grouping_info_from_variants(variants), event.platform, ReferrerOptions.INGEST
226+
),
221227
),
222228
"exception_type": filter_null_from_string(exception_type) if exception_type else None,
223229
"k": num_neighbors,

src/sentry/issues/endpoints/group_similar_issues_embeddings.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from sentry.seer.similarity.similar_issues import get_similarity_data_from_seer
1919
from sentry.seer.similarity.types import SeerSimilarIssueData, SimilarIssuesEmbeddingsRequest
2020
from sentry.seer.similarity.utils import (
21+
TooManyOnlySystemFramesException,
2122
event_content_has_stacktrace,
2223
get_stacktrace_string,
2324
killswitch_enabled,
@@ -82,9 +83,12 @@ def get(self, request: Request, group) -> Response:
8283
stacktrace_string = ""
8384
if latest_event and event_content_has_stacktrace(latest_event):
8485
grouping_info = get_grouping_info(None, project=group.project, event=latest_event)
85-
stacktrace_string = get_stacktrace_string(grouping_info)
86+
try:
87+
stacktrace_string = get_stacktrace_string(grouping_info)
88+
except TooManyOnlySystemFramesException:
89+
stacktrace_string = ""
8690

87-
if stacktrace_string == "" or not latest_event:
91+
if not stacktrace_string or not latest_event:
8892
return Response([]) # No exception, stacktrace or in-app frames, or event
8993

9094
similar_issues_params: SimilarIssuesEmbeddingsRequest = {

src/sentry/seer/similarity/utils.py

+41
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from enum import StrEnum
23
from typing import Any, TypeVar
34

45
from sentry import options
@@ -151,6 +152,15 @@
151152
]
152153

153154

155+
class ReferrerOptions(StrEnum):
156+
INGEST = "ingest"
157+
BACKFILL = "backfill"
158+
159+
160+
class TooManyOnlySystemFramesException(Exception):
161+
pass
162+
163+
154164
def _get_value_if_exists(exception_value: dict[str, Any]) -> str:
155165
return exception_value["values"][0] if exception_value.get("values") else ""
156166

@@ -177,6 +187,7 @@ def get_stacktrace_string(data: dict[str, Any]) -> str:
177187

178188
frame_count = 0
179189
html_frame_count = 0 # for a temporary metric
190+
is_frames_truncated = False
180191
stacktrace_str = ""
181192
found_non_snipped_context_line = False
182193

@@ -185,12 +196,15 @@ def get_stacktrace_string(data: dict[str, Any]) -> str:
185196
def _process_frames(frames: list[dict[str, Any]]) -> list[str]:
186197
nonlocal frame_count
187198
nonlocal html_frame_count
199+
nonlocal is_frames_truncated
188200
nonlocal found_non_snipped_context_line
189201
frame_strings = []
190202

191203
contributing_frames = [
192204
frame for frame in frames if frame.get("id") == "frame" and frame.get("contributes")
193205
]
206+
if len(contributing_frames) + frame_count > MAX_FRAME_COUNT:
207+
is_frames_truncated = True
194208
contributing_frames = _discard_excess_frames(
195209
contributing_frames, MAX_FRAME_COUNT, frame_count
196210
)
@@ -255,6 +269,8 @@ def _process_frames(frames: list[dict[str, Any]]) -> list[str]:
255269
exc_value = _get_value_if_exists(exception_value)
256270
elif exception_value.get("id") == "stacktrace" and frame_count < MAX_FRAME_COUNT:
257271
frame_strings = _process_frames(exception_value["values"])
272+
if is_frames_truncated and not app_hash:
273+
raise TooManyOnlySystemFramesException
258274
# Only exceptions have the type and value properties, so we don't need to handle the threads
259275
# case here
260276
header = f"{exc_type}: {exc_value}\n" if exception["id"] == "exception" else ""
@@ -290,6 +306,31 @@ def _process_frames(frames: list[dict[str, Any]]) -> list[str]:
290306
return stacktrace_str.strip()
291307

292308

309+
def get_stacktrace_string_with_metrics(
310+
data: dict[str, Any], platform: str | None, referrer: ReferrerOptions
311+
) -> str | None:
312+
try:
313+
stacktrace_string = get_stacktrace_string(data)
314+
except TooManyOnlySystemFramesException:
315+
platform = platform if platform else "unknown"
316+
metrics.incr(
317+
"grouping.similarity.over_threshold_only_system_frames",
318+
sample_rate=options.get("seer.similarity.metrics_sample_rate"),
319+
tags={"platform": platform, "referrer": referrer},
320+
)
321+
if referrer == ReferrerOptions.INGEST:
322+
metrics.incr(
323+
"grouping.similarity.did_call_seer",
324+
sample_rate=options.get("seer.similarity.metrics_sample_rate"),
325+
tags={
326+
"call_made": False,
327+
"blocker": "over-threshold-only-system-frames",
328+
},
329+
)
330+
stacktrace_string = None
331+
return stacktrace_string
332+
333+
293334
def event_content_has_stacktrace(event: Event) -> bool:
294335
# If an event has no stacktrace, there's no data for Seer to analyze, so no point in making the
295336
# API call. If we ever start analyzing message-only events, we'll need to add `event.title in

src/sentry/tasks/embeddings_grouping/utils.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@
3232
SimilarHashNotFoundError,
3333
)
3434
from sentry.seer.similarity.utils import (
35+
ReferrerOptions,
3536
event_content_has_stacktrace,
3637
filter_null_from_string,
37-
get_stacktrace_string,
38+
get_stacktrace_string_with_metrics,
3839
)
3940
from sentry.snuba.dataset import Dataset
4041
from sentry.snuba.referrer import Referrer
@@ -355,8 +356,10 @@ def get_events_from_nodestore(
355356
event._project_cache = project
356357
if event and event.data and event_content_has_stacktrace(event):
357358
grouping_info = get_grouping_info(None, project=project, event=event)
358-
stacktrace_string = get_stacktrace_string(grouping_info)
359-
if stacktrace_string == "":
359+
stacktrace_string = get_stacktrace_string_with_metrics(
360+
grouping_info, event.platform, ReferrerOptions.BACKFILL
361+
)
362+
if not stacktrace_string:
360363
invalid_event_group_ids.append(group_id)
361364
continue
362365
primary_hash = event.get_primary_hash()

tests/sentry/grouping/ingest/test_seer.py

+49
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from sentry.grouping.ingest.seer import get_seer_similar_issues, should_call_seer_for_grouping
99
from sentry.models.grouphash import GroupHash
1010
from sentry.seer.similarity.types import SeerSimilarIssueData
11+
from sentry.seer.similarity.utils import MAX_FRAME_COUNT
1112
from sentry.testutils.cases import TestCase
1213
from sentry.testutils.helpers.eventprocessing import save_new_event
1314
from sentry.testutils.helpers.options import override_options
@@ -306,3 +307,51 @@ def test_returns_no_grouphash_and_empty_metadata_if_no_similar_group_found(self)
306307
expected_metadata,
307308
None,
308309
)
310+
311+
@patch("sentry.seer.similarity.utils.metrics")
312+
def test_too_many_only_system_frames(self, mock_metrics: Mock) -> None:
313+
type = "FailedToFetchError"
314+
value = "Charlie didn't bring the ball back"
315+
context_line = f"raise {type}('{value}')"
316+
new_event = Event(
317+
project_id=self.project.id,
318+
event_id="22312012112120120908201304152013",
319+
data={
320+
"title": f"{type}('{value}')",
321+
"exception": {
322+
"values": [
323+
{
324+
"type": type,
325+
"value": value,
326+
"stacktrace": {
327+
"frames": [
328+
{
329+
"function": f"play_fetch_{i}",
330+
"filename": f"dogpark{i}.py",
331+
"context_line": context_line,
332+
}
333+
for i in range(MAX_FRAME_COUNT + 1)
334+
]
335+
},
336+
}
337+
]
338+
},
339+
"platform": "python",
340+
},
341+
)
342+
get_seer_similar_issues(new_event, new_event.get_grouping_variants())
343+
344+
sample_rate = options.get("seer.similarity.metrics_sample_rate")
345+
mock_metrics.incr.assert_any_call(
346+
"grouping.similarity.over_threshold_only_system_frames",
347+
sample_rate=sample_rate,
348+
tags={"platform": "python", "referrer": "ingest"},
349+
)
350+
mock_metrics.incr.assert_any_call(
351+
"grouping.similarity.did_call_seer",
352+
sample_rate=1.0,
353+
tags={
354+
"call_made": False,
355+
"blocker": "over-threshold-only-system-frames",
356+
},
357+
)

tests/sentry/seer/similarity/test_utils.py

+36-6
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
from typing import Any, Literal, cast
44
from uuid import uuid1
55

6+
import pytest
7+
68
from sentry.eventstore.models import Event
79
from sentry.seer.similarity.utils import (
810
BASE64_ENCODED_PREFIXES,
11+
MAX_FRAME_COUNT,
912
SEER_ELIGIBLE_PLATFORMS,
13+
TooManyOnlySystemFramesException,
1014
_is_snipped_context_line,
1115
event_content_is_seer_eligible,
1216
filter_null_from_string,
@@ -670,18 +674,18 @@ def test_chained_too_many_frames_minified_js_frame_limit(self):
670674
)
671675

672676
def test_chained_too_many_exceptions(self):
673-
"""Test that we restrict number of chained exceptions to 30."""
677+
"""Test that we restrict number of chained exceptions to MAX_FRAME_COUNT."""
674678
data_chained_exception = copy.deepcopy(self.CHAINED_APP_DATA)
675679
data_chained_exception["app"]["component"]["values"][0]["values"] = [
676680
self.create_exception(
677681
exception_type_str="Exception",
678682
exception_value=f"exception {i} message!",
679683
frames=self.create_frames(num_frames=1, context_line_factory=lambda i: f"line {i}"),
680684
)
681-
for i in range(1, 32)
685+
for i in range(1, MAX_FRAME_COUNT + 2)
682686
]
683687
stacktrace_str = get_stacktrace_string(data_chained_exception)
684-
for i in range(2, 32):
688+
for i in range(2, MAX_FRAME_COUNT + 2):
685689
assert f"exception {i} message!" in stacktrace_str
686690
assert "exception 1 message!" not in stacktrace_str
687691

@@ -710,9 +714,35 @@ def test_no_app_no_system(self):
710714
stacktrace_str = get_stacktrace_string(data)
711715
assert stacktrace_str == ""
712716

713-
def test_over_30_contributing_frames(self):
714-
"""Check that when there are over 30 contributing frames, the last 30 are included."""
717+
def test_too_many_system_frames_single_exception(self):
718+
data_system = copy.deepcopy(self.BASE_APP_DATA)
719+
data_system["system"] = data_system.pop("app")
720+
data_system["system"]["component"]["values"][0]["values"][0][
721+
"values"
722+
] += self.create_frames(MAX_FRAME_COUNT + 1, True)
723+
724+
with pytest.raises(TooManyOnlySystemFramesException):
725+
get_stacktrace_string(data_system)
726+
727+
def test_too_many_system_frames_chained_exception(self):
728+
data_system = copy.deepcopy(self.CHAINED_APP_DATA)
729+
data_system["system"] = data_system.pop("app")
730+
# Split MAX_FRAME_COUNT across the two exceptions
731+
data_system["system"]["component"]["values"][0]["values"][0]["values"][0][
732+
"values"
733+
] += self.create_frames(MAX_FRAME_COUNT // 2, True)
734+
data_system["system"]["component"]["values"][0]["values"][1]["values"][0][
735+
"values"
736+
] += self.create_frames(MAX_FRAME_COUNT // 2, True)
737+
738+
with pytest.raises(TooManyOnlySystemFramesException):
739+
get_stacktrace_string(data_system)
715740

741+
def test_too_many_in_app_contributing_frames(self):
742+
"""
743+
Check that when there are over MAX_FRAME_COUNT contributing frames, the last MAX_FRAME_COUNT
744+
are included.
745+
"""
716746
data_frames = copy.deepcopy(self.BASE_APP_DATA)
717747
# Create 30 contributing frames, 1-20 -> last 10 should be included
718748
data_frames["app"]["component"]["values"][0]["values"][0]["values"] = self.create_frames(
@@ -739,7 +769,7 @@ def test_over_30_contributing_frames(self):
739769
for i in range(41, 61):
740770
num_frames += 1
741771
assert ("test = " + str(i) + "!") in stacktrace_str
742-
assert num_frames == 30
772+
assert num_frames == MAX_FRAME_COUNT
743773

744774
def test_too_many_frames_minified_js_frame_limit(self):
745775
"""Test that we restrict fully-minified stacktraces to 20 frames, and all other stacktraces to 30 frames."""

0 commit comments

Comments
 (0)