Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing stack frames #3673

Merged
merged 19 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Loading