diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 127e650ca4..ddb1c2a993 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -13,7 +13,6 @@ from typing import TYPE_CHECKING import sentry_sdk -from sentry_sdk.api import continue_trace from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations._asgi_common import ( _get_headers, @@ -38,10 +37,8 @@ ) from sentry_sdk.tracing import ( SOURCE_FOR_STYLE, - Transaction, TransactionSource, ) -from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( _get_installed_modules, capture_internal_exceptions, @@ -54,10 +51,9 @@ ) if TYPE_CHECKING: - from typing import Any, ContextManager, Dict, Optional, Tuple, Union + from typing import Any, ContextManager, Dict, Optional, Tuple from sentry_sdk._types import Attributes, Event, Hint - from sentry_sdk.tracing import Span _asgi_middleware_applied: "ContextVar[bool]" = ContextVar( @@ -202,9 +198,6 @@ async def _run_app( self._capture_lifespan_exception(exc) reraise(*exc_info) - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - _asgi_middleware_applied.set(True) try: with sentry_sdk.isolation_scope() as sentry_scope: @@ -225,95 +218,55 @@ async def _run_app( method = scope.get("method", "").upper() - span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" - if span_streaming: - segment: "Optional[StreamedSpan]" = None - attributes: "Attributes" = { - "sentry.segment.name.source": getattr( - transaction_source, "value", transaction_source - ), - "sentry.origin": self.span_origin, - "network.protocol.name": ty, - } - - if scope.get("client"): - client_options = sentry_sdk.get_client().options - if has_data_collection_enabled(client_options): - if client_options["data_collection"]["user_info"]: - sentry_scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, _get_ip(scope) - ) - elif should_send_default_pii(): + span: "Optional[ContextManager[Optional[StreamedSpan]]]" = None + attributes: "Attributes" = { + "sentry.segment.name.source": getattr( + transaction_source, "value", transaction_source + ), + "sentry.origin": self.span_origin, + "network.protocol.name": ty, + } + + if scope.get("client"): + client_options = sentry_sdk.get_client().options + if has_data_collection_enabled(client_options): + if client_options["data_collection"]["user_info"]: sentry_scope.set_attribute( SPANDATA.USER_IP_ADDRESS, _get_ip(scope) ) + elif should_send_default_pii(): + sentry_scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, _get_ip(scope) + ) - if ty in ("http", "websocket"): - if ( - ty == "websocket" - or method in self.http_methods_to_capture - ): - sentry_sdk.traces.continue_trace(_get_headers(scope)) - - Scope.set_custom_sampling_context({"asgi_scope": scope}) - - attributes["sentry.op"] = f"{ty}.server" - segment = sentry_sdk.traces.start_span( - name=transaction_name, - attributes=attributes, - parent_span=None, - ) - sentry_scope.get_current_scope()._server_segment_span = segment - else: - sentry_sdk.traces.new_trace() + if ty in ("http", "websocket"): + if ty == "websocket" or method in self.http_methods_to_capture: + sentry_sdk.traces.continue_trace(_get_headers(scope)) Scope.set_custom_sampling_context({"asgi_scope": scope}) - attributes["sentry.op"] = OP.HTTP_SERVER - segment = sentry_sdk.traces.start_span( + attributes["sentry.op"] = f"{ty}.server" + span = sentry_sdk.traces.start_span( name=transaction_name, attributes=attributes, parent_span=None, ) - sentry_scope.get_current_scope()._server_segment_span = ( - segment - ) - - span_ctx = segment or nullcontext() - + sentry_scope.get_current_scope()._server_segment_span = span else: - transaction = None - if ty in ("http", "websocket"): - if ( - ty == "websocket" - or method in self.http_methods_to_capture - ): - transaction = continue_trace( - _get_headers(scope), - op="{}.server".format(ty), - name=transaction_name, - source=transaction_source, - origin=self.span_origin, - ) - else: - transaction = Transaction( - op=OP.HTTP_SERVER, - name=transaction_name, - source=transaction_source, - origin=self.span_origin, - ) + sentry_sdk.traces.new_trace() - span_ctx = ( - sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"asgi_scope": scope}, - ) - if transaction is not None - else nullcontext() + Scope.set_custom_sampling_context({"asgi_scope": scope}) + + attributes["sentry.op"] = OP.HTTP_SERVER + span = sentry_sdk.traces.start_span( + name=transaction_name, + attributes=attributes, + parent_span=None, ) + sentry_scope.get_current_scope()._server_segment_span = span - with span_ctx as span: - if isinstance(span, StreamedSpan): + with span or nullcontext() as span: + if span is not None: for attribute, value in _get_request_attributes( scope, root_path_in_path=self.root_path_in_path, @@ -331,18 +284,13 @@ async def _sentry_wrapped_send( and "status" in event ) if is_http_response: - if isinstance(span, StreamedSpan): - span.status = ( - "error" - if event["status"] >= 400 - else "ok" - ) - span.set_attribute( - "http.response.status_code", - event["status"], - ) - else: - span.set_http_status(event["status"]) + span.status = ( + "error" if event["status"] >= 400 else "ok" + ) + span.set_attribute( + "http.response.status_code", + event["status"], + ) return await send(event) @@ -362,7 +310,7 @@ async def _sentry_wrapped_send( reraise(*exc_info) finally: - if isinstance(span, StreamedSpan): + if span is not None: already_set = ( span is not None and span.name != _DEFAULT_TRANSACTION_NAME diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index 2749c9235e..9365c83302 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -443,7 +443,11 @@ async def test_websocket( async def test_auto_session_tracking_with_aggregates( sentry_init, asgi3_app, capture_envelopes ): - sentry_init(send_default_pii=True, traces_sample_rate=1.0) + sentry_init( + send_default_pii=True, + traces_sample_rate=1.0, + trace_lifecycle="stream", + ) app = SentryAsgiMiddleware(asgi3_app) scope = { @@ -466,7 +470,7 @@ async def test_auto_session_tracking_with_aggregates( for envelope in envelopes: count_item_types[envelope.items[0].type] += 1 - assert count_item_types["transaction"] == 3 + assert count_item_types["span"] == 3 assert count_item_types["event"] == 1 assert count_item_types["sessions"] == 1 assert len(envelopes) == 5 @@ -674,36 +678,6 @@ def test_get_headers(): } -@pytest.mark.asyncio -async def test_get_request_data_url_with_filtered_host( - sentry_init, capture_events, asgi3_app -): - # allowlist mode in data collection that does not allow "host" scrubs the host - # header value, but the reported URL must still resolve rather than embedding the - # substituted "[Filtered]" value. - sentry_init( - traces_sample_rate=1.0, - _experiments={ - "data_collection": { - "http_headers": {"request": {"mode": "allowlist", "terms": []}} - } - }, - ) - app = SentryAsgiMiddleware(asgi3_app) - - events = capture_events() - scope = {"server": ("example.com", 80), "scheme": "http"} - async with TestClient(app, scope=scope) as client: - await client.get("/foo", headers={"host": "example.com"}) - - sentry_sdk.flush() - - (transaction_event,) = events - - assert transaction_event["request"]["headers"]["host"] == "[Filtered]" - assert transaction_event["request"]["url"] == "http://example.com/foo" - - @pytest.mark.asyncio async def test_get_request_attributes_url_with_filtered_host( sentry_init, capture_items, asgi3_app @@ -774,141 +748,6 @@ def _http_scope(): return {"server": ("example.com", 80), "scheme": "http"} -@pytest.mark.asyncio -@pytest.mark.parametrize( - "init_kwargs, expected_query_string", - [ - pytest.param( - {"send_default_pii": True}, - QUERY_STRING, - id="send_default_pii_true", - ), - pytest.param( - {"send_default_pii": False}, - QUERY_STRING, - id="send_default_pii_false", - ), - pytest.param( - {}, - QUERY_STRING, - id="defaults", - ), - pytest.param( - {"_experiments": {"data_collection": {}}}, - "token=%5BFiltered%5D&theme=dark&lang=en&session=%5BFiltered%5D", - id="data_collection_denylist_default", - ), - pytest.param( - { - "_experiments": { - "data_collection": { - "url_query_params": {"mode": "denylist", "terms": ["theme"]} - } - } - }, - "token=%5BFiltered%5D&theme=%5BFiltered%5D&lang=en&session=%5BFiltered%5D", - id="data_collection_denylist_custom_terms", - ), - pytest.param( - { - "_experiments": { - "data_collection": { - "url_query_params": {"mode": "allowlist", "terms": ["theme"]} - } - } - }, - "token=%5BFiltered%5D&theme=dark&lang=%5BFiltered%5D&session=%5BFiltered%5D", - id="data_collection_allowlist", - ), - pytest.param( - { - "_experiments": { - "data_collection": { - "url_query_params": {"mode": "allowlist", "terms": ["token"]} - } - } - }, - "token=%5BFiltered%5D&theme=%5BFiltered%5D&lang=%5BFiltered%5D&session=%5BFiltered%5D", - id="data_collection_allowlist_sensitive_term", - ), - pytest.param( - { - "_experiments": { - "data_collection": {"url_query_params": {"mode": "off"}} - } - }, - None, - id="data_collection_off", - ), - # data_collection wins over send_default_pii: filtering still applies. - pytest.param( - { - "send_default_pii": True, - "_experiments": { - "data_collection": {"url_query_params": {"mode": "off"}} - }, - }, - None, - id="data_collection_wins_over_send_default_pii", - ), - ], -) -async def test_get_request_data_query_string_data_collection( - sentry_init, capture_events, asgi3_app, init_kwargs, expected_query_string -): - sentry_init(traces_sample_rate=1.0, **init_kwargs) - app = SentryAsgiMiddleware(asgi3_app) - - events = capture_events() - async with TestClient(app, scope=_http_scope()) as client: - await client.get(f"/foo?{QUERY_STRING}", headers={"host": "example.com"}) - - sentry_sdk.flush() - - (transaction_event,) = events - request_data = transaction_event["request"] - - if expected_query_string is None: - assert "query_string" not in request_data - else: - assert request_data["query_string"] == expected_query_string - - -@pytest.mark.asyncio -async def test_get_request_data_query_string_empty_legacy_is_none( - sentry_init, capture_events, asgi3_app -): - # Legacy path: the query string is always set even when empty (``None``). - sentry_init(send_default_pii=True, traces_sample_rate=1.0) - app = SentryAsgiMiddleware(asgi3_app) - - events = capture_events() - async with TestClient(app, scope=_http_scope()) as client: - await client.get("/foo", headers={"host": "example.com"}) - - sentry_sdk.flush() - - (transaction_event,) = events - assert transaction_event["request"]["query_string"] is None - - -@pytest.mark.asyncio -async def test_get_request_data_empty_query_string_dropped_with_data_collection( - sentry_init, capture_events, asgi3_app -): - sentry_init(traces_sample_rate=1.0, _experiments={"data_collection": {}}) - app = SentryAsgiMiddleware(asgi3_app) - - events = capture_events() - async with TestClient(app, scope=_http_scope()) as client: - await client.get("/foo", headers={"host": "example.com"}) - - sentry_sdk.flush() - - (transaction_event,) = events - assert "query_string" not in transaction_event["request"] - - @pytest.mark.asyncio @pytest.mark.parametrize( "init_kwargs, expected_query, expected_url_full", @@ -1052,33 +891,6 @@ async def test_get_request_attributes_query_data_collection( ] -@pytest.mark.asyncio -@pytest.mark.parametrize("init_kwargs, has_client, expect_ip", USER_INFO_CASES) -async def test_get_request_data_env_user_info( - sentry_init, capture_events, asgi3_app, init_kwargs, has_client, expect_ip -): - sentry_init(traces_sample_rate=1.0, **init_kwargs) - app = SentryAsgiMiddleware(asgi3_app) - - scope = _http_scope() - if has_client: - scope["client"] = ("127.0.0.1", 60457) - - events = capture_events() - async with TestClient(app, scope=scope) as client: - await client.get("/foo", headers={"host": "example.com"}) - - sentry_sdk.flush() - - (transaction_event,) = events - request_data = transaction_event["request"] - - if expect_ip: - assert request_data["env"] == {"REMOTE_ADDR": "127.0.0.1"} - else: - assert "env" not in request_data - - @pytest.mark.asyncio @pytest.mark.parametrize("init_kwargs, has_client, expect_ip", USER_INFO_CASES) async def test_get_request_attributes_client_address_user_info(