Skip to content

Commit 781e4ce

Browse files
committed
feat(sentry): no more deprecation warning for sentry_sdk 2.x; now fill a transaction source when modify event data
Return sentry_sdk 1.x testset to quick check it still work, but recommended way is use 2.x branch
1 parent f45aa62 commit 781e4ce

File tree

3 files changed

+206
-13
lines changed

3 files changed

+206
-13
lines changed

fastapi_jsonrpc/__init__.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,30 @@
3838

3939
try:
4040
import sentry_sdk
41+
import sentry_sdk.tracing
4142
from sentry_sdk.utils import transaction_from_function as sentry_transaction_from_function
4243
except ImportError:
4344
sentry_sdk = None
4445
sentry_transaction_from_function = None
4546

46-
4747
try:
4848
from fastapi._compat import _normalize_errors # noqa
4949
except ImportError:
5050
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
5151
return errors
5252

53+
if hasattr(sentry_sdk, 'new_scope'):
54+
# sentry_sdk 2.x
55+
sentry_new_scope = sentry_sdk.new_scope
56+
else:
57+
# sentry_sdk 1.x
58+
@contextmanager
59+
def sentry_new_scope():
60+
hub = sentry_sdk.Hub.current
61+
with sentry_sdk.Hub(hub) as hub:
62+
with hub.configure_scope() as scope:
63+
yield scope
64+
5365

5466
class Params(fastapi.params.Body):
5567
def __init__(
@@ -95,6 +107,7 @@ def __init__(
95107

96108
def component_name(name: str, module: str = None):
97109
"""OpenAPI components must be unique by name"""
110+
98111
def decorator(obj):
99112
assert issubclass(obj, BaseModel)
100113
opts = {
@@ -127,6 +140,7 @@ def decorator(obj):
127140
return components[key]
128141
components[key] = obj
129142
return obj
143+
130144
return decorator
131145

132146

@@ -598,7 +612,7 @@ async def __aenter__(self):
598612
assert self.exit_stack is None
599613
self.exit_stack = await AsyncExitStack().__aenter__()
600614
if sentry_sdk is not None:
601-
self.exit_stack.enter_context(self._fix_sentry_scope())
615+
self.exit_stack.enter_context(self._enter_sentry_scope())
602616
await self.exit_stack.enter_async_context(self._handle_exception(reraise=False))
603617
self.jsonrpc_context_token = _jsonrpc_context.set(self)
604618
return self
@@ -626,23 +640,29 @@ async def _handle_exception(self, reraise=True):
626640
if self.exception is not None and self.is_unhandled_exception:
627641
logger.exception(str(self.exception), exc_info=self.exception)
628642

643+
@contextmanager
644+
def _enter_sentry_scope(self):
645+
with sentry_new_scope() as scope:
646+
# Actually we can use set_transaction_name
647+
# scope.set_transaction_name(
648+
# sentry_transaction_from_function(method_route.func),
649+
# source=sentry_sdk.tracing.TRANSACTION_SOURCE_CUSTOM,
650+
# )
651+
# and we need `method_route` instance for that,
652+
# but method_route is optional and is harder to track it than adding event processor
653+
scope.clear_breadcrumbs()
654+
scope.add_event_processor(self._make_sentry_event_processor())
655+
yield scope
656+
629657
def _make_sentry_event_processor(self):
630658
def event_processor(event, _):
631659
if self.method_route is not None:
632660
event['transaction'] = sentry_transaction_from_function(self.method_route.func)
661+
event['transaction_info']['source'] = sentry_sdk.tracing.TRANSACTION_SOURCE_CUSTOM
633662
return event
634663

635664
return event_processor
636665

637-
@contextmanager
638-
def _fix_sentry_scope(self):
639-
hub = sentry_sdk.Hub.current
640-
with sentry_sdk.Hub(hub) as hub:
641-
with hub.configure_scope() as scope:
642-
scope.clear_breadcrumbs()
643-
scope.add_event_processor(self._make_sentry_event_processor())
644-
yield
645-
646666
async def enter_middlewares(self, middlewares: Sequence['JsonRpcMiddleware']):
647667
for mw in middlewares:
648668
cm = mw(self)
@@ -1388,7 +1408,7 @@ def openapi(self):
13881408
self._restore_json_schema_fine_component_names(result)
13891409

13901410
for route in self.routes:
1391-
if isinstance(route, (EntrypointRoute, MethodRoute, )):
1411+
if isinstance(route, (EntrypointRoute, MethodRoute,)):
13921412
route: Union[EntrypointRoute, MethodRoute]
13931413
for media_type in result['paths'][route.path]:
13941414
result['paths'][route.path][media_type]['responses'].pop('default', None)

tests/test_sentry_sdk_1x.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
"""Test fixtures copied from https://github.com/getsentry/sentry-python/
2+
TODO: move integration to sentry_sdk
3+
"""
4+
import importlib.metadata
5+
6+
import pytest
7+
import sentry_sdk
8+
from sentry_sdk import Transport
9+
10+
from sentry_sdk.utils import capture_internal_exceptions
11+
12+
13+
sentry_sdk_version = importlib.metadata.version('sentry_sdk')
14+
if not sentry_sdk_version.startswith('1.'):
15+
pytest.skip(f"Testset is only for sentry_sdk 1.x, given {sentry_sdk_version=}", allow_module_level=True)
16+
17+
18+
@pytest.fixture
19+
def probe(ep):
20+
@ep.method()
21+
def probe() -> str:
22+
raise ZeroDivisionError
23+
24+
@ep.method()
25+
def probe2() -> str:
26+
raise RuntimeError
27+
28+
return ep
29+
30+
31+
def test_transaction_is_jsonrpc_method(
32+
probe,
33+
json_request,
34+
sentry_init,
35+
capture_exceptions,
36+
capture_events,
37+
assert_log_errors,
38+
):
39+
sentry_init(send_default_pii=True)
40+
exceptions = capture_exceptions()
41+
events = capture_events()
42+
43+
# Test in batch to ensure we correctly handle multiple requests
44+
json_request([
45+
{
46+
'id': 1,
47+
'jsonrpc': '2.0',
48+
'method': 'probe',
49+
'params': {},
50+
},
51+
{
52+
'id': 2,
53+
'jsonrpc': '2.0',
54+
'method': 'probe2',
55+
'params': {},
56+
},
57+
])
58+
59+
assert {type(e) for e in exceptions} == {RuntimeError, ZeroDivisionError}
60+
61+
assert_log_errors(
62+
'', pytest.raises(ZeroDivisionError),
63+
'', pytest.raises(RuntimeError),
64+
)
65+
66+
assert set([
67+
e.get('transaction') for e in events
68+
]) == {'test_sentry_sdk_1x.probe.<locals>.probe', 'test_sentry_sdk_1x.probe.<locals>.probe2'}
69+
70+
71+
class _TestTransport(Transport):
72+
def __init__(self, capture_event_callback, capture_envelope_callback):
73+
Transport.__init__(self)
74+
self.capture_event = capture_event_callback
75+
self.capture_envelope = capture_envelope_callback
76+
self._queue = None
77+
78+
79+
@pytest.fixture
80+
def monkeypatch_test_transport(monkeypatch):
81+
def check_event(event):
82+
def check_string_keys(map):
83+
for key, value in map.items:
84+
assert isinstance(key, str)
85+
if isinstance(value, dict):
86+
check_string_keys(value)
87+
88+
with capture_internal_exceptions():
89+
check_string_keys(event)
90+
91+
def check_envelope(envelope):
92+
with capture_internal_exceptions():
93+
# Assert error events are sent without envelope to server, for compat.
94+
# This does not apply if any item in the envelope is an attachment.
95+
if not any(x.type == "attachment" for x in envelope.items):
96+
assert not any(item.data_category == "error" for item in envelope.items)
97+
assert not any(item.get_event() is not None for item in envelope.items)
98+
99+
def inner(client):
100+
monkeypatch.setattr(
101+
client, "transport", _TestTransport(check_event, check_envelope)
102+
)
103+
104+
return inner
105+
106+
107+
@pytest.fixture
108+
def sentry_init(monkeypatch_test_transport, request):
109+
def inner(*a, **kw):
110+
hub = sentry_sdk.Hub.current
111+
client = sentry_sdk.Client(*a, **kw)
112+
hub.bind_client(client)
113+
if "transport" not in kw:
114+
monkeypatch_test_transport(sentry_sdk.Hub.current.client)
115+
116+
if request.node.get_closest_marker("forked"):
117+
# Do not run isolation if the test is already running in
118+
# ultimate isolation (seems to be required for celery tests that
119+
# fork)
120+
yield inner
121+
else:
122+
with sentry_sdk.Hub(None):
123+
yield inner
124+
125+
126+
@pytest.fixture
127+
def capture_events(monkeypatch):
128+
def inner():
129+
events = []
130+
test_client = sentry_sdk.Hub.current.client
131+
old_capture_event = test_client.transport.capture_event
132+
old_capture_envelope = test_client.transport.capture_envelope
133+
134+
def append_event(event):
135+
events.append(event)
136+
return old_capture_event(event)
137+
138+
def append_envelope(envelope):
139+
for item in envelope:
140+
if item.headers.get("type") in ("event", "transaction"):
141+
test_client.transport.capture_event(item.payload.json)
142+
return old_capture_envelope(envelope)
143+
144+
monkeypatch.setattr(test_client.transport, "capture_event", append_event)
145+
monkeypatch.setattr(test_client.transport, "capture_envelope", append_envelope)
146+
return events
147+
148+
return inner
149+
150+
151+
@pytest.fixture
152+
def capture_exceptions(monkeypatch):
153+
def inner():
154+
errors = set()
155+
old_capture_event = sentry_sdk.Hub.capture_event
156+
157+
def capture_event(self, event, hint=None):
158+
if hint:
159+
if "exc_info" in hint:
160+
error = hint["exc_info"][1]
161+
errors.add(error)
162+
return old_capture_event(self, event, hint=hint)
163+
164+
monkeypatch.setattr(sentry_sdk.Hub, "capture_event", capture_event)
165+
return errors
166+
167+
return inner

tests/test_sentry.py renamed to tests/test_sentry_sdk_2x.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
"""Test fixtures copied from https://github.com/getsentry/sentry-python/
22
TODO: move integration to sentry_sdk
33
"""
4+
import importlib.metadata
45

56
import pytest
67
import sentry_sdk
78
from sentry_sdk import Transport
89
from sentry_sdk.envelope import Envelope
910

1011

12+
sentry_sdk_version = importlib.metadata.version('sentry_sdk')
13+
if not sentry_sdk_version.startswith('2.'):
14+
pytest.skip(f"Testset is only for sentry_sdk 2.x, given {sentry_sdk_version=}", allow_module_level=True)
15+
16+
1117
@pytest.fixture
1218
def probe(ep):
1319
@ep.method()
@@ -58,7 +64,7 @@ def test_transaction_is_jsonrpc_method(
5864

5965
assert set([
6066
e.get('transaction') for e in events
61-
]) == {'test_sentry.probe.<locals>.probe', 'test_sentry.probe.<locals>.probe2'}
67+
]) == {'test_sentry_sdk_2x.probe.<locals>.probe', 'test_sentry_sdk_2x.probe.<locals>.probe2'}
6268

6369

6470
class TestTransport(Transport):

0 commit comments

Comments
 (0)