Skip to content

Commit cda5127

Browse files
authored
Add missing stack frames (#3673)
Add a new `init()` option `add_full_stack` (default `False`), when set to `True` it will add all the missing frames from the beginning of the execution to the stack trace sent to Sentry. Also adds another option `max_stack_frames` (default `100`) to limit the number of frames sent. The limitation is only enforced when `add_full_stack=True` to not change behavior for existing users. Fixes #3646
1 parent 50ad148 commit cda5127

File tree

3 files changed

+185
-5
lines changed

3 files changed

+185
-5
lines changed

sentry_sdk/consts.py

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
# up top to prevent circular import due to integration import
77
DEFAULT_MAX_VALUE_LENGTH = 1024
88

9+
DEFAULT_MAX_STACK_FRAMES = 100
10+
DEFAULT_ADD_FULL_STACK = False
11+
912

1013
# Also needs to be at the top to prevent circular import
1114
class EndpointType(Enum):
@@ -551,6 +554,8 @@ def __init__(
551554
cert_file=None, # type: Optional[str]
552555
key_file=None, # type: Optional[str]
553556
custom_repr=None, # type: Optional[Callable[..., Optional[str]]]
557+
add_full_stack=DEFAULT_ADD_FULL_STACK, # type: bool
558+
max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int]
554559
):
555560
# type: (...) -> None
556561
pass

sentry_sdk/utils.py

+77-5
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@
2626

2727
import sentry_sdk
2828
from sentry_sdk._compat import PY37
29-
from sentry_sdk.consts import DEFAULT_MAX_VALUE_LENGTH, EndpointType
29+
from sentry_sdk.consts import (
30+
DEFAULT_ADD_FULL_STACK,
31+
DEFAULT_MAX_STACK_FRAMES,
32+
DEFAULT_MAX_VALUE_LENGTH,
33+
EndpointType,
34+
)
3035

3136
from typing import TYPE_CHECKING
3237

@@ -737,6 +742,7 @@ def single_exception_from_error_tuple(
737742
exception_id=None, # type: Optional[int]
738743
parent_id=None, # type: Optional[int]
739744
source=None, # type: Optional[str]
745+
full_stack=None, # type: Optional[list[dict[str, Any]]]
740746
):
741747
# type: (...) -> Dict[str, Any]
742748
"""
@@ -804,10 +810,15 @@ def single_exception_from_error_tuple(
804810
custom_repr=custom_repr,
805811
)
806812
for tb in iter_stacks(tb)
807-
]
813+
] # type: List[Dict[str, Any]]
808814

809815
if frames:
810-
exception_value["stacktrace"] = {"frames": frames}
816+
if not full_stack:
817+
new_frames = frames
818+
else:
819+
new_frames = merge_stack_frames(frames, full_stack, client_options)
820+
821+
exception_value["stacktrace"] = {"frames": new_frames}
811822

812823
return exception_value
813824

@@ -862,6 +873,7 @@ def exceptions_from_error(
862873
exception_id=0, # type: int
863874
parent_id=0, # type: int
864875
source=None, # type: Optional[str]
876+
full_stack=None, # type: Optional[list[dict[str, Any]]]
865877
):
866878
# type: (...) -> Tuple[int, List[Dict[str, Any]]]
867879
"""
@@ -881,6 +893,7 @@ def exceptions_from_error(
881893
exception_id=exception_id,
882894
parent_id=parent_id,
883895
source=source,
896+
full_stack=full_stack,
884897
)
885898
exceptions = [parent]
886899

@@ -906,6 +919,7 @@ def exceptions_from_error(
906919
mechanism=mechanism,
907920
exception_id=exception_id,
908921
source="__cause__",
922+
full_stack=full_stack,
909923
)
910924
exceptions.extend(child_exceptions)
911925

@@ -927,6 +941,7 @@ def exceptions_from_error(
927941
mechanism=mechanism,
928942
exception_id=exception_id,
929943
source="__context__",
944+
full_stack=full_stack,
930945
)
931946
exceptions.extend(child_exceptions)
932947

@@ -943,6 +958,7 @@ def exceptions_from_error(
943958
exception_id=exception_id,
944959
parent_id=parent_id,
945960
source="exceptions[%s]" % idx,
961+
full_stack=full_stack,
946962
)
947963
exceptions.extend(child_exceptions)
948964

@@ -953,6 +969,7 @@ def exceptions_from_error_tuple(
953969
exc_info, # type: ExcInfo
954970
client_options=None, # type: Optional[Dict[str, Any]]
955971
mechanism=None, # type: Optional[Dict[str, Any]]
972+
full_stack=None, # type: Optional[list[dict[str, Any]]]
956973
):
957974
# type: (...) -> List[Dict[str, Any]]
958975
exc_type, exc_value, tb = exc_info
@@ -970,14 +987,20 @@ def exceptions_from_error_tuple(
970987
mechanism=mechanism,
971988
exception_id=0,
972989
parent_id=0,
990+
full_stack=full_stack,
973991
)
974992

975993
else:
976994
exceptions = []
977995
for exc_type, exc_value, tb in walk_exception_chain(exc_info):
978996
exceptions.append(
979997
single_exception_from_error_tuple(
980-
exc_type, exc_value, tb, client_options, mechanism
998+
exc_type=exc_type,
999+
exc_value=exc_value,
1000+
tb=tb,
1001+
client_options=client_options,
1002+
mechanism=mechanism,
1003+
full_stack=full_stack,
9811004
)
9821005
)
9831006

@@ -1096,6 +1119,46 @@ def exc_info_from_error(error):
10961119
return exc_info
10971120

10981121

1122+
def merge_stack_frames(frames, full_stack, client_options):
1123+
# type: (List[Dict[str, Any]], List[Dict[str, Any]], Optional[Dict[str, Any]]) -> List[Dict[str, Any]]
1124+
"""
1125+
Add the missing frames from full_stack to frames and return the merged list.
1126+
"""
1127+
frame_ids = {
1128+
(
1129+
frame["abs_path"],
1130+
frame["context_line"],
1131+
frame["lineno"],
1132+
frame["function"],
1133+
)
1134+
for frame in frames
1135+
}
1136+
1137+
new_frames = [
1138+
stackframe
1139+
for stackframe in full_stack
1140+
if (
1141+
stackframe["abs_path"],
1142+
stackframe["context_line"],
1143+
stackframe["lineno"],
1144+
stackframe["function"],
1145+
)
1146+
not in frame_ids
1147+
]
1148+
new_frames.extend(frames)
1149+
1150+
# Limit the number of frames
1151+
max_stack_frames = (
1152+
client_options.get("max_stack_frames", DEFAULT_MAX_STACK_FRAMES)
1153+
if client_options
1154+
else None
1155+
)
1156+
if max_stack_frames is not None:
1157+
new_frames = new_frames[len(new_frames) - max_stack_frames :]
1158+
1159+
return new_frames
1160+
1161+
10991162
def event_from_exception(
11001163
exc_info, # type: Union[BaseException, ExcInfo]
11011164
client_options=None, # type: Optional[Dict[str, Any]]
@@ -1104,12 +1167,21 @@ def event_from_exception(
11041167
# type: (...) -> Tuple[Event, Dict[str, Any]]
11051168
exc_info = exc_info_from_error(exc_info)
11061169
hint = event_hint_with_exc_info(exc_info)
1170+
1171+
if client_options and client_options.get("add_full_stack", DEFAULT_ADD_FULL_STACK):
1172+
full_stack = current_stacktrace(
1173+
include_local_variables=client_options["include_local_variables"],
1174+
max_value_length=client_options["max_value_length"],
1175+
)["frames"]
1176+
else:
1177+
full_stack = None
1178+
11071179
return (
11081180
{
11091181
"level": "error",
11101182
"exception": {
11111183
"values": exceptions_from_error_tuple(
1112-
exc_info, client_options, mechanism
1184+
exc_info, client_options, mechanism, full_stack
11131185
)
11141186
},
11151187
},

tests/test_full_stack_frames.py

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import sentry_sdk
2+
3+
4+
def test_full_stack_frames_default(sentry_init, capture_events):
5+
sentry_init()
6+
events = capture_events()
7+
8+
def foo():
9+
try:
10+
bar()
11+
except Exception as e:
12+
sentry_sdk.capture_exception(e)
13+
14+
def bar():
15+
raise Exception("This is a test exception")
16+
17+
foo()
18+
19+
(event,) = events
20+
frames = event["exception"]["values"][0]["stacktrace"]["frames"]
21+
22+
assert len(frames) == 2
23+
assert frames[-1]["function"] == "bar"
24+
assert frames[-2]["function"] == "foo"
25+
26+
27+
def test_full_stack_frames_enabled(sentry_init, capture_events):
28+
sentry_init(
29+
add_full_stack=True,
30+
)
31+
events = capture_events()
32+
33+
def foo():
34+
try:
35+
bar()
36+
except Exception as e:
37+
sentry_sdk.capture_exception(e)
38+
39+
def bar():
40+
raise Exception("This is a test exception")
41+
42+
foo()
43+
44+
(event,) = events
45+
frames = event["exception"]["values"][0]["stacktrace"]["frames"]
46+
47+
assert len(frames) > 2
48+
assert frames[-1]["function"] == "bar"
49+
assert frames[-2]["function"] == "foo"
50+
assert frames[-3]["function"] == "foo"
51+
assert frames[-4]["function"] == "test_full_stack_frames_enabled"
52+
53+
54+
def test_full_stack_frames_enabled_truncated(sentry_init, capture_events):
55+
sentry_init(
56+
add_full_stack=True,
57+
max_stack_frames=3,
58+
)
59+
events = capture_events()
60+
61+
def foo():
62+
try:
63+
bar()
64+
except Exception as e:
65+
sentry_sdk.capture_exception(e)
66+
67+
def bar():
68+
raise Exception("This is a test exception")
69+
70+
foo()
71+
72+
(event,) = events
73+
frames = event["exception"]["values"][0]["stacktrace"]["frames"]
74+
75+
assert len(frames) == 3
76+
assert frames[-1]["function"] == "bar"
77+
assert frames[-2]["function"] == "foo"
78+
assert frames[-3]["function"] == "foo"
79+
80+
81+
def test_full_stack_frames_default_no_truncation_happening(sentry_init, capture_events):
82+
sentry_init(
83+
max_stack_frames=1, # this is ignored if add_full_stack=False (which is the default)
84+
)
85+
events = capture_events()
86+
87+
def foo():
88+
try:
89+
bar()
90+
except Exception as e:
91+
sentry_sdk.capture_exception(e)
92+
93+
def bar():
94+
raise Exception("This is a test exception")
95+
96+
foo()
97+
98+
(event,) = events
99+
frames = event["exception"]["values"][0]["stacktrace"]["frames"]
100+
101+
assert len(frames) == 2
102+
assert frames[-1]["function"] == "bar"
103+
assert frames[-2]["function"] == "foo"

0 commit comments

Comments
 (0)