Skip to content

Commit 6057951

Browse files
authored
feat: New processor system (#34)
1 parent 98399ca commit 6057951

File tree

22 files changed

+543
-450
lines changed

22 files changed

+543
-450
lines changed

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ dist:
1010
test: .venv
1111
@pip install -r test-requirements.txt
1212
@pip install --editable .
13-
@pytest tests
13+
@pytest tests --tb=short
1414
.PHONY: test
1515

16+
format:
17+
@black sentry_sdk tests
18+
.PHONY: format
19+
1620
tox-test:
1721
@sh ./scripts/runtox.sh
1822
.PHONY: tox-test

sentry_sdk/_compat.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
text_type = unicode # noqa
1010
import Queue as queue # noqa
1111

12+
string_types = (str, text_type)
1213
number_types = (int, long, float) # noqa
1314

1415
def implements_str(cls):
@@ -29,6 +30,7 @@ def implements_iterator(cls):
2930
import queue # noqa
3031

3132
text_type = str
33+
string_types = (text_type,)
3234
number_types = (int, float)
3335

3436
def _identity(x):

sentry_sdk/api.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .hub import Hub
2-
from .client import Client
2+
from .client import Client, get_options
3+
from .integrations import setup_integrations
34

45

56
class _InitGuard(object):
@@ -15,12 +16,27 @@ def __exit__(self, exc_type, exc_value, tb):
1516
c.close()
1617

1718

18-
def init(*args, **kwargs):
19-
client = Client(*args, **kwargs)
20-
Hub.main.bind_client(client)
19+
def _init_on_hub(hub, args, kwargs):
20+
options = get_options(*args, **kwargs)
21+
install = setup_integrations(options)
22+
client = Client(options)
23+
hub.bind_client(client)
24+
install()
2125
return _InitGuard(client)
2226

2327

28+
def init(*args, **kwargs):
29+
"""Initializes the SDK and optionally integrations."""
30+
return _init_on_hub(Hub.main, args, kwargs)
31+
32+
33+
def _init_on_current(*args, **kwargs):
34+
# This function only exists to support unittests. Do not call it as
35+
# initializing integrations on anything but the main hub is not going
36+
# to yield the results you expect.
37+
return _init_on_hub(Hub.current, args, kwargs)
38+
39+
2440
from . import minimal as sentry_minimal
2541

2642
__all__ = ["Hub", "Scope", "Client", "init"] + sentry_minimal.__all__

sentry_sdk/client.py

Lines changed: 92 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,57 @@
22
import uuid
33
import random
44
import atexit
5-
6-
from .utils import Dsn, SkipEvent, ContextVar
5+
import weakref
6+
from datetime import datetime
7+
8+
from ._compat import string_types
9+
from .utils import (
10+
strip_event,
11+
flatten_metadata,
12+
convert_types,
13+
handle_in_app,
14+
get_type_name,
15+
pop_hidden_keys,
16+
Dsn,
17+
)
718
from .transport import Transport
819
from .consts import DEFAULT_OPTIONS, SDK_INFO
9-
from .event import strip_event, flatten_metadata, convert_types, Event
1020

1121

1222
NO_DSN = object()
1323

14-
_most_recent_exception = ContextVar("sentry_most_recent_exception")
1524

25+
def get_options(*args, **kwargs):
26+
if args and (isinstance(args[0], string_types) or args[0] is None):
27+
dsn = args[0]
28+
args = args[1:]
29+
else:
30+
dsn = None
31+
32+
rv = dict(DEFAULT_OPTIONS)
33+
options = dict(*args, **kwargs)
34+
if dsn is not None and options.get("dsn") is None:
35+
options["dsn"] = dsn
36+
37+
for key, value in options.items():
38+
if key not in rv:
39+
raise TypeError("Unknown option %r" % (key,))
40+
rv[key] = value
1641

17-
def _get_default_integrations():
18-
from .integrations.logging import LoggingIntegration
19-
from .integrations.excepthook import ExcepthookIntegration
42+
if rv["dsn"] is None:
43+
rv["dsn"] = os.environ.get("SENTRY_DSN")
2044

21-
yield LoggingIntegration
22-
yield ExcepthookIntegration
45+
return rv
2346

2447

2548
class Client(object):
26-
def __init__(self, dsn=None, *args, **kwargs):
27-
passed_dsn = dsn
28-
if dsn is NO_DSN:
29-
dsn = None
30-
else:
31-
if dsn is None:
32-
dsn = os.environ.get("SENTRY_DSN")
33-
if not dsn:
34-
dsn = None
35-
else:
36-
dsn = Dsn(dsn)
37-
options = dict(DEFAULT_OPTIONS)
38-
options.update(*args, **kwargs)
49+
def __init__(self, *args, **kwargs):
50+
options = get_options(*args, **kwargs)
51+
52+
dsn = options["dsn"]
53+
if dsn is not None:
54+
dsn = Dsn(dsn)
55+
3956
self.options = options
4057
self._transport = self.options.pop("transport")
4158
if self._transport is None and dsn is not None:
@@ -45,18 +62,6 @@ def __init__(self, dsn=None, *args, **kwargs):
4562
https_proxy=self.options.pop("https_proxy"),
4663
)
4764
self._transport.start()
48-
elif passed_dsn is not None and self._transport is not None:
49-
raise ValueError("Cannot pass DSN and a custom transport.")
50-
51-
integrations = list(options.pop("integrations") or ())
52-
53-
if options["default_integrations"]:
54-
for cls in _get_default_integrations():
55-
if not any(isinstance(x, cls) for x in integrations):
56-
integrations.append(cls())
57-
58-
for integration in integrations:
59-
integration(self)
6065

6166
request_bodies = ("always", "never", "small", "medium")
6267
if options["request_bodies"] not in request_bodies:
@@ -66,6 +71,8 @@ def __init__(self, dsn=None, *args, **kwargs):
6671
)
6772
)
6873

74+
self._exceptions_seen = weakref.WeakKeyDictionary()
75+
6976
atexit.register(self.close)
7077

7178
@property
@@ -80,11 +87,13 @@ def disabled(cls):
8087
return cls(NO_DSN)
8188

8289
def _prepare_event(self, event, scope):
83-
if event.get("event_id") is None:
84-
event["event_id"] = uuid.uuid4().hex
90+
if event.get("timestamp") is None:
91+
event["timestamp"] = datetime.utcnow()
8592

8693
if scope is not None:
87-
scope.apply_to_event(event)
94+
event = scope.apply_to_event(event)
95+
if event is None:
96+
return
8897

8998
for key in "release", "environment", "server_name", "repos", "dist":
9099
if event.get(key) is None:
@@ -95,41 +104,67 @@ def _prepare_event(self, event, scope):
95104
if event.get("platform") is None:
96105
event["platform"] = "python"
97106

107+
event = handle_in_app(
108+
event, self.options["in_app_exclude"], self.options["in_app_include"]
109+
)
98110
event = strip_event(event)
99-
event = flatten_metadata(event)
100-
event = convert_types(event)
111+
112+
before_send = self.options["before_send"]
113+
if before_send is not None:
114+
event = before_send(event)
115+
116+
if event is not None:
117+
pop_hidden_keys(event)
118+
event = flatten_metadata(event)
119+
event = convert_types(event)
120+
101121
return event
102122

103-
def _check_should_capture(self, event):
123+
def _is_ignored_error(self, event):
124+
exc_info = event.get("__sentry_exc_info")
125+
126+
if not exc_info or exc_info[0] is None:
127+
return False
128+
129+
type_name = get_type_name(exc_info[0])
130+
full_name = "%s.%s" % (exc_info[0].__module__, type_name)
131+
132+
for errcls in self.options["ignore_errors"]:
133+
# String types are matched against the type name in the
134+
# exception only
135+
if isinstance(errcls, string_types):
136+
if errcls == full_name or errcls == type_name:
137+
return True
138+
else:
139+
if issubclass(exc_info[0], errcls):
140+
return True
141+
142+
return False
143+
144+
def _should_capture(self, event, scope=None):
104145
if (
105146
self.options["sample_rate"] < 1.0
106147
and random.random() >= self.options["sample_rate"]
107148
):
108-
raise SkipEvent()
149+
return False
109150

110-
if event._exc_value is not None:
111-
exclusions = self.options["ignore_errors"]
112-
exc_type = type(event._exc_value)
151+
if self._is_ignored_error(event):
152+
return False
113153

114-
if any(issubclass(exc_type, e) for e in exclusions):
115-
raise SkipEvent()
116-
117-
if _most_recent_exception.get(None) is event._exc_value:
118-
raise SkipEvent()
119-
_most_recent_exception.set(event._exc_value)
154+
return True
120155

121156
def capture_event(self, event, scope=None):
122157
"""Captures an event."""
123158
if self._transport is None:
124159
return
125-
if not isinstance(event, Event):
126-
event = Event(event)
127-
try:
128-
self._check_should_capture(event)
160+
rv = event.get("event_id")
161+
if rv is None:
162+
event["event_id"] = rv = uuid.uuid4().hex
163+
if self._should_capture(event, scope):
129164
event = self._prepare_event(event, scope)
130-
except SkipEvent:
131-
return
132-
self._transport.capture_event(event)
165+
if event is not None:
166+
self._transport.capture_event(event)
167+
return True
133168

134169
def drain_events(self, timeout=None):
135170
if timeout is None:

sentry_sdk/consts.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
VERSION = "0.1"
44
DEFAULT_SERVER_NAME = socket.gethostname() if hasattr(socket, "gethostname") else None
55
DEFAULT_OPTIONS = {
6+
"dsn": None,
67
"with_locals": True,
78
"max_breadcrumbs": 100,
89
"release": None,
910
"environment": None,
1011
"server_name": DEFAULT_SERVER_NAME,
1112
"shutdown_timeout": 2.0,
1213
"integrations": [],
14+
"in_app_include": [],
15+
"in_app_exclude": [],
1316
"default_integrations": True,
1417
"repos": {},
1518
"dist": None,
@@ -18,8 +21,9 @@
1821
"send_default_pii": False,
1922
"http_proxy": None,
2023
"https_proxy": None,
21-
"ignore_errors": (),
24+
"ignore_errors": [],
2225
"request_bodies": "medium",
26+
"before_send": None,
2327
}
2428

2529
SDK_INFO = {"name": "sentry-python", "version": VERSION}

0 commit comments

Comments
 (0)