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

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
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 @@ -550,6 +553,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
106 changes: 101 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,73 @@ def exc_info_from_error(error):
return exc_info


def get_full_stack():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this replace the code in the logging plugin which does similar?

# type: () -> List[Dict[str, Any]]
"""
Returns a serialized representation of the full stack from the first frame that is not in sentry_sdk.
"""
stack_info = []

# Walk up the stack
frame = sys._getframe(1) # type: Optional[FrameType]
while frame:
in_sdk = False
try:
if "sentry_sdk" in frame.f_code.co_filename:
in_sdk = True
Comment on lines +1134 to +1135
Copy link
Member

@asottile-sentry asottile-sentry Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be better to use f_globals['__module__'] as this is a pretty weak assertion currently

except Exception:
pass

if not in_sdk:
stack_info.append(serialize_frame(frame))

frame = frame.f_back

stack_info.reverse()

return stack_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 +1194,18 @@ 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 = get_full_stack()
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