From d2d256a8778610d0a51015ccd09b72f266731464 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 16 May 2019 16:54:25 +0200 Subject: [PATCH 01/10] ref: Performance improvements when serializing events --- sentry_sdk/client.py | 8 +- sentry_sdk/integrations/django/__init__.py | 2 +- sentry_sdk/scope.py | 4 +- sentry_sdk/serializer.py | 259 +++++++++++++++++++++ sentry_sdk/utils.py | 251 +++----------------- tests/test_client.py | 25 +- tests/utils/test_event.py | 39 ---- tests/utils/test_general.py | 16 -- 8 files changed, 317 insertions(+), 287 deletions(-) create mode 100644 sentry_sdk/serializer.py delete mode 100644 tests/utils/test_event.py diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 15c869a55f..09fe466012 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -5,15 +5,13 @@ from sentry_sdk._compat import string_types, text_type from sentry_sdk.utils import ( - strip_event_mut, - flatten_metadata, - convert_types, handle_in_app, get_type_name, capture_internal_exceptions, current_stacktrace, logger, ) +from sentry_sdk.serializer import Serializer from sentry_sdk.transport import make_transport from sentry_sdk.consts import DEFAULT_OPTIONS, SDK_INFO from sentry_sdk.integrations import setup_integrations @@ -146,9 +144,7 @@ def _prepare_event( # Postprocess the event here so that annotated types do # generally not surface in before_send if event is not None: - event = convert_types(event) - strip_event_mut(event) - event = flatten_metadata(event) + event = Serializer().serialize_event(event) before_send = self.options["before_send"] if before_send is not None: diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 8e94f52e4e..fe8b7017a3 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -30,8 +30,8 @@ from sentry_sdk import Hub from sentry_sdk.hub import _should_send_default_pii from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.serializer import add_global_repr_processor from sentry_sdk.utils import ( - add_global_repr_processor, capture_internal_exceptions, event_from_exception, safe_repr, diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index ce4cb2c501..0e934671dc 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -3,7 +3,7 @@ from functools import wraps from itertools import chain -from sentry_sdk.utils import logger, capture_internal_exceptions, object_to_json +from sentry_sdk.utils import logger, capture_internal_exceptions if False: from typing import Any @@ -192,7 +192,7 @@ def _drop(event, cause, ty): event["fingerprint"] = self._fingerprint if self._extras: - event.setdefault("extra", {}).update(object_to_json(self._extras)) + event.setdefault("extra", {}).update(self._extras) if self._tags: event.setdefault("tags", {}).update(self._tags) diff --git a/sentry_sdk/serializer.py b/sentry_sdk/serializer.py new file mode 100644 index 0000000000..5010ff3e6e --- /dev/null +++ b/sentry_sdk/serializer.py @@ -0,0 +1,259 @@ +import contextlib +from datetime import datetime + +from sentry_sdk.utils import ( + AnnotatedValue, + LazyMap, + LazySequence, + capture_internal_exceptions, + safe_repr, + strip_string, +) + +from sentry_sdk._compat import text_type, PY2, string_types, number_types + +if False: + from typing import Any + from typing import Dict + from typing import List + from typing import Optional + + +if PY2: + # Importing ABCs from collections is deprecated, and will stop working in 3.8 + # https://github.com/python/cpython/blob/master/Lib/collections/__init__.py#L49 + from collections import Mapping, Sequence +else: + # New in 3.3 + # https://docs.python.org/3/library/collections.abc.html + from collections.abc import Mapping, Sequence + +MAX_DATABAG_DEPTH = 5 +MAX_DATABAG_BREADTH = 10 +CYCLE_MARKER = u"" + + +global_repr_processors = [] + + +def add_global_repr_processor(processor): + global_repr_processors.append(processor) + + +class MetaNode(object): + __slots__ = ( + "_parent", + "_segment", + "_depth", + "_data", + "_is_databag", + "_should_repr_strings", + ) + + def __init__(self): + self._parent = None + self._segment = None + self._depth = 0 + self._data = None + self._is_databag = None + self._should_repr_strings = None + + def startswith_path(self, path): + if len(path) > self._depth: + return False + + return self.is_path(path + [None] * (self._depth - len(path))) + + def is_path(self, path): + if len(path) != self._depth: + return False + + cur = self + for segment in reversed(path): + if segment is not None and segment != cur._segment: + return False + cur = cur._parent + + return cur._segment is None + + def enter(self, segment): + rv = MetaNode() + rv._parent = self + rv._depth = self._depth + 1 + rv._segment = segment + return rv + + def _create_annotations(self): + if self._data is not None: + return + + self._data = {} + if self._parent is not None: + self._parent._create_annotations() + self._parent._data[str(self._segment)] = self._data + + def annotate(self, **meta): + self._create_annotations() + self._data.setdefault("", {}).update(meta) + + def should_repr_strings(self): + if self._should_repr_strings is None: + self._should_repr_strings = ( + self.startswith_path( + ["exception", "values", None, "stacktrace", "frames", None, "vars"] + ) + or self.startswith_path( + ["threads", "values", None, "stacktrace", "frames", None, "vars"] + ) + or self.startswith_path(["stacktrace", "frames", None, "vars"]) + ) + + return self._should_repr_strings + + def is_databag(self): + if self._is_databag is None: + self._is_databag = ( + self.startswith_path(["request", "data"]) + or self.startswith_path(["breadcrumbs", None]) + or self.startswith_path(["extra"]) + or self.startswith_path( + ["exception", "values", None, "stacktrace", "frames", None, "vars"] + ) + or self.startswith_path( + ["threads", "values", None, "stacktrace", "frames", None, "vars"] + ) + or self.startswith_path(["stacktrace", "frames", None, "vars"]) + ) + + return self._is_databag + + +def _flatten_annotated(obj, meta_node): + if isinstance(obj, AnnotatedValue): + meta_node.annotate(**obj.metadata) + obj = obj.value + return obj + + +class Memo(object): + def __init__(self): + self._inner = {} + + @contextlib.contextmanager + def memoize(self, obj): + if id(obj) in self._inner: + yield True + else: + self._inner[id(obj)] = obj + yield False + + self._inner.pop(id(obj), None) + + +class Serializer(object): + def __init__(self): + self.memo = Memo() + self.meta_node = MetaNode() + + @contextlib.contextmanager + def enter(self, segment): + old_node = self.meta_node + self.meta_node = self.meta_node.enter(segment) + + try: + yield + finally: + self.meta_node = old_node + + def serialize_event(self, obj): + rv = self._serialize_node(obj) + if self.meta_node._data is not None: + rv["_meta"] = self.meta_node._data + return rv + + def _serialize_node(self, obj, **kwargs): + with capture_internal_exceptions(): + with self.memo.memoize(obj) as result: + if result: + return CYCLE_MARKER + + return self._serialize_node_impl(obj, **kwargs) + + return u"" + + def _serialize_node_impl(self, obj, max_depth=None, max_breadth=None): + # type: (Any, Optional[int], Optional[int]) -> Any + if max_depth is None and max_breadth is None and self.meta_node.is_databag(): + max_depth = self.meta_node._depth + MAX_DATABAG_DEPTH + max_breadth = self.meta_node._depth + MAX_DATABAG_BREADTH + + if max_depth is None: + remaining_depth = None + else: + remaining_depth = max_depth - self.meta_node._depth + + obj = _flatten_annotated(obj, self.meta_node) + + if max_depth is not None and remaining_depth <= 0: + self.meta_node.annotate(rem=[["!limit", "x"]]) + if self.meta_node.is_databag(): + return _flatten_annotated(strip_string(safe_repr(obj)), self.meta_node) + return None + + if self.meta_node.is_databag(): + hints = {"memo": self.memo, "remaining_depth": remaining_depth} + for processor in global_repr_processors: + with capture_internal_exceptions(): + result = processor(obj, hints) + if result is not NotImplemented: + return _flatten_annotated(result, self.meta_node) + + if isinstance(obj, (Mapping, LazyMap)): + rv_dict = {} # type: Dict[Any, Any] + for i, (k, v) in enumerate(obj.items()): + if max_breadth is not None and i >= max_breadth: + self.meta_node.annotate(len=max_breadth) + break + + with self.enter(k): + v = self._serialize_node( + v, max_depth=max_depth, max_breadth=max_breadth + ) + if v is not None: + rv_dict[k] = v + + return rv_dict + elif isinstance(obj, (LazySequence, Sequence)) and not isinstance( + obj, string_types + ): + rv_list = [] # type: List[Any] + for i, v in enumerate(obj): + if max_breadth is not None and i >= max_breadth: + self.meta_node.annotate(len=max_breadth) + break + + with self.enter(i): + rv_list.append( + self._serialize_node( + v, max_depth=max_depth, max_breadth=max_breadth + ) + ) + + return rv_list + + if self.meta_node.should_repr_strings(): + obj = safe_repr(obj) + else: + if obj is None or isinstance(obj, (bool, number_types)): + return obj + + if isinstance(obj, datetime): + return text_type(obj.strftime("%Y-%m-%dT%H:%M:%SZ")) + + if isinstance(obj, bytes): + obj = obj.decode("utf-8", "replace") + + if not isinstance(obj, string_types): + obj = safe_repr(obj) + + return _flatten_annotated(strip_string(obj), self.meta_node) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 912dbdb4ab..d9efb929eb 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -10,9 +10,8 @@ urlparse, text_type, implements_str, - string_types, - number_types, int_types, + iteritems, PY2, ) @@ -33,15 +32,6 @@ Optional[Type[BaseException]], Optional[BaseException], Optional[Any] ] -if PY2: - # Importing ABCs from collections is deprecated, and will stop working in 3.8 - # https://github.com/python/cpython/blob/master/Lib/collections/__init__.py#L49 - from collections import Mapping, Sequence -else: - # New in 3.3 - # https://docs.python.org/3/library/collections.abc.html - from collections.abc import Mapping, Sequence - epoch = datetime(1970, 1, 1) @@ -51,13 +41,6 @@ CYCLE_MARKER = object() -global_repr_processors = [] - - -def add_global_repr_processor(processor): - global_repr_processors.append(processor) - - def _get_debug_hub(): # This function is replaced by debug.py pass @@ -200,6 +183,33 @@ def to_header(self, timestamp=None): return u"Sentry " + u", ".join("%s=%s" % (key, value) for key, value in rv) +class LazyMap(object): + __slots__ = ("_iterator",) + + def __init__(self, iterator): + self._iterator = iterator + + def items(self): + return self._iterator + + +class LazySequence(object): + __slots__ = ("_iterator",) + + def __init__(self, iterator): + self._iterator = iterator + + def __iter__(self): + return iter(self._iterator) + + +class AnnotatedValue(object): + def __init__(self, value, metadata): + # type: (Optional[Any], Dict[str, Any]) -> None + self.value = value + self.metadata = metadata + + def get_type_name(cls): # type: (Any) -> str return getattr(cls, "__qualname__", None) or getattr(cls, "__name__", None) @@ -348,52 +358,9 @@ def safe_repr(value): return u"" -def object_to_json(obj, remaining_depth=4, memo=None): - with capture_internal_exceptions(): - if memo is None: - memo = Memo() - if memo.memoize(obj): - return CYCLE_MARKER - - try: - if remaining_depth > 0: - hints = {"memo": memo, "remaining_depth": remaining_depth} - for processor in global_repr_processors: - with capture_internal_exceptions(): - result = processor(obj, hints) - if result is not NotImplemented: - return result - - if isinstance(obj, (list, tuple)): - # It is not safe to iterate over another sequence types as this may raise errors or - # bring undesired side-effects (e.g. Django querysets are executed during iteration) - return [ - object_to_json( - x, remaining_depth=remaining_depth - 1, memo=memo - ) - for x in obj - ] - - if isinstance(obj, Mapping): - return { - safe_str(k): object_to_json( - v, remaining_depth=remaining_depth - 1, memo=memo - ) - for k, v in list(obj.items()) - } - - return safe_repr(obj) - finally: - memo.unmemoize(obj) - return u"" - - def extract_locals(frame): - # type: (Any) -> Dict[str, Any] - rv = {} - for key, value in frame.f_locals.items(): - rv[str(key)] = object_to_json(value) - return rv + # type: (Any) -> LazyMap + return LazyMap((str(key), value) for key, value in iteritems(frame.f_locals)) def filename_for_module(module, abs_path): @@ -698,164 +665,6 @@ def _module_in_set(name, set): return False -class AnnotatedValue(object): - def __init__(self, value, metadata): - # type: (Optional[Any], Dict[str, Any]) -> None - self.value = value - self.metadata = metadata - - -def flatten_metadata(obj): - # type: (Dict[str, Any]) -> Dict[str, Any] - def inner(obj): - # type: (Any) -> Any - if isinstance(obj, Mapping): - dict_rv = {} - meta = {} - for k, v in obj.items(): - # if we actually have "" keys in our data, throw them away. It's - # unclear how we would tell them apart from metadata - if k == "": - continue - - dict_rv[k], meta[k] = inner(v) - if meta[k] is None: - del meta[k] - if dict_rv[k] is None: - del dict_rv[k] - return dict_rv, (meta or None) - if isinstance(obj, Sequence) and not isinstance(obj, (text_type, bytes)): - list_rv = [] - meta = {} - for i, v in enumerate(obj): - new_v, meta[str(i)] = inner(v) - list_rv.append(new_v) - if meta[str(i)] is None: - del meta[str(i)] - return list_rv, (meta or None) - if isinstance(obj, AnnotatedValue): - return (inner(obj.value)[0], {"": obj.metadata}) - return obj, None - - obj, meta = inner(obj) - if meta is not None: - obj["_meta"] = meta - return obj - - -def strip_event_mut(event): - # type: (Dict[str, Any]) -> None - strip_stacktrace_mut(event.get("stacktrace", None)) - exception = event.get("exception", None) - if exception: - for exception in exception.get("values", None) or (): - strip_stacktrace_mut(exception.get("stacktrace", None)) - - strip_request_mut(event.get("request", None)) - strip_breadcrumbs_mut(event.get("breadcrumbs", None)) - - -def strip_stacktrace_mut(stacktrace): - # type: (Optional[Dict[str, List[Dict[str, Any]]]]) -> None - if not stacktrace: - return - for frame in stacktrace.get("frames", None) or (): - strip_frame_mut(frame) - - -def strip_request_mut(request): - # type: (Dict[str, Any]) -> None - if not request: - return - data = request.get("data", None) - if not data: - return - request["data"] = strip_databag(data) - - -def strip_breadcrumbs_mut(breadcrumbs): - if not breadcrumbs: - return - - for i in range(len(breadcrumbs)): - breadcrumbs[i] = strip_databag(breadcrumbs[i]) - - -def strip_frame_mut(frame): - # type: (Dict[str, Any]) -> None - if "vars" in frame: - frame["vars"] = strip_databag(frame["vars"]) - - -class Memo(object): - def __init__(self): - self._inner = {} - - def memoize(self, obj): - if id(obj) in self._inner: - return True - self._inner[id(obj)] = obj - return False - - def unmemoize(self, obj): - self._inner.pop(id(obj), None) - - -def convert_types(obj): - # type: (Any) -> Any - if obj is None: - return None - if obj is CYCLE_MARKER: - return u"" - if isinstance(obj, datetime): - return text_type(obj.strftime("%Y-%m-%dT%H:%M:%SZ")) - if isinstance(obj, Mapping): - return {k: convert_types(v) for k, v in obj.items()} - if isinstance(obj, Sequence) and not isinstance(obj, (text_type, bytes)): - return [convert_types(v) for v in obj] - if isinstance(obj, AnnotatedValue): - return AnnotatedValue(convert_types(obj.value), obj.metadata) - - if not isinstance(obj, string_types + number_types): - return safe_repr(obj) - if isinstance(obj, bytes): - return obj.decode("utf-8", "replace") - - return obj - - -def strip_databag(obj, remaining_depth=20, max_breadth=20): - # type: (Any, int, int) -> Any - assert not isinstance(obj, bytes), "bytes should have been normalized before" - if remaining_depth <= 0: - return AnnotatedValue(None, {"rem": [["!limit", "x"]]}) - if isinstance(obj, text_type): - return strip_string(obj) - if isinstance(obj, Mapping): - rv_dict = {} # type: Dict[Any, Any] - for i, (k, v) in enumerate(obj.items()): - if i >= max_breadth: - return AnnotatedValue(rv_dict, {"len": max_breadth}) - rv_dict[k] = strip_databag( - v, remaining_depth=remaining_depth - 1, max_breadth=max_breadth - ) - - return rv_dict - if isinstance(obj, Sequence): - rv_list = [] # type: List[Any] - for i, v in enumerate(obj): - if i >= max_breadth: - return AnnotatedValue(rv_list, {"len": max_breadth}) - rv_list.append( - strip_databag( - v, remaining_depth=remaining_depth - 1, max_breadth=max_breadth - ) - ) - - return rv_list - return obj - - def strip_string(value, max_length=512): # type: (str, int) -> Union[AnnotatedValue, str] # TODO: read max_length from config diff --git a/tests/test_client.py b/tests/test_client.py index 9f1637cf9b..46367bc4a4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -475,7 +475,7 @@ def test_cyclic_data(sentry_init, capture_events): event, = events data = event["extra"]["foo"] - assert data == {"not_cyclic2": "''", "not_cyclic": "''", "is_cyclic": ""} + assert data == {"not_cyclic2": "", "not_cyclic": "", "is_cyclic": ""} def test_databag_stripping(sentry_init, capture_events): @@ -559,7 +559,7 @@ def __repr__(self): event, = events assert ( event["exception"]["values"][0]["stacktrace"]["frames"][0]["vars"]["a"] - == "" + == "" ) @@ -576,3 +576,24 @@ class Foo(Exception): exception, = event["exception"]["values"] assert exception["mechanism"]["meta"]["errno"]["number"] == 69 + + +def test_non_string_variables(sentry_init, capture_events): + """There is some extremely terrible code in the wild that + inserts non-strings as variable names into `locals()`.""" + + sentry_init() + events = capture_events() + + try: + locals()[42] = True + 1 / 0 + except ZeroDivisionError: + capture_exception() + + event, = events + + exception, = event["exception"]["values"] + assert exception["type"] == "ZeroDivisionError" + frame, = exception["stacktrace"]["frames"] + assert frame["vars"]["42"] == "True" diff --git a/tests/utils/test_event.py b/tests/utils/test_event.py deleted file mode 100644 index 2bd0390d20..0000000000 --- a/tests/utils/test_event.py +++ /dev/null @@ -1,39 +0,0 @@ -import sys -import json - -from sentry_sdk.utils import ( - AnnotatedValue, - convert_types, - event_from_exception, - flatten_metadata, - strip_databag, - strip_event_mut, -) - - -def test_flatten_metadata(): - assert flatten_metadata({"foo": u"bar"}) == {"foo": u"bar"} - assert flatten_metadata({"foo": ["bar"]}) == {"foo": [u"bar"]} - assert flatten_metadata({"foo": [AnnotatedValue("bar", u"meta")]}) == { - "foo": [u"bar"], - "_meta": {"foo": {"0": {"": u"meta"}}}, - } - - -def test_strip_databag(): - d = strip_databag({"foo": u"a" * 2000}) - assert len(d["foo"].value) == 512 - - -def test_strip_exception_vars(): - try: - a = "A" * 16000 # noqa - 1 / 0 - except Exception: - event, _ = event_from_exception(sys.exc_info()) - - assert len(json.dumps(event)) > 10000 - strip_event_mut(event) - event = flatten_metadata(event) - event = convert_types(event) - assert len(json.dumps(event)) < 10000 diff --git a/tests/utils/test_general.py b/tests/utils/test_general.py index a21a755343..25b6a13327 100644 --- a/tests/utils/test_general.py +++ b/tests/utils/test_general.py @@ -55,22 +55,6 @@ def test_abs_path(): assert frame2["filename"] == "test.py" -def test_non_string_variables(): - """There is some extremely terrible code in the wild that - inserts non-strings as variable names into `locals()`.""" - - try: - locals()[42] = True - 1 / 0 - except ZeroDivisionError: - exceptions = exceptions_from_error_tuple(sys.exc_info()) - - exception, = exceptions - assert exception["type"] == "ZeroDivisionError" - frame, = exception["stacktrace"]["frames"] - assert frame["vars"]["42"] == "True" - - def test_format_and_strip(): max_length = None From 2cdb9c134a528497a9c7e6564ebfaa173522b2ea Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 16 May 2019 18:01:06 +0200 Subject: [PATCH 02/10] test: Add benchmarks --- .flake8 | 2 +- tests/conftest.py | 13 ++++++++++ tests/test_client.py | 60 ++++++++++++++++++++++++++++++++------------ 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/.flake8 b/.flake8 index d99adbe1fa..de30b59aec 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -ignore = E203, E266, E501, W503, E402, E731 +ignore = E203, E266, E501, W503, E402, E731, C901 max-line-length = 80 max-complexity = 18 select = B,C,E,F,W,T4,B9 diff --git a/tests/conftest.py b/tests/conftest.py index d37c14de0e..2753f4e132 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,19 @@ SEMAPHORE = None +try: + import pytest_benchmark +except ImportError: + + @pytest.fixture + def benchmark(): + return lambda x: x() + + +else: + del pytest_benchmark + + @pytest.fixture(autouse=True) def reraise_internal_exceptions(request, monkeypatch): errors = [] diff --git a/tests/test_client.py b/tests/test_client.py index 46367bc4a4..84943f3ae2 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -478,34 +478,62 @@ def test_cyclic_data(sentry_init, capture_events): assert data == {"not_cyclic2": "", "not_cyclic": "", "is_cyclic": ""} -def test_databag_stripping(sentry_init, capture_events): +def test_databag_depth_stripping(sentry_init, capture_events, benchmark): sentry_init() events = capture_events() - try: - a = "A" * 16000 # noqa - 1 / 0 - except Exception: - capture_exception() + value = ["a"] + for _ in range(100000): + value = [value] - event, = events + @benchmark + def inner(): + del events[:] + try: + a = value # noqa + 1 / 0 + except Exception: + capture_exception() + + event, = events - assert len(json.dumps(event)) < 10000 + assert len(json.dumps(event)) < 10000 -def test_databag_breadth_stripping(sentry_init, capture_events): +def test_databag_string_stripping(sentry_init, capture_events, benchmark): sentry_init() events = capture_events() - try: - a = ["a"] * 16000 # noqa - 1 / 0 - except Exception: - capture_exception() + @benchmark + def inner(): + del events[:] + try: + a = "A" * 1000000 # noqa + 1 / 0 + except Exception: + capture_exception() - event, = events + event, = events + + assert len(json.dumps(event)) < 10000 + + +def test_databag_breadth_stripping(sentry_init, capture_events, benchmark): + sentry_init() + events = capture_events() + + @benchmark + def inner(): + del events[:] + try: + a = ["a"] * 1000000 # noqa + 1 / 0 + except Exception: + capture_exception() + + event, = events - assert len(json.dumps(event)) < 10000 + assert len(json.dumps(event)) < 10000 @pytest.mark.skipif(not HAS_CHAINED_EXCEPTIONS, reason="Only works on 3.3+") From f9a8be8384916445351904ad19a21a56e8ac5a30 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 16 May 2019 19:10:36 +0200 Subject: [PATCH 03/10] fix: Fix some tests --- sentry_sdk/utils.py | 11 +++++++---- tests/integrations/django/test_basic.py | 6 ++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index d9efb929eb..a350438448 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -38,7 +38,8 @@ # The logger is created here but initialized in the debug support module logger = logging.getLogger("sentry_sdk.errors") -CYCLE_MARKER = object() +MAX_STRING_LENGTH = 512 +MAX_FORMAT_PARAM_LENGTH = 128 def _get_debug_hub(): @@ -249,7 +250,7 @@ def iter_stacks(tb): tb = tb.tb_next -def slim_string(value, length=512): +def slim_string(value, length=MAX_STRING_LENGTH): # type: (str, int) -> str if not value: return value @@ -682,7 +683,9 @@ def strip_string(value, max_length=512): return value -def format_and_strip(template, params, strip_string=strip_string): +def format_and_strip( + template, params, strip_string=strip_string, max_length=MAX_FORMAT_PARAM_LENGTH +): """Format a string containing %s for placeholders and call `strip_string` on each parameter. The string template itself does not have a maximum length. @@ -713,7 +716,7 @@ def realign_remark(remark): raise ValueError("Not enough params.") param = params.pop() - stripped_param = strip_string(param) + stripped_param = strip_string(param, max_length=max_length) if isinstance(stripped_param, AnnotatedValue): rv_remarks.extend( realign_remark(remark) for remark in stripped_param.metadata["rem"] diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index 55686a0929..a917beeb4b 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -289,7 +289,8 @@ def test_sql_queries_large_params(sentry_init, capture_events): with pytest.raises(OperationalError): # table doesn't even exist sql.execute( - """SELECT count(*) FROM people_person WHERE foo = %s""", ["x" * 1000] + """SELECT count(*) FROM people_person WHERE foo = %s and bar IS NULL""", + ["x" * 1000], ) capture_message("HI") @@ -298,7 +299,8 @@ def test_sql_queries_large_params(sentry_init, capture_events): crumb, = event["breadcrumbs"] assert crumb["message"] == ( - "SELECT count(*) FROM people_person WHERE foo = '%s..." % ("x" * 508,) + "SELECT count(*) FROM people_person WHERE foo = '%s... and bar IS NULL" + % ("x" * 124,) ) From 1ff54860efce34d69e3dba06d578b32b3d25c7ae Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 16 May 2019 20:51:00 +0200 Subject: [PATCH 04/10] fix: Fix test --- tests/utils/test_general.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/test_general.py b/tests/utils/test_general.py index 25b6a13327..562f9e0b39 100644 --- a/tests/utils/test_general.py +++ b/tests/utils/test_general.py @@ -62,7 +62,7 @@ def x(template, params): return format_and_strip( template, params, - strip_string=lambda x: strip_string(x, max_length=max_length), + strip_string=lambda x, **_: strip_string(x, max_length=max_length), ) max_length = 3 From d5acd5feb52b127be2ec715d382894fe5011292f Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 16 May 2019 21:00:53 +0200 Subject: [PATCH 05/10] ref: Replace everything with iteritems --- sentry_sdk/client.py | 4 ++-- sentry_sdk/integrations/_wsgi_common.py | 8 ++++---- sentry_sdk/integrations/logging.py | 3 ++- sentry_sdk/integrations/pyramid.py | 6 +++--- sentry_sdk/integrations/tornado.py | 7 ++++--- sentry_sdk/integrations/wsgi.py | 4 ++-- sentry_sdk/serializer.py | 4 ++-- sentry_sdk/utils.py | 3 +++ tests/conftest.py | 4 ++-- 9 files changed, 24 insertions(+), 19 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 09fe466012..f8ef0c53f3 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -3,7 +3,7 @@ import random from datetime import datetime -from sentry_sdk._compat import string_types, text_type +from sentry_sdk._compat import string_types, text_type, iteritems from sentry_sdk.utils import ( handle_in_app, get_type_name, @@ -41,7 +41,7 @@ def get_options(*args, **kwargs): if dsn is not None and options.get("dsn") is None: options["dsn"] = dsn # type: ignore - for key, value in options.items(): + for key, value in iteritems(options): if key not in rv: raise TypeError("Unknown option %r" % (key,)) rv[key] = value # type: ignore diff --git a/sentry_sdk/integrations/_wsgi_common.py b/sentry_sdk/integrations/_wsgi_common.py index b9825589ed..0940310c32 100644 --- a/sentry_sdk/integrations/_wsgi_common.py +++ b/sentry_sdk/integrations/_wsgi_common.py @@ -2,7 +2,7 @@ from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.utils import AnnotatedValue -from sentry_sdk._compat import text_type +from sentry_sdk._compat import text_type, iteritems if False: from typing import Any @@ -75,8 +75,8 @@ def parsed_body(self): form = self.form() files = self.files() if form or files: - data = dict(form.items()) - for k, v in files.items(): + data = dict(iteritems(form)) + for k, v in iteritems(files): size = self.size_of_file(v) data[k] = AnnotatedValue( "", {"len": size, "rem": [["!raw", "x", 0, size]]} @@ -130,6 +130,6 @@ def _filter_headers(headers): return { k: v - for k, v in headers.items() + for k, v in iteritems(headers) if k.lower().replace("_", "-") not in ("set-cookie", "cookie", "authorization") } diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py index bcbe86304b..a4aadd3c48 100644 --- a/sentry_sdk/integrations/logging.py +++ b/sentry_sdk/integrations/logging.py @@ -11,6 +11,7 @@ capture_internal_exceptions, ) from sentry_sdk.integrations import Integration +from sentry_sdk._compat import iteritems if False: from logging import LogRecord @@ -135,7 +136,7 @@ def _extra_from_record(record): # type: (LogRecord) -> Dict[str, None] return { k: v - for k, v in vars(record).items() + for k, v in iteritems(vars(record)) if k not in COMMON_RECORD_ATTRS and not k.startswith("_") } diff --git a/sentry_sdk/integrations/pyramid.py b/sentry_sdk/integrations/pyramid.py index a062bfb460..277aa1716b 100644 --- a/sentry_sdk/integrations/pyramid.py +++ b/sentry_sdk/integrations/pyramid.py @@ -9,7 +9,7 @@ from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.utils import capture_internal_exceptions, event_from_exception -from sentry_sdk._compat import reraise +from sentry_sdk._compat import reraise, iteritems from sentry_sdk.integrations import Integration from sentry_sdk.integrations._wsgi_common import RequestExtractor @@ -135,7 +135,7 @@ def form(self): # type: () -> Dict[str, str] return { key: value - for key, value in self.request.POST.items() + for key, value in iteritems(self.request.POST) if not getattr(value, "filename", None) } @@ -143,7 +143,7 @@ def files(self): # type: () -> Dict[str, cgi_FieldStorage] return { key: value - for key, value in self.request.POST.items() + for key, value in iteritems(self.request.POST) if getattr(value, "filename", None) } diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index 88d3a98720..e02fafef70 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -15,6 +15,7 @@ _is_json_content_type, ) from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk._compat import iteritems from tornado.web import RequestHandler, HTTPError # type: ignore from tornado.gen import coroutine # type: ignore @@ -166,7 +167,7 @@ def content_length(self): def cookies(self): # type: () -> Dict - return {k: v.value for k, v in self.request.cookies.items()} + return {k: v.value for k, v in iteritems(self.request.cookies)} def raw_data(self): # type: () -> bytes @@ -176,7 +177,7 @@ def form(self): # type: () -> Optional[Any] return { k: [v.decode("latin1", "replace") for v in vs] - for k, vs in self.request.body_arguments.items() + for k, vs in iteritems(self.request.body_arguments) } def is_json(self): @@ -185,7 +186,7 @@ def is_json(self): def files(self): # type: () -> Dict - return {k: v[0] for k, v in self.request.files.items() if v} + return {k: v[0] for k, v in iteritems(self.request.files) if v} def size_of_file(self, file): return len(file.body or ()) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index ecf9012ca8..f91f10ac67 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -2,7 +2,7 @@ from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.utils import capture_internal_exceptions, event_from_exception -from sentry_sdk._compat import PY2, reraise +from sentry_sdk._compat import PY2, reraise, iteritems from sentry_sdk.tracing import SpanContext from sentry_sdk.integrations._wsgi_common import _filter_headers @@ -118,7 +118,7 @@ def _get_headers(environ): Returns only proper HTTP headers. """ - for key, value in environ.items(): + for key, value in iteritems(environ): key = str(key) if key.startswith("HTTP_") and key not in ( "HTTP_CONTENT_TYPE", diff --git a/sentry_sdk/serializer.py b/sentry_sdk/serializer.py index 5010ff3e6e..d60c1baf97 100644 --- a/sentry_sdk/serializer.py +++ b/sentry_sdk/serializer.py @@ -10,7 +10,7 @@ strip_string, ) -from sentry_sdk._compat import text_type, PY2, string_types, number_types +from sentry_sdk._compat import text_type, PY2, string_types, number_types, iteritems if False: from typing import Any @@ -210,7 +210,7 @@ def _serialize_node_impl(self, obj, max_depth=None, max_breadth=None): if isinstance(obj, (Mapping, LazyMap)): rv_dict = {} # type: Dict[Any, Any] - for i, (k, v) in enumerate(obj.items()): + for i, (k, v) in enumerate(iteritems(obj)): if max_breadth is not None and i >= max_breadth: self.meta_node.annotate(len=max_breadth) break diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index a350438448..53d965a35c 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -193,6 +193,9 @@ def __init__(self, iterator): def items(self): return self._iterator + def iteritems(self): + return self._iterator + class LazySequence(object): __slots__ = ("_iterator",) diff --git a/tests/conftest.py b/tests/conftest.py index 2753f4e132..13192327a0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ import pytest import sentry_sdk -from sentry_sdk._compat import reraise, string_types +from sentry_sdk._compat import reraise, string_types, iteritems from sentry_sdk.transport import Transport SEMAPHORE = "./semaphore" @@ -50,7 +50,7 @@ def _(): def monkeypatch_test_transport(monkeypatch, assert_semaphore_acceptance): def check_event(event): def check_string_keys(map): - for key, value in map.items(): + for key, value in iteritems(map): assert isinstance(key, string_types) if isinstance(value, dict): check_string_keys(value) From 25b4fce6afc1dd258324955f4e3ea707229d3d98 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 16 May 2019 21:05:41 +0200 Subject: [PATCH 06/10] fix: Make code more readable, and fix mypy --- sentry_sdk/serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/serializer.py b/sentry_sdk/serializer.py index d60c1baf97..35e5111b9d 100644 --- a/sentry_sdk/serializer.py +++ b/sentry_sdk/serializer.py @@ -194,7 +194,7 @@ def _serialize_node_impl(self, obj, max_depth=None, max_breadth=None): obj = _flatten_annotated(obj, self.meta_node) - if max_depth is not None and remaining_depth <= 0: + if remaining_depth is not None and remaining_depth <= 0: self.meta_node.annotate(rem=[["!limit", "x"]]) if self.meta_node.is_databag(): return _flatten_annotated(strip_string(safe_repr(obj)), self.meta_node) From ca1a83a5c6c15b7396e2befc0181dc728a667852 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Fri, 17 May 2019 10:29:57 +0200 Subject: [PATCH 07/10] fix: Fix more tests --- pytest.ini | 1 + tests/conftest.py | 4 +++- tests/integrations/wsgi/test_wsgi.py | 6 +++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index 97ca37ea10..ca43883681 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,4 @@ [pytest] DJANGO_SETTINGS_MODULE = tests.integrations.django.myapp.settings addopts = --boxed --tb=short +markers = tests_internal_exceptions diff --git a/tests/conftest.py b/tests/conftest.py index 13192327a0..2f4ea5ebab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,7 +28,7 @@ def benchmark(): @pytest.fixture(autouse=True) -def reraise_internal_exceptions(request, monkeypatch): +def internal_exceptions(request, monkeypatch): errors = [] if "tests_internal_exceptions" in request.keywords: return @@ -45,6 +45,8 @@ def _(): sentry_sdk.Hub, "_capture_internal_exception", _capture_internal_exception ) + return errors + @pytest.fixture def monkeypatch_test_transport(monkeypatch, assert_semaphore_acceptance): diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index e4f17c9a02..5c319f2588 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -52,7 +52,9 @@ def test_basic(sentry_init, crashing_app, capture_events): } -def test_env_modifing_app(sentry_init, crashing_env_modifing_app, capture_events): +def test_env_modifing_app( + sentry_init, crashing_env_modifing_app, capture_events, internal_exceptions +): sentry_init(send_default_pii=True) app = SentryWsgiMiddleware(crashing_env_modifing_app) client = Client(app) @@ -62,3 +64,5 @@ def test_env_modifing_app(sentry_init, crashing_env_modifing_app, capture_events client.get("/") assert len(events) == 1 # only one exception is raised + assert len(internal_exceptions) == 1 + del internal_exceptions[:] From 88808c8113f13b1638d510a58abbccd3f38532fe Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Fri, 17 May 2019 15:28:37 +0200 Subject: [PATCH 08/10] fix: Fix more tests --- tests/integrations/wsgi/test_wsgi.py | 36 --------------------------- tests/test_client.py | 37 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index 5c319f2588..b63540c3f6 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -12,26 +12,6 @@ def app(environ, start_response): return app -@pytest.fixture -def crashing_env_modifing_app(): - class TooSmartClass(object): - def __init__(self, environ): - self.environ = environ - - def __repr__(self): - if "my_representation" in self.environ: - return self.environ["my_representation"] - - self.environ["my_representation"] = "" - return self.environ["my_representation"] - - def app(environ, start_response): - environ["tsc"] = TooSmartClass(environ) - 1 / 0 - - return app - - def test_basic(sentry_init, crashing_app, capture_events): sentry_init(send_default_pii=True) app = SentryWsgiMiddleware(crashing_app) @@ -50,19 +30,3 @@ def test_basic(sentry_init, crashing_app, capture_events): "query_string": "", "url": "http://localhost/", } - - -def test_env_modifing_app( - sentry_init, crashing_env_modifing_app, capture_events, internal_exceptions -): - sentry_init(send_default_pii=True) - app = SentryWsgiMiddleware(crashing_env_modifing_app) - client = Client(app) - events = capture_events() - - with pytest.raises(ZeroDivisionError): - client.get("/") - - assert len(events) == 1 # only one exception is raised - assert len(internal_exceptions) == 1 - del internal_exceptions[:] diff --git a/tests/test_client.py b/tests/test_client.py index 84943f3ae2..32d7feb6af 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -625,3 +625,40 @@ def test_non_string_variables(sentry_init, capture_events): assert exception["type"] == "ZeroDivisionError" frame, = exception["stacktrace"]["frames"] assert frame["vars"]["42"] == "True" + + +def test_dict_changed_during_iteration( + sentry_init, capture_events, internal_exceptions +): + """Some versions of Bottle modify the WSGI environment inside of a __repr__ impl""" + sentry_init(send_default_pii=True) + events = capture_events() + + class TooSmartClass(object): + def __init__(self, environ): + self.environ = environ + + def __repr__(self): + if "my_representation" in self.environ: + return self.environ["my_representation"] + + self.environ["my_representation"] = "" + return self.environ["my_representation"] + + try: + environ = {} + environ["aaaaa"] = TooSmartClass(environ) + 1 / 0 + except ZeroDivisionError: + capture_exception() + + event, = events + exception, = event["exception"]["values"] + frame, = exception["stacktrace"]["frames"] + assert ( + frame["vars"]["environ"] + == "" + ) + + assert len(internal_exceptions) == 1 + del internal_exceptions[:] From 3c826075be346182af1dc146d5e288b84715f102 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Fri, 17 May 2019 15:34:52 +0200 Subject: [PATCH 09/10] ref: Remove useless Lazy types --- sentry_sdk/serializer.py | 8 ++++---- sentry_sdk/utils.py | 30 +----------------------------- 2 files changed, 5 insertions(+), 33 deletions(-) diff --git a/sentry_sdk/serializer.py b/sentry_sdk/serializer.py index 35e5111b9d..9d9fc517bc 100644 --- a/sentry_sdk/serializer.py +++ b/sentry_sdk/serializer.py @@ -3,8 +3,6 @@ from sentry_sdk.utils import ( AnnotatedValue, - LazyMap, - LazySequence, capture_internal_exceptions, safe_repr, strip_string, @@ -208,13 +206,15 @@ def _serialize_node_impl(self, obj, max_depth=None, max_breadth=None): if result is not NotImplemented: return _flatten_annotated(result, self.meta_node) - if isinstance(obj, (Mapping, LazyMap)): + if isinstance(obj, Mapping): rv_dict = {} # type: Dict[Any, Any] for i, (k, v) in enumerate(iteritems(obj)): if max_breadth is not None and i >= max_breadth: self.meta_node.annotate(len=max_breadth) break + k = text_type(k) + with self.enter(k): v = self._serialize_node( v, max_depth=max_depth, max_breadth=max_breadth @@ -223,7 +223,7 @@ def _serialize_node_impl(self, obj, max_depth=None, max_breadth=None): rv_dict[k] = v return rv_dict - elif isinstance(obj, (LazySequence, Sequence)) and not isinstance( + elif isinstance(obj, Sequence) and not isinstance( obj, string_types ): rv_list = [] # type: List[Any] diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 53d965a35c..78a9c7a2b2 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -184,29 +184,6 @@ def to_header(self, timestamp=None): return u"Sentry " + u", ".join("%s=%s" % (key, value) for key, value in rv) -class LazyMap(object): - __slots__ = ("_iterator",) - - def __init__(self, iterator): - self._iterator = iterator - - def items(self): - return self._iterator - - def iteritems(self): - return self._iterator - - -class LazySequence(object): - __slots__ = ("_iterator",) - - def __init__(self, iterator): - self._iterator = iterator - - def __iter__(self): - return iter(self._iterator) - - class AnnotatedValue(object): def __init__(self, value, metadata): # type: (Optional[Any], Dict[str, Any]) -> None @@ -362,11 +339,6 @@ def safe_repr(value): return u"" -def extract_locals(frame): - # type: (Any) -> LazyMap - return LazyMap((str(key), value) for key, value in iteritems(frame.f_locals)) - - def filename_for_module(module, abs_path): # type: (str, str) -> str try: @@ -415,7 +387,7 @@ def serialize_frame(frame, tb_lineno=None, with_locals=True): "post_context": post_context, } if with_locals: - rv["vars"] = extract_locals(frame) + rv["vars"] = frame.f_locals return rv From 2189ac6255db56a0d85ed6466938dfa96d0e5709 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Fri, 17 May 2019 17:39:09 +0200 Subject: [PATCH 10/10] fix: Restore original fix for mutated dictionaries --- sentry_sdk/serializer.py | 17 ++++++++++++----- sentry_sdk/utils.py | 9 +-------- tests/test_client.py | 21 +++++++++------------ 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/sentry_sdk/serializer.py b/sentry_sdk/serializer.py index 9d9fc517bc..69712269c9 100644 --- a/sentry_sdk/serializer.py +++ b/sentry_sdk/serializer.py @@ -177,7 +177,10 @@ def _serialize_node(self, obj, **kwargs): return self._serialize_node_impl(obj, **kwargs) - return u"" + if self.meta_node.is_databag(): + return u"" + + return None def _serialize_node_impl(self, obj, max_depth=None, max_breadth=None): # type: (Any, Optional[int], Optional[int]) -> Any @@ -207,12 +210,18 @@ def _serialize_node_impl(self, obj, max_depth=None, max_breadth=None): return _flatten_annotated(result, self.meta_node) if isinstance(obj, Mapping): - rv_dict = {} # type: Dict[Any, Any] + # Create temporary list here to avoid calling too much code that + # might mutate our dictionary while we're still iterating over it. + items = [] for i, (k, v) in enumerate(iteritems(obj)): if max_breadth is not None and i >= max_breadth: self.meta_node.annotate(len=max_breadth) break + items.append((k, v)) + + rv_dict = {} # type: Dict[Any, Any] + for k, v in items: k = text_type(k) with self.enter(k): @@ -223,9 +232,7 @@ def _serialize_node_impl(self, obj, max_depth=None, max_breadth=None): rv_dict[k] = v return rv_dict - elif isinstance(obj, Sequence) and not isinstance( - obj, string_types - ): + elif isinstance(obj, Sequence) and not isinstance(obj, string_types): rv_list = [] # type: List[Any] for i, v in enumerate(obj): if max_breadth is not None and i >= max_breadth: diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 78a9c7a2b2..b997a5e1f1 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -6,14 +6,7 @@ from contextlib import contextmanager from datetime import datetime -from sentry_sdk._compat import ( - urlparse, - text_type, - implements_str, - int_types, - iteritems, - PY2, -) +from sentry_sdk._compat import urlparse, text_type, implements_str, int_types, PY2 if False: from typing import Any diff --git a/tests/test_client.py b/tests/test_client.py index 32d7feb6af..180a8f0ed9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -627,10 +627,13 @@ def test_non_string_variables(sentry_init, capture_events): assert frame["vars"]["42"] == "True" -def test_dict_changed_during_iteration( - sentry_init, capture_events, internal_exceptions -): - """Some versions of Bottle modify the WSGI environment inside of a __repr__ impl""" +def test_dict_changed_during_iteration(sentry_init, capture_events): + """ + Some versions of Bottle modify the WSGI environment inside of this __repr__ + impl: https://github.com/bottlepy/bottle/blob/0.12.16/bottle.py#L1386 + + See https://github.com/getsentry/sentry-python/pull/298 for discussion + """ sentry_init(send_default_pii=True) events = capture_events() @@ -647,7 +650,7 @@ def __repr__(self): try: environ = {} - environ["aaaaa"] = TooSmartClass(environ) + environ["a"] = TooSmartClass(environ) 1 / 0 except ZeroDivisionError: capture_exception() @@ -655,10 +658,4 @@ def __repr__(self): event, = events exception, = event["exception"]["values"] frame, = exception["stacktrace"]["frames"] - assert ( - frame["vars"]["environ"] - == "" - ) - - assert len(internal_exceptions) == 1 - del internal_exceptions[:] + assert frame["vars"]["environ"] == {"a": ""}