Skip to content

Commit 33115bf

Browse files
authored
feat: Added event hints and better exception handling
1 parent 0838daa commit 33115bf

File tree

11 files changed

+129
-74
lines changed

11 files changed

+129
-74
lines changed

sentry_sdk/api.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .hub import Hub
2+
from .utils import EventHint
23
from .client import Client, get_options
34
from .integrations import setup_integrations
45

@@ -38,11 +39,7 @@ def _init_on_current(*args, **kwargs):
3839

3940

4041
from . import minimal as sentry_minimal
42+
from .minimal import * # noqa
4143

42-
__all__ = ["Hub", "Scope", "Client", "init"] + sentry_minimal.__all__
43-
44-
45-
for _key in sentry_minimal.__all__:
46-
globals()[_key] = getattr(sentry_minimal, _key)
47-
globals()[_key].__module__ = __name__
48-
del _key
44+
__all__ = ["Hub", "Scope", "Client", "EventHint", "init"]
45+
__all__ += sentry_minimal.__all__

sentry_sdk/client.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
convert_types,
1212
handle_in_app,
1313
get_type_name,
14-
pop_hidden_keys,
1514
Dsn,
1615
)
1716
from .transport import Transport
@@ -83,12 +82,12 @@ def disabled(cls):
8382
"""Creates a guarnateed to be disabled client."""
8483
return cls(NO_DSN)
8584

86-
def _prepare_event(self, event, scope):
85+
def _prepare_event(self, event, scope, hint=None):
8786
if event.get("timestamp") is None:
8887
event["timestamp"] = datetime.utcnow()
8988

9089
if scope is not None:
91-
event = scope.apply_to_event(event)
90+
event = scope.apply_to_event(event, hint)
9291
if event is None:
9392
return
9493

@@ -111,16 +110,14 @@ def _prepare_event(self, event, scope):
111110
event = before_send(event)
112111

113112
if event is not None:
114-
pop_hidden_keys(event)
115113
event = flatten_metadata(event)
116114
event = convert_types(event)
117115

118116
return event
119117

120-
def _is_ignored_error(self, event):
121-
exc_info = event.get("__sentry_exc_info")
122-
123-
if not exc_info or exc_info[0] is None:
118+
def _is_ignored_error(self, event, hint=None):
119+
exc_info = hint and hint.exc_info or None
120+
if exc_info is None:
124121
return False
125122

126123
type_name = get_type_name(exc_info[0])
@@ -138,27 +135,27 @@ def _is_ignored_error(self, event):
138135

139136
return False
140137

141-
def _should_capture(self, event, scope=None):
138+
def _should_capture(self, event, scope=None, hint=None):
142139
if (
143140
self.options["sample_rate"] < 1.0
144141
and random.random() >= self.options["sample_rate"]
145142
):
146143
return False
147144

148-
if self._is_ignored_error(event):
145+
if self._is_ignored_error(event, hint):
149146
return False
150147

151148
return True
152149

153-
def capture_event(self, event, scope=None):
150+
def capture_event(self, event, scope=None, hint=None):
154151
"""Captures an event."""
155152
if self._transport is None:
156153
return
157154
rv = event.get("event_id")
158155
if rv is None:
159156
event["event_id"] = rv = uuid.uuid4().hex
160-
if self._should_capture(event, scope):
161-
event = self._prepare_event(event, scope)
157+
if self._should_capture(event, scope, hint):
158+
event = self._prepare_event(event, scope, hint)
162159
if event is not None:
163160
self._transport.capture_event(event)
164161
return True

sentry_sdk/hub.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def _internal_exceptions():
1515
try:
1616
yield
1717
except Exception:
18-
Hub.current.capture_internal_exception()
18+
Hub.current.capture_internal_exception(sys.exc_info())
1919

2020

2121
def _get_client_options():
@@ -99,11 +99,11 @@ def bind_client(self, new):
9999
top = self._stack[-1]
100100
self._stack[-1] = (new, top[1])
101101

102-
def capture_event(self, event):
102+
def capture_event(self, event, hint=None):
103103
"""Captures an event."""
104104
client, scope = self._stack[-1]
105105
if client is not None:
106-
return client.capture_event(event, scope)
106+
return client.capture_event(event, scope, hint)
107107

108108
def capture_message(self, message, level=None):
109109
"""Captures a message."""
@@ -123,15 +123,15 @@ def capture_exception(self, error=None):
123123
else:
124124
exc_info = exc_info_from_error(error)
125125

126-
event = event_from_exception(
126+
event, hint = event_from_exception(
127127
exc_info, with_locals=client.options["with_locals"]
128128
)
129129
try:
130-
return self.capture_event(event)
130+
return self.capture_event(event, hint=hint)
131131
except Exception:
132-
self.capture_internal_exception()
132+
self.capture_internal_exception(sys.exc_info())
133133

134-
def capture_internal_exception(self, error=None):
134+
def capture_internal_exception(self, exc_info):
135135
"""Capture an exception that is likely caused by a bug in the SDK
136136
itself."""
137137
pass

sentry_sdk/integrations/logging.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,10 @@ def _emit(self, record):
6464
return
6565

6666
with _internal_exceptions():
67+
hint = None
6768
# exc_info might be None or (None, None, None)
68-
if record.exc_info and all(record.exc_info):
69-
event = event_from_exception(
69+
if record.exc_info is not None and record.exc_info[0] is not None:
70+
event, hint = event_from_exception(
7071
record.exc_info, with_locals=hub.client.options["with_locals"]
7172
)
7273
else:
@@ -79,7 +80,7 @@ def _emit(self, record):
7980
"params": record.args,
8081
}
8182

82-
capture_event(event)
83+
capture_event(event, hint=hint)
8384

8485
with _internal_exceptions():
8586
add_breadcrumb(self._breadcrumb_from_record(record))

sentry_sdk/minimal.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ def public(f):
1010

1111

1212
@public
13-
def capture_event(event):
13+
def capture_event(event, hint=None):
1414
hub = Hub.current
1515
if hub is not None:
16-
return hub.capture_event(event)
16+
return hub.capture_event(event, hint)
1717

1818

1919
@public

sentry_sdk/scope.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,22 @@ def clear(self):
5757
def add_event_processor(self, func):
5858
self._event_processors.append(func)
5959

60-
def add_error_processor(self, func):
60+
def add_error_processor(self, func, cls=None):
61+
if cls is not None:
62+
real_func = func
63+
64+
def func(event, exc_info):
65+
try:
66+
is_inst = isinstance(exc_info[1], cls)
67+
except Exception:
68+
is_inst = False
69+
if is_inst:
70+
return real_func(event, exc_info)
71+
return event
72+
6173
self._error_processors.append(func)
6274

63-
def apply_to_event(self, event):
75+
def apply_to_event(self, event, hint=None):
6476
event.setdefault("breadcrumbs", []).extend(self._breadcrumbs)
6577
if event.get("user") is None and "user" in self._data:
6678
event["user"] = self._data["user"]
@@ -84,8 +96,8 @@ def apply_to_event(self, event):
8496
if contexts:
8597
event.setdefault("contexts", {}).update(contexts)
8698

87-
exc_info = event.get("__sentry_exc_info", None)
88-
if exc_info and exc_info[0] is not None:
99+
if hint is not None and hint.exc_info is not None:
100+
exc_info = hint.exc_info
89101
for processor in self._error_processors:
90102
event = processor(event, exc_info)
91103
if event is None:

sentry_sdk/utils.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,24 @@ def to_timestamp(value):
1515
return (value - epoch).total_seconds()
1616

1717

18+
class EventHint(object):
19+
"""Extra information for an event that can be used during processing."""
20+
21+
def __init__(self, exc_info=None):
22+
self.exc_info = exc_info
23+
24+
@classmethod
25+
def with_exc_info(cls, exc_info=None):
26+
"""Creates a hint with the exc info filled in."""
27+
if exc_info is None:
28+
exc_info = sys.exc_info()
29+
else:
30+
exc_info = exc_info_from_error(exc_info)
31+
if exc_info[0] is None:
32+
exc_info = None
33+
return cls(exc_info=exc_info)
34+
35+
1836
class BadDsn(ValueError):
1937
"""Raised on invalid DSNs."""
2038

@@ -383,11 +401,14 @@ def exc_info_from_error(error):
383401

384402
def event_from_exception(exc_info, with_locals=False, processors=None):
385403
exc_info = exc_info_from_error(exc_info)
386-
return {
387-
"level": "error",
388-
"exception": {"values": exceptions_from_error_tuple(exc_info, with_locals)},
389-
"__sentry_exc_info": exc_info,
390-
}
404+
hint = EventHint.with_exc_info(exc_info)
405+
return (
406+
{
407+
"level": "error",
408+
"exception": {"values": exceptions_from_error_tuple(exc_info, with_locals)},
409+
},
410+
hint,
411+
)
391412

392413

393414
def _module_in_set(name, set):
@@ -458,10 +479,6 @@ def strip_frame(frame):
458479
return frame, ({"vars": meta} if meta is not None else None)
459480

460481

461-
def pop_hidden_keys(event):
462-
event.pop("__sentry_exc_info", None)
463-
464-
465482
def convert_types(obj):
466483
if isinstance(obj, datetime):
467484
return obj.strftime("%Y-%m-%dT%H:%M:%SZ")

tests/conftest.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66

77
import sentry_sdk
8+
from sentry_sdk._compat import reraise
89
from sentry_sdk.client import Transport
910

1011
SEMAPHORE = "./checkouts/semaphore/target/debug/semaphore"
@@ -15,10 +16,8 @@
1516

1617
@pytest.fixture(autouse=True)
1718
def reraise_internal_exceptions(monkeypatch):
18-
def capture_internal_exception(error=None):
19-
if not error:
20-
raise
21-
raise error
19+
def capture_internal_exception(exc_info):
20+
reraise(*exc_info)
2221

2322
monkeypatch.setattr(
2423
sentry_sdk.get_current_hub(),
@@ -97,3 +96,20 @@ def close(self):
9796
pass
9897

9998
dsn = "LOL"
99+
100+
101+
@pytest.fixture
102+
def capture_events(monkeypatch):
103+
def inner():
104+
events = []
105+
test_client = sentry_sdk.Hub.current.client
106+
old_capture_event = test_client._transport.capture_event
107+
108+
def append(event):
109+
events.append(event)
110+
return old_capture_event(event)
111+
112+
monkeypatch.setattr(test_client._transport, "capture_event", append)
113+
return events
114+
115+
return inner

tests/integrations/conftest.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,3 @@ def capture_exception(error=None):
2323
return errors
2424

2525
return inner
26-
27-
28-
@pytest.fixture
29-
def capture_events(monkeypatch):
30-
def inner():
31-
events = []
32-
test_client = sentry_sdk.Hub.current.client
33-
old_capture_event = test_client._transport.capture_event
34-
35-
def append(event):
36-
events.append(event)
37-
return old_capture_event(event)
38-
39-
monkeypatch.setattr(test_client._transport, "capture_event", append)
40-
return events
41-
42-
return inner

tests/test_basics.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from sentry_sdk import configure_scope, capture_exception
2+
3+
4+
def test_processors(sentry_init, capture_events):
5+
sentry_init()
6+
events = capture_events()
7+
8+
with configure_scope() as scope:
9+
10+
def error_processor(event, exc_info):
11+
event["exception"]["values"][0]["value"] += " whatever"
12+
return event
13+
14+
scope.add_error_processor(error_processor, ValueError)
15+
16+
try:
17+
raise ValueError("aha!")
18+
except Exception:
19+
capture_exception()
20+
21+
event, = events
22+
23+
assert event["exception"]["values"][0]["value"] == "aha! whatever"

0 commit comments

Comments
 (0)