Skip to content
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
16 changes: 10 additions & 6 deletions ddtrace/errortracking/_handled_exceptions/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def _add_span_events(span: Span) -> None:
a span event for every handled exceptions, we store them in the span
and add them when the span finishes.
"""
span_exc_events = list(HandledExceptionCollector.get_exception_events(span.span_id).values())
exception_data = HandledExceptionCollector.get_exception_events(span.span_id).values()
span_exc_events = [event for _exc, event in exception_data]
if span_exc_events:
span._set_tag_str(SPAN_EVENTS_HAS_EXCEPTION, "true")
span._events.extend(span_exc_events)
Expand All @@ -30,13 +31,14 @@ def _add_span_events(span: Span) -> None:

def _on_span_exception(span, _exc_msg, exc_val, _exc_tb):
exception_events = HandledExceptionCollector.get_exception_events(span.span_id)
if exception_events and exc_val in exception_events:
del exception_events[exc_val]
exc_id = id(exc_val)
if exception_events and exc_id in exception_events:
del exception_events[exc_id]


class HandledExceptionCollector(Service):
_instance: t.Optional["HandledExceptionCollector"] = None
_span_exception_events: t.Dict[int, t.Dict[Exception, SpanEvent]] = {}
_span_exception_events: t.Dict[int, t.Dict[int, t.Tuple[Exception, SpanEvent]]] = {}

def __init__(self) -> None:
super(HandledExceptionCollector, self).__init__()
Expand Down Expand Up @@ -111,8 +113,10 @@ def capture_exception_event(cls, span: Span, exc: Exception, event: SpanEvent):
events_dict = cls._span_exception_events.setdefault(span_id, {})
if not events_dict:
span._add_on_finish_exception_callback(_add_span_events)
if exc in events_dict or len(events_dict) < COLLECTOR_MAX_SIZE_PER_SPAN:
events_dict[exc] = event
exc_id = id(exc)
if exc_id in events_dict or len(events_dict) < COLLECTOR_MAX_SIZE_PER_SPAN:
# Store both exception and event to keep exception alive and prevent ID reuse
events_dict[exc_id] = (exc, event)

@classmethod
def get_exception_events(cls, span_id: int):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
fixes:
- |
Error Tracking: Modifies the way exception events are stored such that the exception id is stored instead of the exception object, to prevent TypeErrors with custom exception objects.
18 changes: 18 additions & 0 deletions tests/errortracking/_test_functions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import asyncio


class UnhashableException(Exception):
def __init__(self, message, mutable_data):
super().__init__(message)
self.mutable_data = mutable_data

def __eq__(self, other):
# This makes the exception unhashable if __hash__ is not defined
return isinstance(other, UnhashableException) and str(self) == str(other)


def test_unhashable_exception_f(value):
try:
raise UnhashableException("unhashable error", {"key": "value"})
except UnhashableException:
value = 10
return value


def test_basic_try_except_f(value):
try:
raise ValueError("auto caught error")
Expand Down
25 changes: 23 additions & 2 deletions tests/errortracking/test_handled_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ def test_handle_same_error_multiple_times(self):
)
# This asserts that the reported span event contained the info
# of the last time the error is handled
assert "line 30" in self.spans[0]._events[0].attributes["exception.stacktrace"]
assert "line 48" in self.spans[0]._events[0].attributes["exception.stacktrace"], (
self.spans[0]._events[0].attributes["exception.stacktrace"]
)

@run_in_subprocess(env_overrides=dict(DD_ERROR_TRACKING_HANDLED_ERRORS="all"))
def test_handled_same_error_different_type(self):
Expand Down Expand Up @@ -159,11 +161,30 @@ def test_handled_in_parent_span(self):
self.spans[0].assert_span_event_attributes(
0, {"exception.type": "builtins.ValueError", "exception.message": "auto caught error"}
)
assert "line 72" in self.spans[0]._events[0].attributes["exception.stacktrace"]
assert "line 100" in self.spans[0]._events[0].attributes["exception.stacktrace"], (
self.spans[0]._events[0].attributes["exception.stacktrace"]
)

assert self.spans[1].name == "child_span"
assert len(self.spans[1]._events) == 0

@run_in_subprocess(env_overrides=dict(DD_ERROR_TRACKING_HANDLED_ERRORS="all"))
def test_unhashable_exception(self):
"""Test that unhashable exceptions (e.g., with mutable attributes) are handled correctly."""
self._run_error_test(
"test_unhashable_exception_f",
initial_value=0,
expected_value=10,
expected_events=[
[
{
"exception.type": "tests.errortracking._test_functions.UnhashableException",
"exception.message": "unhashable error",
}
]
],
)


@skipif_errortracking_not_supported
class UserCodeErrorTestCases(TracerTestCase):
Expand Down
Loading