Skip to content

Commit

Permalink
Add missing stack frames (#3673)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
antonpirker authored Dec 5, 2024
1 parent 50ad148 commit cda5127
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 5 deletions.
5 changes: 5 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
# up top to prevent circular import due to integration import
DEFAULT_MAX_VALUE_LENGTH = 1024

DEFAULT_MAX_STACK_FRAMES = 100
DEFAULT_ADD_FULL_STACK = False


# Also needs to be at the top to prevent circular import
class EndpointType(Enum):
Expand Down Expand Up @@ -551,6 +554,8 @@ def __init__(
cert_file=None, # type: Optional[str]
key_file=None, # type: Optional[str]
custom_repr=None, # type: Optional[Callable[..., Optional[str]]]
add_full_stack=DEFAULT_ADD_FULL_STACK, # type: bool
max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int]
):
# type: (...) -> None
pass
Expand Down
82 changes: 77 additions & 5 deletions sentry_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@

import sentry_sdk
from sentry_sdk._compat import PY37
from sentry_sdk.consts import DEFAULT_MAX_VALUE_LENGTH, EndpointType
from sentry_sdk.consts import (
DEFAULT_ADD_FULL_STACK,
DEFAULT_MAX_STACK_FRAMES,
DEFAULT_MAX_VALUE_LENGTH,
EndpointType,
)

from typing import TYPE_CHECKING

Expand Down Expand Up @@ -737,6 +742,7 @@ def single_exception_from_error_tuple(
exception_id=None, # type: Optional[int]
parent_id=None, # type: Optional[int]
source=None, # type: Optional[str]
full_stack=None, # type: Optional[list[dict[str, Any]]]
):
# type: (...) -> Dict[str, Any]
"""
Expand Down Expand Up @@ -804,10 +810,15 @@ def single_exception_from_error_tuple(
custom_repr=custom_repr,
)
for tb in iter_stacks(tb)
]
] # type: List[Dict[str, Any]]

if frames:
exception_value["stacktrace"] = {"frames": frames}
if not full_stack:
new_frames = frames
else:
new_frames = merge_stack_frames(frames, full_stack, client_options)

exception_value["stacktrace"] = {"frames": new_frames}

return exception_value

Expand Down Expand Up @@ -862,6 +873,7 @@ def exceptions_from_error(
exception_id=0, # type: int
parent_id=0, # type: int
source=None, # type: Optional[str]
full_stack=None, # type: Optional[list[dict[str, Any]]]
):
# type: (...) -> Tuple[int, List[Dict[str, Any]]]
"""
Expand All @@ -881,6 +893,7 @@ def exceptions_from_error(
exception_id=exception_id,
parent_id=parent_id,
source=source,
full_stack=full_stack,
)
exceptions = [parent]

Expand All @@ -906,6 +919,7 @@ def exceptions_from_error(
mechanism=mechanism,
exception_id=exception_id,
source="__cause__",
full_stack=full_stack,
)
exceptions.extend(child_exceptions)

Expand All @@ -927,6 +941,7 @@ def exceptions_from_error(
mechanism=mechanism,
exception_id=exception_id,
source="__context__",
full_stack=full_stack,
)
exceptions.extend(child_exceptions)

Expand All @@ -943,6 +958,7 @@ def exceptions_from_error(
exception_id=exception_id,
parent_id=parent_id,
source="exceptions[%s]" % idx,
full_stack=full_stack,
)
exceptions.extend(child_exceptions)

Expand All @@ -953,6 +969,7 @@ def exceptions_from_error_tuple(
exc_info, # type: ExcInfo
client_options=None, # type: Optional[Dict[str, Any]]
mechanism=None, # type: Optional[Dict[str, Any]]
full_stack=None, # type: Optional[list[dict[str, Any]]]
):
# type: (...) -> List[Dict[str, Any]]
exc_type, exc_value, tb = exc_info
Expand All @@ -970,14 +987,20 @@ def exceptions_from_error_tuple(
mechanism=mechanism,
exception_id=0,
parent_id=0,
full_stack=full_stack,
)

else:
exceptions = []
for exc_type, exc_value, tb in walk_exception_chain(exc_info):
exceptions.append(
single_exception_from_error_tuple(
exc_type, exc_value, tb, client_options, mechanism
exc_type=exc_type,
exc_value=exc_value,
tb=tb,
client_options=client_options,
mechanism=mechanism,
full_stack=full_stack,
)
)

Expand Down Expand Up @@ -1096,6 +1119,46 @@ def exc_info_from_error(error):
return exc_info


def merge_stack_frames(frames, full_stack, client_options):
# type: (List[Dict[str, Any]], List[Dict[str, Any]], Optional[Dict[str, Any]]) -> List[Dict[str, Any]]
"""
Add the missing frames from full_stack to frames and return the merged list.
"""
frame_ids = {
(
frame["abs_path"],
frame["context_line"],
frame["lineno"],
frame["function"],
)
for frame in frames
}

new_frames = [
stackframe
for stackframe in full_stack
if (
stackframe["abs_path"],
stackframe["context_line"],
stackframe["lineno"],
stackframe["function"],
)
not in frame_ids
]
new_frames.extend(frames)

# Limit the number of frames
max_stack_frames = (
client_options.get("max_stack_frames", DEFAULT_MAX_STACK_FRAMES)
if client_options
else None
)
if max_stack_frames is not None:
new_frames = new_frames[len(new_frames) - max_stack_frames :]

return new_frames


def event_from_exception(
exc_info, # type: Union[BaseException, ExcInfo]
client_options=None, # type: Optional[Dict[str, Any]]
Expand All @@ -1104,12 +1167,21 @@ def event_from_exception(
# type: (...) -> Tuple[Event, Dict[str, Any]]
exc_info = exc_info_from_error(exc_info)
hint = event_hint_with_exc_info(exc_info)

if client_options and client_options.get("add_full_stack", DEFAULT_ADD_FULL_STACK):
full_stack = current_stacktrace(
include_local_variables=client_options["include_local_variables"],
max_value_length=client_options["max_value_length"],
)["frames"]
else:
full_stack = None

return (
{
"level": "error",
"exception": {
"values": exceptions_from_error_tuple(
exc_info, client_options, mechanism
exc_info, client_options, mechanism, full_stack
)
},
},
Expand Down
103 changes: 103 additions & 0 deletions tests/test_full_stack_frames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import sentry_sdk


def test_full_stack_frames_default(sentry_init, capture_events):
sentry_init()
events = capture_events()

def foo():
try:
bar()
except Exception as e:
sentry_sdk.capture_exception(e)

def bar():
raise Exception("This is a test exception")

foo()

(event,) = events
frames = event["exception"]["values"][0]["stacktrace"]["frames"]

assert len(frames) == 2
assert frames[-1]["function"] == "bar"
assert frames[-2]["function"] == "foo"


def test_full_stack_frames_enabled(sentry_init, capture_events):
sentry_init(
add_full_stack=True,
)
events = capture_events()

def foo():
try:
bar()
except Exception as e:
sentry_sdk.capture_exception(e)

def bar():
raise Exception("This is a test exception")

foo()

(event,) = events
frames = event["exception"]["values"][0]["stacktrace"]["frames"]

assert len(frames) > 2
assert frames[-1]["function"] == "bar"
assert frames[-2]["function"] == "foo"
assert frames[-3]["function"] == "foo"
assert frames[-4]["function"] == "test_full_stack_frames_enabled"


def test_full_stack_frames_enabled_truncated(sentry_init, capture_events):
sentry_init(
add_full_stack=True,
max_stack_frames=3,
)
events = capture_events()

def foo():
try:
bar()
except Exception as e:
sentry_sdk.capture_exception(e)

def bar():
raise Exception("This is a test exception")

foo()

(event,) = events
frames = event["exception"]["values"][0]["stacktrace"]["frames"]

assert len(frames) == 3
assert frames[-1]["function"] == "bar"
assert frames[-2]["function"] == "foo"
assert frames[-3]["function"] == "foo"


def test_full_stack_frames_default_no_truncation_happening(sentry_init, capture_events):
sentry_init(
max_stack_frames=1, # this is ignored if add_full_stack=False (which is the default)
)
events = capture_events()

def foo():
try:
bar()
except Exception as e:
sentry_sdk.capture_exception(e)

def bar():
raise Exception("This is a test exception")

foo()

(event,) = events
frames = event["exception"]["values"][0]["stacktrace"]["frames"]

assert len(frames) == 2
assert frames[-1]["function"] == "bar"
assert frames[-2]["function"] == "foo"

0 comments on commit cda5127

Please sign in to comment.