Skip to content

Commit a370b30

Browse files
authored
Merge branch 'getsentry:master' into master
2 parents 8e4b9f8 + ec88aa9 commit a370b30

File tree

15 files changed

+246
-148
lines changed

15 files changed

+246
-148
lines changed

sentry_sdk/integrations/django/asgi.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ def wrap_async_view(callback):
172172
@functools.wraps(callback)
173173
async def sentry_wrapped_callback(request, *args, **kwargs):
174174
# type: (Any, *Any, **Any) -> Any
175+
current_scope = sentry_sdk.get_current_scope()
176+
if current_scope.transaction is not None:
177+
current_scope.transaction.update_active_thread()
178+
175179
sentry_scope = sentry_sdk.get_isolation_scope()
176180
if sentry_scope.profile is not None:
177181
sentry_scope.profile.update_active_thread_id()

sentry_sdk/integrations/django/views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ def _wrap_sync_view(callback):
7676
@functools.wraps(callback)
7777
def sentry_wrapped_callback(request, *args, **kwargs):
7878
# type: (Any, *Any, **Any) -> Any
79+
current_scope = sentry_sdk.get_current_scope()
80+
if current_scope.transaction is not None:
81+
current_scope.transaction.update_active_thread()
82+
7983
sentry_scope = sentry_sdk.get_isolation_scope()
8084
# set the active thread id to the handler thread for sync views
8185
# this isn't necessary for async views since that runs on main

sentry_sdk/integrations/fastapi.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,14 @@ def _sentry_get_request_handler(*args, **kwargs):
8888
@wraps(old_call)
8989
def _sentry_call(*args, **kwargs):
9090
# type: (*Any, **Any) -> Any
91+
current_scope = sentry_sdk.get_current_scope()
92+
if current_scope.transaction is not None:
93+
current_scope.transaction.update_active_thread()
94+
9195
sentry_scope = sentry_sdk.get_isolation_scope()
9296
if sentry_scope.profile is not None:
9397
sentry_scope.profile.update_active_thread_id()
98+
9499
return old_call(*args, **kwargs)
95100

96101
dependant.call = _sentry_call

sentry_sdk/integrations/quart.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import asyncio
22
import inspect
3-
import threading
43
from functools import wraps
54

65
import sentry_sdk
@@ -122,11 +121,13 @@ def decorator(old_func):
122121
@ensure_integration_enabled(QuartIntegration, old_func)
123122
def _sentry_func(*args, **kwargs):
124123
# type: (*Any, **Any) -> Any
125-
scope = sentry_sdk.get_isolation_scope()
126-
if scope.profile is not None:
127-
scope.profile.active_thread_id = (
128-
threading.current_thread().ident
129-
)
124+
current_scope = sentry_sdk.get_current_scope()
125+
if current_scope.transaction is not None:
126+
current_scope.transaction.update_active_thread()
127+
128+
sentry_scope = sentry_sdk.get_isolation_scope()
129+
if sentry_scope.profile is not None:
130+
sentry_scope.profile.update_active_thread_id()
130131

131132
return old_func(*args, **kwargs)
132133

sentry_sdk/integrations/starlette.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,8 +487,11 @@ def _sentry_sync_func(*args, **kwargs):
487487
if integration is None:
488488
return old_func(*args, **kwargs)
489489

490-
sentry_scope = sentry_sdk.get_isolation_scope()
490+
current_scope = sentry_sdk.get_current_scope()
491+
if current_scope.transaction is not None:
492+
current_scope.transaction.update_active_thread()
491493

494+
sentry_scope = sentry_sdk.get_isolation_scope()
492495
if sentry_scope.profile is not None:
493496
sentry_scope.profile.update_active_thread_id()
494497

sentry_sdk/integrations/strawberry.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,18 @@
3131
from strawberry import Schema
3232
from strawberry.extensions import SchemaExtension # type: ignore
3333
from strawberry.extensions.tracing.utils import should_skip_tracing as strawberry_should_skip_tracing # type: ignore
34+
from strawberry.http import async_base_view, sync_base_view # type: ignore
35+
except ImportError:
36+
raise DidNotEnable("strawberry-graphql is not installed")
37+
38+
try:
3439
from strawberry.extensions.tracing import ( # type: ignore
3540
SentryTracingExtension as StrawberrySentryAsyncExtension,
3641
SentryTracingExtensionSync as StrawberrySentrySyncExtension,
3742
)
38-
from strawberry.http import async_base_view, sync_base_view # type: ignore
3943
except ImportError:
40-
raise DidNotEnable("strawberry-graphql is not installed")
44+
StrawberrySentryAsyncExtension = None
45+
StrawberrySentrySyncExtension = None
4146

4247
from typing import TYPE_CHECKING
4348

sentry_sdk/tracing.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,7 @@ def __init__(
329329
self._span_recorder = None # type: Optional[_SpanRecorder]
330330
self._local_aggregator = None # type: Optional[LocalAggregator]
331331

332-
thread_id, thread_name = get_current_thread_meta()
333-
self.set_thread(thread_id, thread_name)
332+
self.update_active_thread()
334333
self.set_profiler_id(get_profiler_id())
335334

336335
# TODO this should really live on the Transaction class rather than the Span
@@ -732,6 +731,11 @@ def get_profile_context(self):
732731
"profiler_id": profiler_id,
733732
}
734733

734+
def update_active_thread(self):
735+
# type: () -> None
736+
thread_id, thread_name = get_current_thread_meta()
737+
self.set_thread(thread_id, thread_name)
738+
735739

736740
class Transaction(Span):
737741
"""The Transaction is the root element that holds all the spans

sentry_sdk/transport.py

Lines changed: 42 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -215,15 +215,7 @@ def __init__(self, options):
215215
) # type: DefaultDict[Tuple[EventDataCategory, str], int]
216216
self._last_client_report_sent = time.time()
217217

218-
self._pool = self._make_pool(
219-
self.parsed_dsn,
220-
http_proxy=options["http_proxy"],
221-
https_proxy=options["https_proxy"],
222-
ca_certs=options["ca_certs"],
223-
cert_file=options["cert_file"],
224-
key_file=options["key_file"],
225-
proxy_headers=options["proxy_headers"],
226-
)
218+
self._pool = self._make_pool()
227219

228220
# Backwards compatibility for deprecated `self.hub_class` attribute
229221
self._hub_cls = sentry_sdk.Hub
@@ -532,8 +524,8 @@ def _serialize_envelope(self, envelope):
532524

533525
return content_encoding, body
534526

535-
def _get_pool_options(self, ca_certs, cert_file=None, key_file=None):
536-
# type: (Self, Optional[Any], Optional[Any], Optional[Any]) -> Dict[str, Any]
527+
def _get_pool_options(self):
528+
# type: (Self) -> Dict[str, Any]
537529
raise NotImplementedError()
538530

539531
def _in_no_proxy(self, parsed_dsn):
@@ -547,17 +539,8 @@ def _in_no_proxy(self, parsed_dsn):
547539
return True
548540
return False
549541

550-
def _make_pool(
551-
self,
552-
parsed_dsn, # type: Dsn
553-
http_proxy, # type: Optional[str]
554-
https_proxy, # type: Optional[str]
555-
ca_certs, # type: Optional[Any]
556-
cert_file, # type: Optional[Any]
557-
key_file, # type: Optional[Any]
558-
proxy_headers, # type: Optional[Dict[str, str]]
559-
):
560-
# type: (...) -> Union[PoolManager, ProxyManager, httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]
542+
def _make_pool(self):
543+
# type: (Self) -> Union[PoolManager, ProxyManager, httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]
561544
raise NotImplementedError()
562545

563546
def _request(
@@ -631,8 +614,8 @@ class HttpTransport(BaseHttpTransport):
631614
if TYPE_CHECKING:
632615
_pool: Union[PoolManager, ProxyManager]
633616

634-
def _get_pool_options(self, ca_certs, cert_file=None, key_file=None):
635-
# type: (Self, Any, Any, Any) -> Dict[str, Any]
617+
def _get_pool_options(self):
618+
# type: (Self) -> Dict[str, Any]
636619

637620
num_pools = self.options.get("_experiments", {}).get("transport_num_pools")
638621
options = {
@@ -658,42 +641,43 @@ def _get_pool_options(self, ca_certs, cert_file=None, key_file=None):
658641
options["socket_options"] = socket_options
659642

660643
options["ca_certs"] = (
661-
ca_certs # User-provided bundle from the SDK init
644+
self.options["ca_certs"] # User-provided bundle from the SDK init
662645
or os.environ.get("SSL_CERT_FILE")
663646
or os.environ.get("REQUESTS_CA_BUNDLE")
664647
or certifi.where()
665648
)
666649

667-
options["cert_file"] = cert_file or os.environ.get("CLIENT_CERT_FILE")
668-
options["key_file"] = key_file or os.environ.get("CLIENT_KEY_FILE")
650+
options["cert_file"] = self.options["cert_file"] or os.environ.get(
651+
"CLIENT_CERT_FILE"
652+
)
653+
options["key_file"] = self.options["key_file"] or os.environ.get(
654+
"CLIENT_KEY_FILE"
655+
)
669656

670657
return options
671658

672-
def _make_pool(
673-
self,
674-
parsed_dsn, # type: Dsn
675-
http_proxy, # type: Optional[str]
676-
https_proxy, # type: Optional[str]
677-
ca_certs, # type: Any
678-
cert_file, # type: Any
679-
key_file, # type: Any
680-
proxy_headers, # type: Optional[Dict[str, str]]
681-
):
682-
# type: (...) -> Union[PoolManager, ProxyManager]
659+
def _make_pool(self):
660+
# type: (Self) -> Union[PoolManager, ProxyManager]
661+
if self.parsed_dsn is None:
662+
raise ValueError("Cannot create HTTP-based transport without valid DSN")
663+
683664
proxy = None
684-
no_proxy = self._in_no_proxy(parsed_dsn)
665+
no_proxy = self._in_no_proxy(self.parsed_dsn)
685666

686667
# try HTTPS first
687-
if parsed_dsn.scheme == "https" and (https_proxy != ""):
668+
https_proxy = self.options["https_proxy"]
669+
if self.parsed_dsn.scheme == "https" and (https_proxy != ""):
688670
proxy = https_proxy or (not no_proxy and getproxies().get("https"))
689671

690672
# maybe fallback to HTTP proxy
673+
http_proxy = self.options["http_proxy"]
691674
if not proxy and (http_proxy != ""):
692675
proxy = http_proxy or (not no_proxy and getproxies().get("http"))
693676

694-
opts = self._get_pool_options(ca_certs, cert_file, key_file)
677+
opts = self._get_pool_options()
695678

696679
if proxy:
680+
proxy_headers = self.options["proxy_headers"]
697681
if proxy_headers:
698682
opts["proxy_headers"] = proxy_headers
699683

@@ -783,10 +767,11 @@ def _request(
783767
)
784768
return response
785769

786-
def _get_pool_options(self, ca_certs, cert_file=None, key_file=None):
787-
# type: (Any, Any, Any) -> Dict[str, Any]
770+
def _get_pool_options(self):
771+
# type: (Self) -> Dict[str, Any]
788772
options = {
789-
"http2": True,
773+
"http2": self.parsed_dsn is not None
774+
and self.parsed_dsn.scheme == "https",
790775
"retries": 3,
791776
} # type: Dict[str, Any]
792777

@@ -805,45 +790,41 @@ def _get_pool_options(self, ca_certs, cert_file=None, key_file=None):
805790

806791
ssl_context = ssl.create_default_context()
807792
ssl_context.load_verify_locations(
808-
ca_certs # User-provided bundle from the SDK init
793+
self.options["ca_certs"] # User-provided bundle from the SDK init
809794
or os.environ.get("SSL_CERT_FILE")
810795
or os.environ.get("REQUESTS_CA_BUNDLE")
811796
or certifi.where()
812797
)
813-
cert_file = cert_file or os.environ.get("CLIENT_CERT_FILE")
814-
key_file = key_file or os.environ.get("CLIENT_KEY_FILE")
798+
cert_file = self.options["cert_file"] or os.environ.get("CLIENT_CERT_FILE")
799+
key_file = self.options["key_file"] or os.environ.get("CLIENT_KEY_FILE")
815800
if cert_file is not None:
816801
ssl_context.load_cert_chain(cert_file, key_file)
817802

818803
options["ssl_context"] = ssl_context
819804

820805
return options
821806

822-
def _make_pool(
823-
self,
824-
parsed_dsn, # type: Dsn
825-
http_proxy, # type: Optional[str]
826-
https_proxy, # type: Optional[str]
827-
ca_certs, # type: Any
828-
cert_file, # type: Any
829-
key_file, # type: Any
830-
proxy_headers, # type: Optional[Dict[str, str]]
831-
):
832-
# type: (...) -> Union[httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]
807+
def _make_pool(self):
808+
# type: (Self) -> Union[httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]
809+
if self.parsed_dsn is None:
810+
raise ValueError("Cannot create HTTP-based transport without valid DSN")
833811
proxy = None
834-
no_proxy = self._in_no_proxy(parsed_dsn)
812+
no_proxy = self._in_no_proxy(self.parsed_dsn)
835813

836814
# try HTTPS first
837-
if parsed_dsn.scheme == "https" and (https_proxy != ""):
815+
https_proxy = self.options["https_proxy"]
816+
if self.parsed_dsn.scheme == "https" and (https_proxy != ""):
838817
proxy = https_proxy or (not no_proxy and getproxies().get("https"))
839818

840819
# maybe fallback to HTTP proxy
820+
http_proxy = self.options["http_proxy"]
841821
if not proxy and (http_proxy != ""):
842822
proxy = http_proxy or (not no_proxy and getproxies().get("http"))
843823

844-
opts = self._get_pool_options(ca_certs, cert_file, key_file)
824+
opts = self._get_pool_options()
845825

846826
if proxy:
827+
proxy_headers = self.options["proxy_headers"]
847828
if proxy_headers:
848829
opts["proxy_headers"] = proxy_headers
849830

tests/integrations/django/asgi/test_asgi.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,16 @@ async def test_async_views(sentry_init, capture_events, application):
104104
@pytest.mark.skipif(
105105
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
106106
)
107-
async def test_active_thread_id(sentry_init, capture_envelopes, endpoint, application):
107+
async def test_active_thread_id(
108+
sentry_init, capture_envelopes, teardown_profiling, endpoint, application
109+
):
108110
with mock.patch(
109111
"sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0
110112
):
111113
sentry_init(
112114
integrations=[DjangoIntegration()],
113115
traces_sample_rate=1.0,
114-
_experiments={"profiles_sample_rate": 1.0},
116+
profiles_sample_rate=1.0,
115117
)
116118

117119
envelopes = capture_envelopes()
@@ -121,17 +123,26 @@ async def test_active_thread_id(sentry_init, capture_envelopes, endpoint, applic
121123
await comm.wait()
122124

123125
assert response["status"] == 200, response["body"]
124-
assert len(envelopes) == 1
125126

126-
profiles = [item for item in envelopes[0].items if item.type == "profile"]
127-
assert len(profiles) == 1
127+
assert len(envelopes) == 1
128+
129+
profiles = [item for item in envelopes[0].items if item.type == "profile"]
130+
assert len(profiles) == 1
131+
132+
data = json.loads(response["body"])
133+
134+
for item in profiles:
135+
transactions = item.payload.json["transactions"]
136+
assert len(transactions) == 1
137+
assert str(data["active"]) == transactions[0]["active_thread_id"]
128138

129-
data = json.loads(response["body"])
139+
transactions = [item for item in envelopes[0].items if item.type == "transaction"]
140+
assert len(transactions) == 1
130141

131-
for profile in profiles:
132-
transactions = profile.payload.json["transactions"]
133-
assert len(transactions) == 1
134-
assert str(data["active"]) == transactions[0]["active_thread_id"]
142+
for item in transactions:
143+
transaction = item.payload.json
144+
trace_context = transaction["contexts"]["trace"]
145+
assert str(data["active"]) == trace_context["data"]["thread.id"]
135146

136147

137148
@pytest.mark.asyncio

tests/integrations/fastapi/test_fastapi.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def test_legacy_setup(
184184
def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, endpoint):
185185
sentry_init(
186186
traces_sample_rate=1.0,
187-
_experiments={"profiles_sample_rate": 1.0},
187+
profiles_sample_rate=1.0,
188188
)
189189
app = fastapi_app_factory()
190190
asgi_app = SentryAsgiMiddleware(app)
@@ -203,11 +203,19 @@ def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, en
203203
profiles = [item for item in envelopes[0].items if item.type == "profile"]
204204
assert len(profiles) == 1
205205

206-
for profile in profiles:
207-
transactions = profile.payload.json["transactions"]
206+
for item in profiles:
207+
transactions = item.payload.json["transactions"]
208208
assert len(transactions) == 1
209209
assert str(data["active"]) == transactions[0]["active_thread_id"]
210210

211+
transactions = [item for item in envelopes[0].items if item.type == "transaction"]
212+
assert len(transactions) == 1
213+
214+
for item in transactions:
215+
transaction = item.payload.json
216+
trace_context = transaction["contexts"]["trace"]
217+
assert str(data["active"]) == trace_context["data"]["thread.id"]
218+
211219

212220
@pytest.mark.asyncio
213221
async def test_original_request_not_scrubbed(sentry_init, capture_events):

0 commit comments

Comments
 (0)