Skip to content

Commit bea8782

Browse files
use nullcontext and add logic to sync_wrapper
1 parent 0e95d7f commit bea8782

File tree

6 files changed

+113
-68
lines changed

6 files changed

+113
-68
lines changed

sentry_sdk/integrations/_wsgi_common.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from contextlib import contextmanager
21
import json
32
from copy import deepcopy
43

@@ -52,12 +51,6 @@
5251
)
5352

5453

55-
# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support
56-
@contextmanager
57-
def nullcontext() -> "Iterator[None]":
58-
yield
59-
60-
6154
def request_body_within_bounds(
6255
client: "Optional[sentry_sdk.client.BaseClient]", content_length: int
6356
) -> bool:

sentry_sdk/integrations/asgi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
)
2020
from sentry_sdk.integrations._wsgi_common import (
2121
DEFAULT_HTTP_METHODS_TO_CAPTURE,
22-
nullcontext,
2322
)
2423
from sentry_sdk.sessions import track_session
2524
from sentry_sdk.tracing import (
@@ -34,6 +33,7 @@
3433
logger,
3534
transaction_from_function,
3635
_get_installed_modules,
36+
nullcontext,
3737
)
3838
from sentry_sdk.tracing import Transaction
3939

sentry_sdk/integrations/mcp.py

Lines changed: 80 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from sentry_sdk.integrations import Integration, DidNotEnable
1818
from sentry_sdk.utils import safe_serialize
1919
from sentry_sdk.scope import should_send_default_pii
20+
from sentry_sdk.utils import nullcontext
2021

2122
try:
2223
from mcp.server.lowlevel import Server # type: ignore[import-not-found]
@@ -80,8 +81,8 @@ def _get_active_http_scopes() -> (
8081
return None
8182

8283
return (
83-
ctx.request.scope["state"].get("sentry_sdk.current_scope"),
8484
ctx.request.scope["state"].get("sentry_sdk.isolation_scope"),
85+
ctx.request.scope["state"].get("sentry_sdk.current_scope"),
8586
)
8687

8788

@@ -410,17 +411,28 @@ async def _async_handler_wrapper(
410411
scopes = _get_active_http_scopes()
411412

412413
if scopes is None:
413-
current_scope = None
414-
isolation_scope = None
414+
isolation_scope_context = nullcontext()
415+
current_scope_context = nullcontext()
415416
else:
416-
current_scope, isolation_scope = scopes
417+
isolation_scope, current_scope = scopes
418+
419+
isolation_scope_context = (
420+
nullcontext()
421+
if isolation_scope is None
422+
else sentry_sdk.scope.use_isolation_scope(isolation_scope)
423+
)
424+
current_scope_context = (
425+
nullcontext()
426+
if current_scope is None
427+
else sentry_sdk.scope.use_scope(current_scope)
428+
)
417429

418430
# Get request ID, session ID, and transport from context
419431
request_id, session_id, mcp_transport = _get_request_context_data()
420432

421433
# Start span and execute
422-
with sentry_sdk.scope.use_isolation_scope(isolation_scope):
423-
with sentry_sdk.scope.use_scope(current_scope):
434+
with isolation_scope_context:
435+
with current_scope_context:
424436
with get_start_span_function()(
425437
op=OP.MCP_SERVER,
426438
name=span_name,
@@ -490,50 +502,71 @@ def _sync_handler_wrapper(
490502
result_data_key,
491503
) = _prepare_handler_data(handler_type, original_args)
492504

493-
# Start span and execute
494-
with get_start_span_function()(
495-
op=OP.MCP_SERVER,
496-
name=span_name,
497-
origin=MCPIntegration.origin,
498-
) as span:
499-
# Get request ID, session ID, and transport from context
500-
request_id, session_id, mcp_transport = _get_request_context_data()
501-
502-
# Set input span data
503-
_set_span_input_data(
504-
span,
505-
handler_name,
506-
span_data_key,
507-
mcp_method_name,
508-
arguments,
509-
request_id,
510-
session_id,
511-
mcp_transport,
505+
scopes = _get_active_http_scopes()
506+
507+
if scopes is None:
508+
isolation_scope_context = nullcontext()
509+
current_scope_context = nullcontext()
510+
else:
511+
isolation_scope, current_scope = scopes
512+
513+
isolation_scope_context = (
514+
nullcontext()
515+
if isolation_scope is None
516+
else sentry_sdk.scope.use_isolation_scope(isolation_scope)
517+
)
518+
current_scope_context = (
519+
nullcontext()
520+
if current_scope is None
521+
else sentry_sdk.scope.use_scope(current_scope)
512522
)
513523

514-
# For resources, extract and set protocol
515-
if handler_type == "resource":
516-
uri = original_args[0]
517-
protocol = None
518-
if hasattr(uri, "scheme"):
519-
protocol = uri.scheme
520-
elif handler_name and "://" in handler_name:
521-
protocol = handler_name.split("://")[0]
522-
if protocol:
523-
span.set_data(SPANDATA.MCP_RESOURCE_PROTOCOL, protocol)
524+
# Start span and execute
525+
with isolation_scope_context:
526+
with current_scope_context:
527+
with get_start_span_function()(
528+
op=OP.MCP_SERVER,
529+
name=span_name,
530+
origin=MCPIntegration.origin,
531+
) as span:
532+
# Get request ID, session ID, and transport from context
533+
request_id, session_id, mcp_transport = _get_request_context_data()
524534

525-
try:
526-
# Execute the sync handler
527-
result = func(*original_args)
528-
except Exception as e:
529-
# Set error flag for tools
530-
if handler_type == "tool":
531-
span.set_data(SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True)
532-
sentry_sdk.capture_exception(e)
533-
raise
534-
535-
_set_span_output_data(span, result, result_data_key, handler_type)
536-
return result
535+
# Set input span data
536+
_set_span_input_data(
537+
span,
538+
handler_name,
539+
span_data_key,
540+
mcp_method_name,
541+
arguments,
542+
request_id,
543+
session_id,
544+
mcp_transport,
545+
)
546+
547+
# For resources, extract and set protocol
548+
if handler_type == "resource":
549+
uri = original_args[0]
550+
protocol = None
551+
if hasattr(uri, "scheme"):
552+
protocol = uri.scheme
553+
elif handler_name and "://" in handler_name:
554+
protocol = handler_name.split("://")[0]
555+
if protocol:
556+
span.set_data(SPANDATA.MCP_RESOURCE_PROTOCOL, protocol)
557+
558+
try:
559+
# Execute the sync handler
560+
result = func(*original_args)
561+
except Exception as e:
562+
# Set error flag for tools
563+
if handler_type == "tool":
564+
span.set_data(SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True)
565+
sentry_sdk.capture_exception(e)
566+
raise
567+
568+
_set_span_output_data(span, result, result_data_key, handler_type)
569+
return result
537570

538571

539572
def _create_instrumented_handler(

sentry_sdk/integrations/wsgi.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
import functools
23
from functools import partial
34
from typing import TYPE_CHECKING
45

@@ -9,7 +10,6 @@
910
from sentry_sdk.integrations._wsgi_common import (
1011
DEFAULT_HTTP_METHODS_TO_CAPTURE,
1112
_filter_headers,
12-
nullcontext,
1313
)
1414
from sentry_sdk.scope import should_send_default_pii, use_isolation_scope
1515
from sentry_sdk.sessions import track_session
@@ -19,6 +19,7 @@
1919
capture_internal_exceptions,
2020
event_from_exception,
2121
reraise,
22+
nullcontext,
2223
)
2324

2425
if TYPE_CHECKING:
@@ -70,6 +71,7 @@ class SentryWsgiMiddleware:
7071
"use_x_forwarded_for",
7172
"span_origin",
7273
"http_methods_to_capture",
74+
"_wsgi_file_wrapper",
7375
)
7476

7577
def __init__(
@@ -85,11 +87,19 @@ def __init__(
8587
self.http_methods_to_capture = http_methods_to_capture
8688

8789
def __call__(
88-
self, environ: "Dict[str, str]", start_response: "Callable[..., Any]"
90+
self, environ: "Dict[str, Any]", start_response: "Callable[..., Any]"
8991
) -> "_ScopedResponse":
9092
if _wsgi_middleware_applied.get(False):
9193
return self.app(environ, start_response)
9294

95+
old_wsgi_file_wrapper = environ["wsgi.file_wrapper"]
96+
97+
def _patch_environ_wsgi_file_wrapper(fileobj, block_size=8192):
98+
self._wsgi_file_wrapper = old_wsgi_file_wrapper(fileobj, block_size)
99+
return self._wsgi_file_wrapper
100+
101+
environ["wsgi.file_wrapper"] = _patch_environ_wsgi_file_wrapper
102+
93103
_wsgi_middleware_applied.set(True)
94104
try:
95105
with sentry_sdk.isolation_scope() as scope:
@@ -135,6 +145,9 @@ def __call__(
135145
finally:
136146
_wsgi_middleware_applied.set(False)
137147

148+
if response is self._wsgi_file_wrapper:
149+
return response
150+
138151
return _ScopedResponse(scope, response)
139152

140153

@@ -215,6 +228,14 @@ def _capture_exception() -> "ExcInfo":
215228
return exc_info
216229

217230

231+
class _ScopedFileWrapper:
232+
def __init__(self, scope: "sentry_sdk.scope.Scope", fileno):
233+
pass
234+
235+
def fileno():
236+
pass
237+
238+
218239
class _ScopedResponse:
219240
"""
220241
Users a separate scope for each response chunk.

sentry_sdk/scope.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@
100100

101101
F = TypeVar("F", bound=Callable[..., Any])
102102
T = TypeVar("T")
103-
S = TypeVar("S", bound=Optional["Scope"])
104103

105104

106105
# Holds data that will be added to **all** events sent by this process.
@@ -1787,7 +1786,7 @@ def new_scope() -> "Generator[Scope, None, None]":
17871786

17881787

17891788
@contextmanager
1790-
def use_scope(scope: "S") -> "Generator[S, None, None]":
1789+
def use_scope(scope: "Scope") -> "Generator[Scope, None, None]":
17911790
"""
17921791
.. versionadded:: 2.0.0
17931792
@@ -1809,10 +1808,6 @@ def use_scope(scope: "S") -> "Generator[S, None, None]":
18091808
sentry_sdk.capture_message("hello, again") # will NOT include `color` tag.
18101809
18111810
"""
1812-
if scope is None:
1813-
yield scope
1814-
return
1815-
18161811
# set given scope as current scope
18171812
token = _current_scope.set(scope)
18181813

@@ -1876,7 +1871,7 @@ def isolation_scope() -> "Generator[Scope, None, None]":
18761871

18771872

18781873
@contextmanager
1879-
def use_isolation_scope(isolation_scope: "S") -> "Generator[S, None, None]":
1874+
def use_isolation_scope(isolation_scope: "Scope") -> "Generator[Scope, None, None]":
18801875
"""
18811876
.. versionadded:: 2.0.0
18821877
@@ -1897,10 +1892,6 @@ def use_isolation_scope(isolation_scope: "S") -> "Generator[S, None, None]":
18971892
sentry_sdk.capture_message("hello, again") # will NOT include `color` tag.
18981893
18991894
"""
1900-
if isolation_scope is None:
1901-
yield isolation_scope
1902-
return
1903-
19041895
# fork current scope
19051896
current_scope = Scope.get_current_scope()
19061897
forked_current_scope = current_scope.fork()

sentry_sdk/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from functools import partial, partialmethod, wraps
1717
from numbers import Real
1818
from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit
19+
from contextlib import contextmanager
1920

2021
try:
2122
# Python 3.11
@@ -2078,3 +2079,9 @@ def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue":
20782079
# Coerce to string if we don't know what to do with the value. This should
20792080
# never happen as we pre-format early in format_attribute, but let's be safe.
20802081
return {"value": safe_repr(val), "type": "string"}
2082+
2083+
2084+
# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support
2085+
@contextmanager
2086+
def nullcontext() -> "Iterator[None]":
2087+
yield

0 commit comments

Comments
 (0)