Skip to content

Commit fe9fa1d

Browse files
mitsuhikountitaker
authored andcommitted
feat: Added cycle detection (getsentry#236)
* feat: Added cycle detection * fix: Bugs in cycle detector * test: Simple cycle test * test: Better test * fix: Implement cycle detector for frame vars * fix: Linting
1 parent d5e0680 commit fe9fa1d

File tree

3 files changed

+88
-8
lines changed

3 files changed

+88
-8
lines changed

sentry_sdk/client.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
get_type_name,
1313
capture_internal_exceptions,
1414
current_stacktrace,
15+
break_cycles,
1516
logger,
1617
)
1718
from sentry_sdk.transport import make_transport
@@ -118,21 +119,23 @@ def _prepare_event(self, event, hint, scope):
118119
event, self.options["in_app_exclude"], self.options["in_app_include"]
119120
)
120121

122+
# Postprocess the event here so that annotated types do
123+
# generally not surface in before_send
124+
if event is not None:
125+
event = break_cycles(event)
126+
strip_event_mut(event)
127+
event = flatten_metadata(event)
128+
event = convert_types(event)
129+
121130
before_send = self.options["before_send"]
122131
if before_send is not None:
132+
new_event = None
123133
with capture_internal_exceptions():
124134
new_event = before_send(event, hint)
125135
if new_event is None:
126136
logger.info("before send dropped event (%s)", event)
127137
event = new_event
128138

129-
# Postprocess the event in the very end so that annotated types do
130-
# generally not surface in before_send
131-
if event is not None:
132-
strip_event_mut(event)
133-
event = flatten_metadata(event)
134-
event = convert_types(event)
135-
136139
return event
137140

138141
def _is_ignored_error(self, event, hint=None):

sentry_sdk/utils.py

+26-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
# The logger is created here but initialized in the debug support module
3232
logger = logging.getLogger("sentry_sdk.errors")
3333

34+
CYCLE_MARKER = object()
35+
3436

3537
def _get_debug_hub():
3638
# This function is replaced by debug.py
@@ -312,9 +314,13 @@ def _walk(obj, depth):
312314
return [_walk(x, depth + 1) for x in obj]
313315
if isinstance(obj, Mapping):
314316
return {safe_str(k): _walk(v, depth + 1) for k, v in obj.items()}
317+
318+
if obj is CYCLE_MARKER:
319+
return obj
320+
315321
return safe_repr(obj)
316322

317-
return _walk(obj, 0)
323+
return _walk(break_cycles(obj), 0)
318324

319325

320326
def extract_locals(frame):
@@ -615,7 +621,26 @@ def strip_frame_mut(frame):
615621
frame["vars"] = strip_databag(frame["vars"])
616622

617623

624+
def break_cycles(obj, memo=None):
625+
if memo is None:
626+
memo = {}
627+
if id(obj) in memo:
628+
return CYCLE_MARKER
629+
memo[id(obj)] = obj
630+
631+
try:
632+
if isinstance(obj, Mapping):
633+
return {k: break_cycles(v, memo) for k, v in obj.items()}
634+
if isinstance(obj, Sequence) and not isinstance(obj, (text_type, bytes)):
635+
return [break_cycles(v, memo) for v in obj]
636+
return obj
637+
finally:
638+
del memo[id(obj)]
639+
640+
618641
def convert_types(obj):
642+
if obj is CYCLE_MARKER:
643+
return u"<cyclic>"
619644
if isinstance(obj, datetime):
620645
return obj.strftime("%Y-%m-%dT%H:%M:%SZ")
621646
if isinstance(obj, Mapping):

tests/test_client.py

+52
Original file line numberDiff line numberDiff line change
@@ -313,3 +313,55 @@ def test_nan(sentry_init, capture_events):
313313
frames = event["exception"]["values"][0]["stacktrace"]["frames"]
314314
frame, = frames
315315
assert frame["vars"]["nan"] == "nan"
316+
317+
318+
def test_cyclic_frame_vars(sentry_init, capture_events):
319+
sentry_init()
320+
events = capture_events()
321+
322+
try:
323+
a = {}
324+
a["a"] = a
325+
1 / 0
326+
except Exception:
327+
capture_exception()
328+
329+
event, = events
330+
assert event["exception"]["values"][0]["stacktrace"]["frames"][0]["vars"]["a"] == {
331+
"a": "<cyclic>"
332+
}
333+
334+
335+
def test_cyclic_data(sentry_init, capture_events):
336+
sentry_init()
337+
events = capture_events()
338+
339+
with configure_scope() as scope:
340+
data = {}
341+
data["is_cyclic"] = data
342+
343+
other_data = ""
344+
data["not_cyclic"] = other_data
345+
data["not_cyclic2"] = other_data
346+
scope.set_extra("foo", data)
347+
348+
capture_message("hi")
349+
event, = events
350+
351+
data = event["extra"]["foo"]
352+
assert data == {"not_cyclic2": "", "not_cyclic": "", "is_cyclic": "<cyclic>"}
353+
354+
355+
def test_databag_stripping(sentry_init, capture_events):
356+
sentry_init()
357+
events = capture_events()
358+
359+
try:
360+
a = "A" * 16000 # noqa
361+
1 / 0
362+
except Exception:
363+
capture_exception()
364+
365+
event, = events
366+
367+
assert len(json.dumps(event)) < 10000

0 commit comments

Comments
 (0)