Skip to content

Commit 47f7812

Browse files
fix: Don't hang when capturing long stacktrace
Fixes #2764
1 parent aaee45e commit 47f7812

File tree

4 files changed

+78
-12
lines changed

4 files changed

+78
-12
lines changed

sentry_sdk/_types.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,14 @@ def removed_because_raw_data(cls):
4747
)
4848

4949
@classmethod
50-
def removed_because_over_size_limit(cls):
51-
# type: () -> AnnotatedValue
52-
"""The actual value was removed because the size of the field exceeded the configured maximum size (specified with the max_request_body_size sdk option)"""
50+
def removed_because_over_size_limit(cls, value=""):
51+
# type: (Any) -> AnnotatedValue
52+
"""
53+
The actual value was removed because the size of the field exceeded the configured maximum size,
54+
for example specified with the max_request_body_size sdk option.
55+
"""
5356
return AnnotatedValue(
54-
value="",
57+
value=value,
5558
metadata={
5659
"rem": [ # Remark
5760
[
@@ -160,7 +163,7 @@ class SDKInfo(TypedDict):
160163
"errors": list[dict[str, Any]], # TODO: We can expand on this type
161164
"event_id": str,
162165
"exception": dict[
163-
Literal["values"], list[dict[str, Any]]
166+
Literal["values"], list[Annotated[dict[str, Any]]]
164167
], # TODO: We can expand on this type
165168
"extra": MutableMapping[str, object],
166169
"fingerprint": list[str],

sentry_sdk/client.py

+2
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,8 @@ def _update_session_from_event(
756756
if exceptions:
757757
errored = True
758758
for error in exceptions:
759+
if isinstance(error, AnnotatedValue):
760+
error = error.value or {}
759761
mechanism = error.get("mechanism")
760762
if isinstance(mechanism, Mapping) and mechanism.get("handled") is False:
761763
crashed = True

sentry_sdk/utils.py

+28-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import base64
2+
import functools
23
import json
34
import linecache
45
import logging
@@ -77,6 +78,15 @@
7778
FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0"))
7879
TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1"))
7980

81+
MAX_STACK_FRAMES = 2000
82+
"""Maximum number of stack frames to send to Sentry.
83+
84+
If we have more than this number of stack frames, we will stop processing
85+
the stacktrace to avoid getting stuck in a long-lasting loop. This value
86+
exceeds the default sys.getrecursionlimit() of 1000, so users will only
87+
be affected by this limit if they have a custom recursion limit.
88+
"""
89+
8090

8191
def env_to_bool(value, *, strict=False):
8292
# type: (Any, Optional[bool]) -> bool | None
@@ -667,7 +677,7 @@ def single_exception_from_error_tuple(
667677
source=None, # type: Optional[str]
668678
full_stack=None, # type: Optional[list[dict[str, Any]]]
669679
):
670-
# type: (...) -> Dict[str, Any]
680+
# type: (...) -> Annotated[Dict[str, Any]]
671681
"""
672682
Creates a dict that goes into the events `exception.values` list and is ingestible by Sentry.
673683
@@ -732,10 +742,15 @@ def single_exception_from_error_tuple(
732742
max_value_length=max_value_length,
733743
custom_repr=custom_repr,
734744
)
735-
for tb in iter_stacks(tb)
745+
for tb, _ in zip(iter_stacks(tb), range(MAX_STACK_FRAMES + 1))
736746
] # type: List[Dict[str, Any]]
737747

738-
if frames:
748+
if len(frames) > MAX_STACK_FRAMES:
749+
exception_value["stacktrace"] = AnnotatedValue.removed_because_over_size_limit(
750+
value={}
751+
)
752+
753+
elif frames:
739754
if not full_stack:
740755
new_frames = frames
741756
else:
@@ -798,7 +813,7 @@ def exceptions_from_error(
798813
source=None, # type: Optional[str]
799814
full_stack=None, # type: Optional[list[dict[str, Any]]]
800815
):
801-
# type: (...) -> Tuple[int, List[Dict[str, Any]]]
816+
# type: (...) -> Tuple[int, List[Annotated[Dict[str, Any]]]]
802817
"""
803818
Creates the list of exceptions.
804819
This can include chained exceptions and exceptions from an ExceptionGroup.
@@ -894,7 +909,7 @@ def exceptions_from_error_tuple(
894909
mechanism=None, # type: Optional[Dict[str, Any]]
895910
full_stack=None, # type: Optional[list[dict[str, Any]]]
896911
):
897-
# type: (...) -> List[Dict[str, Any]]
912+
# type: (...) -> List[Annotated[Dict[str, Any]]]
898913
exc_type, exc_value, tb = exc_info
899914

900915
is_exception_group = BaseExceptionGroup is not None and isinstance(
@@ -941,7 +956,7 @@ def to_string(value):
941956

942957

943958
def iter_event_stacktraces(event):
944-
# type: (Event) -> Iterator[Dict[str, Any]]
959+
# type: (Event) -> Iterator[Annotated[Dict[str, Any]]]
945960
if "stacktrace" in event:
946961
yield event["stacktrace"]
947962
if "threads" in event:
@@ -950,20 +965,26 @@ def iter_event_stacktraces(event):
950965
yield thread["stacktrace"]
951966
if "exception" in event:
952967
for exception in event["exception"].get("values") or ():
953-
if "stacktrace" in exception:
968+
if isinstance(exception, dict) and "stacktrace" in exception:
954969
yield exception["stacktrace"]
955970

956971

957972
def iter_event_frames(event):
958973
# type: (Event) -> Iterator[Dict[str, Any]]
959974
for stacktrace in iter_event_stacktraces(event):
975+
if isinstance(stacktrace, AnnotatedValue):
976+
stacktrace = stacktrace.value or {}
977+
960978
for frame in stacktrace.get("frames") or ():
961979
yield frame
962980

963981

964982
def handle_in_app(event, in_app_exclude=None, in_app_include=None, project_root=None):
965983
# type: (Event, Optional[List[str]], Optional[List[str]], Optional[str]) -> Event
966984
for stacktrace in iter_event_stacktraces(event):
985+
if isinstance(stacktrace, AnnotatedValue):
986+
stacktrace = stacktrace.value or {}
987+
967988
set_in_app_in_frames(
968989
stacktrace.get("frames"),
969990
in_app_exclude=in_app_exclude,

tests/test_basics.py

+40
Original file line numberDiff line numberDiff line change
@@ -1065,3 +1065,43 @@ def __str__(self):
10651065
(event,) = events
10661066

10671067
assert event["exception"]["values"][0]["value"] == "aha!\nnote 1\nnote 3"
1068+
1069+
1070+
def test_stacktrace_big_recursion(sentry_init, capture_events):
1071+
"""
1072+
Ensure that if the recursion limit is increased, the full stacktrace is not captured,
1073+
as it would take too long to process the entire stack trace.
1074+
Also, ensure that the capturing does not take too long.
1075+
"""
1076+
sentry_init()
1077+
events = capture_events()
1078+
1079+
def recurse():
1080+
recurse()
1081+
1082+
old_recursion_limit = sys.getrecursionlimit()
1083+
1084+
try:
1085+
sys.setrecursionlimit(100_000)
1086+
recurse()
1087+
except RecursionError as e:
1088+
capture_start_time = time.perf_counter_ns()
1089+
sentry_sdk.capture_exception(e)
1090+
capture_end_time = time.perf_counter_ns()
1091+
finally:
1092+
sys.setrecursionlimit(old_recursion_limit)
1093+
1094+
(event,) = events
1095+
1096+
assert event["exception"]["values"][0]["stacktrace"] == {}
1097+
assert event["_meta"] == {
1098+
"exception": {
1099+
"values": {"0": {"stacktrace": {"": {"rem": [["!config", "x"]]}}}}
1100+
}
1101+
}
1102+
1103+
# On my machine, it takes about 100-200ms to capture the exception,
1104+
# so this limit should be generous enough.
1105+
assert (
1106+
capture_end_time - capture_start_time < 10**9
1107+
), "stacktrace capture took too long, check that frame limit is set correctly"

0 commit comments

Comments
 (0)