Skip to content

Commit e8ba3c9

Browse files
committed
Make a deeper copy of the vars object with fallback to safe_repr
To make this work, I had to add a port of python's `copy.deepcopy` (license included) that falls back to `safe_repr` instead of the pickling step. This ensures that we have a deepcopy of all data structure like objects - dicts/lists/tuples and so on and we make a repr early for other random objects.
1 parent 81f5ce6 commit e8ba3c9

File tree

3 files changed

+223
-1
lines changed

3 files changed

+223
-1
lines changed

sentry_sdk/_copy.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
"""
2+
A modified version of Python 3.11's copy.deepcopy (found in Python's 'cpython/Lib/copy.py')
3+
that falls back to repr for non-datastrucure types that we use for extracting frame local variables
4+
in a safe way without holding references to the original objects.
5+
6+
https://github.com/python/cpython/blob/v3.11.7/Lib/copy.py#L128-L241
7+
8+
Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
9+
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
10+
11+
All Rights Reserved
12+
13+
14+
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
15+
--------------------------------------------
16+
17+
1. This LICENSE AGREEMENT is between the Python Software Foundation
18+
("PSF"), and the Individual or Organization ("Licensee") accessing and
19+
otherwise using this software ("Python") in source or binary form and
20+
its associated documentation.
21+
22+
2. Subject to the terms and conditions of this License Agreement, PSF hereby
23+
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
24+
analyze, test, perform and/or display publicly, prepare derivative works,
25+
distribute, and otherwise use Python alone or in any derivative version,
26+
provided, however, that PSF's License Agreement and PSF's notice of copyright,
27+
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
28+
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
29+
All Rights Reserved" are retained in Python alone or in any derivative version
30+
prepared by Licensee.
31+
32+
3. In the event Licensee prepares a derivative work that is based on
33+
or incorporates Python or any part thereof, and wants to make
34+
the derivative work available to others as provided herein, then
35+
Licensee hereby agrees to include in any such work a brief summary of
36+
the changes made to Python.
37+
38+
4. PSF is making Python available to Licensee on an "AS IS"
39+
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
40+
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
41+
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
42+
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
43+
INFRINGE ANY THIRD PARTY RIGHTS.
44+
45+
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
46+
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
47+
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
48+
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
49+
50+
6. This License Agreement will automatically terminate upon a material
51+
breach of its terms and conditions.
52+
53+
7. Nothing in this License Agreement shall be deemed to create any
54+
relationship of agency, partnership, or joint venture between PSF and
55+
Licensee. This License Agreement does not grant permission to use PSF
56+
trademarks or trade name in a trademark sense to endorse or promote
57+
products or services of Licensee, or any third party.
58+
59+
8. By copying, installing or otherwise using Python, Licensee
60+
agrees to be bound by the terms and conditions of this License
61+
Agreement.
62+
63+
"""
64+
65+
import types
66+
import weakref
67+
68+
from sentry_sdk.utils import safe_repr
69+
from sentry_sdk._types import TYPE_CHECKING
70+
71+
if TYPE_CHECKING:
72+
from typing import Any, Optional, Callable
73+
74+
75+
def deepcopy_fallback_repr(x, memo=None, _nil=[]): # noqa: B006
76+
# type: (Any, Optional[dict[int, Any]], Any) -> Any
77+
"""Deep copy like operation on arbitrary Python objects that falls back to repr
78+
for non-datastructure like objects.
79+
"""
80+
81+
if memo is None:
82+
memo = {}
83+
84+
d = id(x)
85+
y = memo.get(d, _nil)
86+
if y is not _nil:
87+
return y
88+
89+
cls = type(x)
90+
91+
copier = _deepcopy_dispatch.get(cls)
92+
if copier is not None:
93+
y = copier(x, memo)
94+
elif issubclass(cls, type):
95+
y = _deepcopy_atomic(x, memo)
96+
else:
97+
y = safe_repr(x)
98+
99+
# If is its own copy, don't memoize.
100+
if y is not x:
101+
memo[d] = y
102+
_keep_alive(x, memo) # Make sure x lives at least as long as d
103+
return y
104+
105+
106+
_deepcopy_dispatch = d = {} # type: dict[Any, Any]
107+
108+
109+
def _deepcopy_atomic(x, memo):
110+
# type: (Any, dict[int, Any]) -> Any
111+
return x
112+
113+
114+
d[type(None)] = _deepcopy_atomic
115+
d[type(Ellipsis)] = _deepcopy_atomic
116+
d[type(NotImplemented)] = _deepcopy_atomic
117+
d[int] = _deepcopy_atomic
118+
d[float] = _deepcopy_atomic
119+
d[bool] = _deepcopy_atomic
120+
d[complex] = _deepcopy_atomic
121+
d[bytes] = _deepcopy_atomic
122+
d[str] = _deepcopy_atomic
123+
d[types.CodeType] = _deepcopy_atomic
124+
d[type] = _deepcopy_atomic
125+
d[range] = _deepcopy_atomic
126+
d[types.BuiltinFunctionType] = _deepcopy_atomic
127+
d[types.FunctionType] = _deepcopy_atomic
128+
d[weakref.ref] = _deepcopy_atomic
129+
d[property] = _deepcopy_atomic
130+
131+
132+
def _deepcopy_list(x, memo, deepcopy=deepcopy_fallback_repr):
133+
# type: (list[Any], dict[int, Any], Callable[..., Any]) -> list[Any]
134+
y = [] # type: list[Any]
135+
memo[id(x)] = y
136+
append = y.append
137+
for a in x:
138+
append(deepcopy(a, memo))
139+
return y
140+
141+
142+
d[list] = _deepcopy_list
143+
144+
145+
def _deepcopy_tuple(x, memo, deepcopy=deepcopy_fallback_repr):
146+
# type: (tuple[Any, ...], dict[int, Any], Callable[..., Any]) -> tuple[Any, ...]
147+
z = [deepcopy(a, memo) for a in x]
148+
# We're not going to put the tuple in the memo, but it's still important we
149+
# check for it, in case the tuple contains recursive mutable structures.
150+
try:
151+
return memo[id(x)]
152+
except KeyError:
153+
pass
154+
for k, j in zip(x, z):
155+
if k is not j:
156+
y = tuple(z)
157+
break
158+
else:
159+
y = x
160+
return y
161+
162+
163+
d[tuple] = _deepcopy_tuple
164+
165+
166+
def _deepcopy_dict(x, memo, deepcopy=deepcopy_fallback_repr):
167+
# type: (dict[Any, Any], dict[int, Any], Callable[..., Any]) -> dict[Any, Any]
168+
y = {} # type: dict[Any, Any]
169+
memo[id(x)] = y
170+
for key, value in x.items():
171+
y[deepcopy(key, memo)] = deepcopy(value, memo)
172+
return y
173+
174+
175+
d[dict] = _deepcopy_dict
176+
177+
178+
def _deepcopy_method(x, memo): # Copy instance methods
179+
# type: (types.MethodType, dict[int, Any]) -> types.MethodType
180+
return type(x)(x.__func__, deepcopy_fallback_repr(x.__self__, memo))
181+
182+
183+
d[types.MethodType] = _deepcopy_method
184+
185+
del d
186+
187+
188+
def _keep_alive(x, memo):
189+
# type: (Any, dict[int, Any]) -> None
190+
"""Keeps a reference to the object x in the memo.
191+
192+
Because we remember objects by their id, we have
193+
to assure that possibly temporary objects are kept
194+
alive by referencing them.
195+
We store a reference at the id of the memo, which should
196+
normally not be used unless someone tries to deepcopy
197+
the memo itself...
198+
"""
199+
try:
200+
memo[id(memo)].append(x)
201+
except KeyError:
202+
# aha, this is the first one :-)
203+
memo[id(memo)] = [x]

sentry_sdk/utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,9 @@ def serialize_frame(
616616
)
617617

618618
if include_local_variables:
619-
rv["vars"] = frame.f_locals.copy()
619+
from sentry_sdk._copy import deepcopy_fallback_repr
620+
621+
rv["vars"] = deepcopy_fallback_repr(frame.f_locals)
620622

621623
return rv
622624

tests/test_scrubber.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,20 @@ def test_recursive_event_scrubber(sentry_init, capture_events):
187187

188188
(event,) = events
189189
assert event["extra"]["deep"]["deeper"][0]["deepest"]["password"] == "'[Filtered]'"
190+
191+
192+
def test_recursive_scrubber_does_not_override_original(sentry_init, capture_events):
193+
sentry_init(event_scrubber=EventScrubber(recursive=True))
194+
events = capture_events()
195+
196+
data = {"csrf": "secret"}
197+
try:
198+
raise RuntimeError("An error")
199+
except Exception:
200+
capture_exception()
201+
202+
(event,) = events
203+
frames = event["exception"]["values"][0]["stacktrace"]["frames"]
204+
(frame,) = frames
205+
assert data["csrf"] == "secret"
206+
assert frame["vars"]["data"]["csrf"] == "[Filtered]"

0 commit comments

Comments
 (0)