8000 fix: Implement repr hooks, refactor cycle breaking (#247) · tb-lib/sentry-python@30eddcf · GitHub
[go: up one dir, main page]

Skip to content

Commit 30eddcf

Browse files
authored
fix: Implement repr hooks, refactor cycle breaking (getsentry#247)
* fix: Implement repr hooks, refactor cycle breaking * fix: Only run queryset_repr if integration is active * fix: Revert unrelated changes * fix: Do not use TLS if not a queryset * fix: Fix import * fix: Do not check for current hub in hook * test: Test new repr hooks in django * fix: Remove dead code * fix: Return unicode string * fix: Imports * fix: Linting
1 parent e252a20 commit 30eddcf

File tree

6 files changed

+91
-32
lines changed

6 files changed

+91
-32
lines changed

sentry_sdk/client.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
get_type_name,
1313
capture_internal_exceptions,
1414
current_stacktrace,
15-
break_cycles,
1615
logger,
1716
)
1817
from sentry_sdk.transport import make_transport
@@ -122,7 +121,6 @@ def _prepare_event(self, event, hint, scope):
122121
# Postprocess the event here so that annotated types do
123122
# generally not surface in before_send
124123
if event is not None:
125-
event = break_cycles(event)
126124
strip_event_mut(event)
127125
event = flatten_metadata(event)
128126
event = convert_types(event)

sentry_sdk/integrations/django/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import weakref
66

77
from django import VERSION as DJANGO_VERSION
8+
from django.db.models.query import QuerySet
89
from django.core import signals
910

1011
try:
@@ -31,6 +32,7 @@ def sql_to_string(sql):
3132
from sentry_sdk.hub import _should_send_default_pii
3233
from sentry_sdk.scope import add_global_event_processor
3334
from sentry_sdk.utils import (
35+
add_global_repr_processor,
3436
capture_internal_exceptions,
3537
event_from_exception,
3638
safe_repr,
@@ -140,6 +142,22 @@ def process_django_templates(event, hint):
140142

141143
return event
142144

145+
@add_global_repr_processor
146+
def _django_queryset_repr(value, hint):
147+
if not isinstance(value, QuerySet) or value._result_cache:
148+
return NotImplemented
149+
150+
# Do not call Hub.get_integration here. It is intentional that
151+
# running under a new hub does not suddenly start executing
152+
# querysets. This might be surprising to the user but it's likely
153+
# less annoying.
154+
155+
return u"<%s from %s at 0x%x>" % (
156+
value.__class__.__name__,
157+
value.__module__,
158+
id(value),
159+
)
160+
143161

144162
def _make_event_processor(weak_request, integration):
145163
def event_processor(event, hint):

sentry_sdk/scope.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from functools import wraps
44
from itertools import chain
55

6-
from sentry_sdk.utils import logger, capture_internal_exceptions
6+
from sentry_sdk.utils import logger, capture_internal_exceptions, object_to_json
77

88

99
global_event_processors = []
@@ -165,7 +165,7 @@ def _drop(event, cause, ty):
165165
event["fingerprint"] = self._fingerprint
166166

167167
if self._extras:
168-
event.setdefault("extra", {}).update(self._extras)
168+
event.setdefault("extra", {}).update(object_to_json(self._extras))
169169

170170
if self._tags:
171171
event.setdefault("tags", {}).update(self._tags)

sentry_sdk/utils.py

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@
3434
CYCLE_MARKER = object()
3535

3636

37+
global_repr_processors = []
38+
39+
40+
def add_global_repr_processor(processor):
41+
global_repr_processors.append(processor)
42+
43+
3744
def _get_debug_hub():
3845
# This function is replaced by debug.py
3946
pass
@@ -307,22 +314,40 @@ def safe_repr(value):
307314
return u"<broken repr>"
308315

309316

310-
def object_to_json(obj):
311-
def _walk(obj, depth):
312-
if depth < 4:
317+
def object_to_json(obj, remaining_depth=4, memo=None):
318+
if memo is None:
319+
memo = Memo()
320+
if memo.memoize(obj):
321+
return CYCLE_MARKER
322+
323+
try:
324+
if remaining_depth > 0:
325+
hints = {"memo": memo, "remaining_depth": remaining_depth}
326+
for processor in global_repr_processors:
327+
with capture_internal_exceptions():
328+
result = processor(obj, hints)
329+
if result is not NotImplemented:
330+
return result
331+
313332
if isinstance(obj, (list, tuple)):
314333
# It is not safe to iterate over another sequence types as this may raise errors or
315334
# bring undesired side-effects (e.g. Django querysets are executed during iteration)
316-
return [_walk(x, depth + 1) for x in obj]
317-
if isinstance(obj, Mapping):
318-
return {safe_str(k): _walk(v, depth + 1) for k, v in obj.items()}
335+
return [
336+
object_to_json(x, remaining_depth=remaining_depth - 1, memo=memo)
337+
for x in obj
338+
]
319339

320-
if obj is CYCLE_MARKER:
321-
return obj
340+
if isinstance(obj, Mapping):
341+
return {
342+
safe_str(k): object_to_json(
343+
v, remaining_depth=remaining_depth - 1, memo=memo
344+
)
345+
for k, v in obj.items()
346+
}
322347

323348
return safe_repr(obj)
324-
325-
return _walk(break_cycles(obj), 0)
349+
finally:
350+
memo.unmemoize(obj)
326351

327352

328353
def extract_locals(frame):
@@ -645,23 +670,18 @@ def strip_frame_mut(frame):
645670
frame["vars"] = strip_databag(frame["vars"])
646671

647672

648-
def break_cycles(obj, memo=None):
649-
if memo is None:
650-
memo = {}
651-
if id(obj) in memo:
652-
return CYCLE_MARKER
653-
memo[id(obj)] = obj
673+
class Memo(object):
674+
def __init__(self):
675+
self._inner = {}
654676

655-
try:
656-
if isinstance(obj, Mapping):
657-
return {k: break_cycles(v, memo) for k, v in obj.items()}
658-
if isinstance(obj, (list, tuple)):
659-
# It is not safe to iterate over another sequence types as this may raise errors or
660-
# bring undesired side-effects (e.g. Django querysets are executed during iteration)
661-
return [break_cycles(v, memo) for v in obj]
662-
return obj
663-
finally:
664-
del memo[id(obj)]
677+
def memoize(self, obj):
678+
if id(obj) in self._inner:
679+
return True
680+
self._inner[id(obj)] = obj
681+
return False
682+
683+
def unmemoize(self, obj):
684+
self._inner.pop(id(obj), None)
665685

666686

667687
def convert_types(obj):

tests/integrations/django/test_basic.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44

55
from werkzeug.test import Client
6+
from django.contrib.auth.models import User
67
from django.core.management import execute_from_command_line
78
from django.db.utils import OperationalError
89

@@ -12,7 +13,7 @@
1213
except ImportError:
1314
from django.core.urlresolvers import reverse
1415

15-
from sentry_sdk import capture_message
16+
from sentry_sdk import capture_message, capture_exception
1617
from sentry_sdk.integrations.django import DjangoIntegration
1718

1819
from tests.integrations.django.myapp.wsgi import application
@@ -101,6 +102,28 @@ def test_user_captured(sentry_init, client, capture_events):
101102
}
102103

103104

105+@pytest.mark.django_db
106+
def test_queryset_repr(sentry_init, capture_events):
107+
sentry_init(integrations=[DjangoIntegration()])
108+
events = capture_events()
109+
User.objects.create_user("john", "lennon@thebeatles.com", "johnpassword")
110+
111+
try:
112+
my_queryset = User.objects.all() # noqa
113+
1 / 0
114+
except Exception:
115+
capture_exception()
116+
117+
event, = events
118+
119+
exception, = event["exception"]["values"]
120+
assert exception["type"] == "ZeroDivisionError"
121+
frame, = exception["stacktrace"]["frames"]
122+
assert frame["vars"]["my_queryset"].startswith(
123+
"<QuerySet from django.db.models.query at"
124+
)
125+
126+
104127
def test_custom_error_handler_request_context(sentry_init, client, capture_events):
105128
sentry_init(integrations=[DjangoIntegration()])
106129
events = capture_events()

tests/test_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ def test_cyclic_data(sentry_init, capture_events):
350350
event, = events
351351

352352
data = event["extra"]["foo"]
353-
assert data == {"not_cyclic2": "", "not_cyclic": "", "is_cyclic": "<cyclic>"}
353+
assert data == {"not_cyclic2": "''", "not_cyclic": "''", "is_cyclic": "<cyclic>"}
354354

355355

356356
def test_databag_stripping(sentry_init, capture_events):

0 commit comments

Comments
 (0)
0