Skip to content

Commit 8bd2f46

Browse files
fix(api): Fix tracing TypeError for static and class methods (#2559)
Fixes TypeError that occurred when static or class methods, which were passed in the `functions_to_trace` argument when initializing the SDK, were called on an instance. Fixes GH-2525 --------- Co-authored-by: Ivana Kellyerova <[email protected]>
1 parent 6470063 commit 8bd2f46

File tree

7 files changed

+168
-132
lines changed

7 files changed

+168
-132
lines changed

sentry_sdk/client.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,13 @@ def _setup_instrumentation(self, functions_to_trace):
198198
module_obj = import_module(module_name)
199199
class_obj = getattr(module_obj, class_name)
200200
function_obj = getattr(class_obj, function_name)
201-
setattr(class_obj, function_name, trace(function_obj))
201+
function_type = type(class_obj.__dict__[function_name])
202+
traced_function = trace(function_obj)
203+
204+
if function_type in (staticmethod, classmethod):
205+
traced_function = staticmethod(traced_function)
206+
207+
setattr(class_obj, function_name, traced_function)
202208
setattr(module_obj, class_name, class_obj)
203209
logger.debug("Enabled tracing for %s", function_qualname)
204210

sentry_sdk/integrations/aiohttp.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ async def sentry_app_handle(self, request, *args, **kwargs):
141141
transaction.set_http_status(response.status)
142142
return response
143143

144-
Application._handle = sentry_app_handle # type: ignore[method-assign]
144+
Application._handle = sentry_app_handle
145145

146146
old_urldispatcher_resolve = UrlDispatcher.resolve
147147

@@ -173,7 +173,7 @@ async def sentry_urldispatcher_resolve(self, request):
173173

174174
return rv
175175

176-
UrlDispatcher.resolve = sentry_urldispatcher_resolve # type: ignore[method-assign]
176+
UrlDispatcher.resolve = sentry_urldispatcher_resolve
177177

178178
old_client_session_init = ClientSession.__init__
179179

@@ -190,7 +190,7 @@ def init(*args, **kwargs):
190190
kwargs["trace_configs"] = client_trace_configs
191191
return old_client_session_init(*args, **kwargs)
192192

193-
ClientSession.__init__ = init # type: ignore[method-assign]
193+
ClientSession.__init__ = init
194194

195195

196196
def create_trace_config():

tests/conftest.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import socket
44
from threading import Thread
5+
from contextlib import contextmanager
56

67
import pytest
78
import jsonschema
@@ -27,8 +28,13 @@
2728
from http.server import BaseHTTPRequestHandler, HTTPServer
2829

2930

31+
try:
32+
from unittest import mock
33+
except ImportError:
34+
import mock
35+
3036
import sentry_sdk
31-
from sentry_sdk._compat import iteritems, reraise, string_types
37+
from sentry_sdk._compat import iteritems, reraise, string_types, PY2
3238
from sentry_sdk.envelope import Envelope
3339
from sentry_sdk.integrations import _processed_integrations # noqa: F401
3440
from sentry_sdk.profiler import teardown_profiler
@@ -37,6 +43,12 @@
3743

3844
from tests import _warning_recorder, _warning_recorder_mgr
3945

46+
from sentry_sdk._types import TYPE_CHECKING
47+
48+
if TYPE_CHECKING:
49+
from typing import Optional
50+
from collections.abc import Iterator
51+
4052

4153
SENTRY_EVENT_SCHEMA = "./checkouts/data-schemas/relay/event.schema.json"
4254

@@ -620,3 +632,23 @@ def werkzeug_set_cookie(client, servername, key, value):
620632
client.set_cookie(servername, key, value)
621633
except TypeError:
622634
client.set_cookie(key, value)
635+
636+
637+
@contextmanager
638+
def patch_start_tracing_child(fake_transaction_is_none=False):
639+
# type: (bool) -> Iterator[Optional[mock.MagicMock]]
640+
if not fake_transaction_is_none:
641+
fake_transaction = mock.MagicMock()
642+
fake_start_child = mock.MagicMock()
643+
fake_transaction.start_child = fake_start_child
644+
else:
645+
fake_transaction = None
646+
fake_start_child = None
647+
648+
version = "2" if PY2 else "3"
649+
650+
with mock.patch(
651+
"sentry_sdk.tracing_utils_py%s.get_current_span" % version,
652+
return_value=fake_transaction,
653+
):
654+
yield fake_start_child

tests/test_basics.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import pytest
77

8+
from tests.conftest import patch_start_tracing_child
9+
810
from sentry_sdk import (
911
Client,
1012
push_scope,
@@ -17,7 +19,7 @@
1719
last_event_id,
1820
Hub,
1921
)
20-
from sentry_sdk._compat import reraise
22+
from sentry_sdk._compat import reraise, PY2
2123
from sentry_sdk.integrations import (
2224
_AUTO_ENABLING_INTEGRATIONS,
2325
Integration,
@@ -736,3 +738,59 @@ def test_multiple_setup_integrations_calls():
736738

737739
second_call_return = setup_integrations([NoOpIntegration()], with_defaults=False)
738740
assert second_call_return == {NoOpIntegration.identifier: NoOpIntegration()}
741+
742+
743+
class TracingTestClass:
744+
@staticmethod
745+
def static(arg):
746+
return arg
747+
748+
@classmethod
749+
def class_(cls, arg):
750+
return cls, arg
751+
752+
753+
def test_staticmethod_tracing(sentry_init):
754+
test_staticmethod_name = "tests.test_basics.TracingTestClass.static"
755+
if not PY2:
756+
# Skip this check on Python 2 since __qualname__ is available in Python 3 only. Skipping is okay,
757+
# since the assertion would be expected to fail in Python 3 if there is any problem.
758+
assert (
759+
".".join(
760+
[
761+
TracingTestClass.static.__module__,
762+
TracingTestClass.static.__qualname__,
763+
]
764+
)
765+
== test_staticmethod_name
766+
), "The test static method was moved or renamed. Please update the name accordingly"
767+
768+
sentry_init(functions_to_trace=[{"qualified_name": test_staticmethod_name}])
769+
770+
for instance_or_class in (TracingTestClass, TracingTestClass()):
771+
with patch_start_tracing_child() as fake_start_child:
772+
assert instance_or_class.static(1) == 1
773+
assert fake_start_child.call_count == 1
774+
775+
776+
def test_classmethod_tracing(sentry_init):
777+
test_classmethod_name = "tests.test_basics.TracingTestClass.class_"
778+
if not PY2:
779+
# Skip this check on Python 2 since __qualname__ is available in Python 3 only. Skipping is okay,
780+
# since the assertion would be expected to fail in Python 3 if there is any problem.
781+
assert (
782+
".".join(
783+
[
784+
TracingTestClass.class_.__module__,
785+
TracingTestClass.class_.__qualname__,
786+
]
787+
)
788+
== test_classmethod_name
789+
), "The test class method was moved or renamed. Please update the name accordingly"
790+
791+
sentry_init(functions_to_trace=[{"qualified_name": test_classmethod_name}])
792+
793+
for instance_or_class in (TracingTestClass, TracingTestClass()):
794+
with patch_start_tracing_child() as fake_start_child:
795+
assert instance_or_class.class_(1) == (TracingTestClass, 1)
796+
assert fake_start_child.call_count == 1
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from unittest import mock
2+
import pytest
3+
import sys
4+
5+
from tests.conftest import patch_start_tracing_child
6+
7+
from sentry_sdk.tracing_utils_py3 import (
8+
start_child_span_decorator as start_child_span_decorator_py3,
9+
)
10+
from sentry_sdk.utils import logger
11+
12+
if sys.version_info < (3, 6):
13+
pytest.skip("Async decorator only works on Python 3.6+", allow_module_level=True)
14+
15+
16+
async def my_async_example_function():
17+
return "return_of_async_function"
18+
19+
20+
@pytest.mark.asyncio
21+
async def test_trace_decorator_async_py3():
22+
with patch_start_tracing_child() as fake_start_child:
23+
result = await my_async_example_function()
24+
fake_start_child.assert_not_called()
25+
assert result == "return_of_async_function"
26+
27+
result2 = await start_child_span_decorator_py3(my_async_example_function)()
28+
fake_start_child.assert_called_once_with(
29+
op="function",
30+
description="test_decorator_async_py3.my_async_example_function",
31+
)
32+
assert result2 == "return_of_async_function"
33+
34+
35+
@pytest.mark.asyncio
36+
async def test_trace_decorator_async_py3_no_trx():
37+
with patch_start_tracing_child(fake_transaction_is_none=True):
38+
with mock.patch.object(logger, "warning", mock.Mock()) as fake_warning:
39+
result = await my_async_example_function()
40+
fake_warning.assert_not_called()
41+
assert result == "return_of_async_function"
42+
43+
result2 = await start_child_span_decorator_py3(my_async_example_function)()
44+
fake_warning.assert_called_once_with(
45+
"Can not create a child span for %s. "
46+
"Please start a Sentry transaction before calling this function.",
47+
"test_decorator_async_py3.my_async_example_function",
48+
)
49+
assert result2 == "return_of_async_function"

tests/tracing/test_decorator_py3.py

Lines changed: 0 additions & 103 deletions
This file was deleted.

0 commit comments

Comments
 (0)