diff --git a/sentry_sdk/integrations/_asgi_common.py b/sentry_sdk/integrations/_asgi_common.py index a8022c6bb1..6e9d9d2881 100644 --- a/sentry_sdk/integrations/_asgi_common.py +++ b/sentry_sdk/integrations/_asgi_common.py @@ -12,6 +12,7 @@ from typing import Union from typing_extensions import Literal + from sentry_sdk._types import Attributes from sentry_sdk.utils import AnnotatedValue @@ -105,3 +106,33 @@ def _get_request_data(asgi_scope: "Any") -> "Dict[str, Any]": request_data["env"] = {"REMOTE_ADDR": _get_ip(asgi_scope)} return request_data + + +def _get_request_attributes(asgi_scope: "Any") -> "dict[str, Any]": + """ + Return attributes related to the HTTP request from the ASGI scope. + """ + attributes: "Attributes" = {} + + ty = asgi_scope["type"] + if ty in ("http", "websocket"): + if asgi_scope.get("method"): + attributes["http.request.method"] = asgi_scope["method"].upper() + + headers = _filter_headers(_get_headers(asgi_scope), use_annotated_value=False) + for header, value in headers.items(): + attributes[f"http.request.header.{header.lower()}"] = value + + query = _get_query(asgi_scope) + if query: + attributes["http.query"] = query + + attributes["url.full"] = _get_url( + asgi_scope, "http" if ty == "http" else "ws", headers.get("host") + ) + + client = asgi_scope.get("client") + if client and should_send_default_pii(): + attributes["client.address"] = _get_ip(asgi_scope) + + return attributes diff --git a/sentry_sdk/integrations/_wsgi_common.py b/sentry_sdk/integrations/_wsgi_common.py index 688e965be4..8a23f77e83 100644 --- a/sentry_sdk/integrations/_wsgi_common.py +++ b/sentry_sdk/integrations/_wsgi_common.py @@ -3,6 +3,7 @@ from copy import deepcopy import sentry_sdk +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE from sentry_sdk.scope import should_send_default_pii from sentry_sdk.utils import AnnotatedValue, logger @@ -211,16 +212,18 @@ def _is_json_content_type(ct: "Optional[str]") -> bool: def _filter_headers( headers: "Mapping[str, str]", + use_annotated_value: bool = True, ) -> "Mapping[str, Union[AnnotatedValue, str]]": if should_send_default_pii(): return headers + if use_annotated_value: + substitute = AnnotatedValue.removed_because_over_size_limit() + else: + substitute = SENSITIVE_DATA_SUBSTITUTE + return { - k: ( - v - if k.upper().replace("-", "_") not in SENSITIVE_HEADERS - else AnnotatedValue.removed_because_over_size_limit() - ) + k: (v if k.upper().replace("-", "_") not in SENSITIVE_HEADERS else substitute) for k, v in headers.items() } diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 2294781f05..e8287bc55c 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -15,6 +15,7 @@ from sentry_sdk.consts import OP from sentry_sdk.integrations._asgi_common import ( _get_headers, + _get_request_attributes, _get_request_data, _get_url, ) @@ -23,7 +24,11 @@ nullcontext, ) from sentry_sdk.sessions import track_session -from sentry_sdk.traces import StreamedSpan +from sentry_sdk.traces import ( + StreamedSpan, + SegmentSource, + SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, +) from sentry_sdk.tracing import ( SOURCE_FOR_STYLE, Transaction, @@ -40,6 +45,7 @@ _get_installed_modules, reraise, capture_internal_exceptions, + qualname_from_function, ) from typing import TYPE_CHECKING @@ -235,7 +241,7 @@ async def _run_app( transaction_source, "value", transaction_source ), "sentry.origin": self.span_origin, - "asgi.type": ty, + "network.protocol.name": ty, } if ty in ("http", "websocket"): @@ -301,6 +307,9 @@ async def _run_app( else nullcontext() ) + for attribute, value in _get_request_attributes(scope).items(): + sentry_scope.set_attribute(attribute, value) + with span_ctx as span: try: @@ -336,6 +345,7 @@ async def _sentry_wrapped_send( return await self.app( scope, receive, _sentry_wrapped_send ) + except Exception as exc: suppress_chained_exceptions = ( sentry_sdk.get_client() @@ -350,6 +360,26 @@ async def _sentry_wrapped_send( with capture_internal_exceptions(): self._capture_request_exception(exc) reraise(*exc_info) + + finally: + if isinstance(span, StreamedSpan): + already_set = ( + span is not None + and span.name != _DEFAULT_TRANSACTION_NAME + and span.get_attributes().get("sentry.span.source") + in [ + SegmentSource.COMPONENT.value, + SegmentSource.ROUTE.value, + SegmentSource.CUSTOM.value, + ] + ) + with capture_internal_exceptions(): + if not already_set: + name, source = self._get_segment_name_and_source( + self.transaction_style, scope + ) + span.name = name + span.set_attribute("sentry.span.source", source) finally: _asgi_middleware_applied.set(False) @@ -424,3 +454,40 @@ def _get_transaction_name_and_source( return name, source return name, source + + def _get_segment_name_and_source( + self: "SentryAsgiMiddleware", segment_style: str, asgi_scope: "Any" + ) -> "Tuple[str, str]": + name = None + source = SEGMENT_SOURCE_FOR_STYLE[segment_style].value + ty = asgi_scope.get("type") + + if segment_style == "endpoint": + endpoint = asgi_scope.get("endpoint") + # Webframeworks like Starlette mutate the ASGI env once routing is + # done, which is sometime after the request has started. If we have + # an endpoint, overwrite our generic transaction name. + if endpoint: + name = qualname_from_function(endpoint) or "" + else: + name = _get_url(asgi_scope, "http" if ty == "http" else "ws", host=None) + source = SegmentSource.URL.value + + elif segment_style == "url": + # FastAPI includes the route object in the scope to let Sentry extract the + # path from it for the transaction name + route = asgi_scope.get("route") + if route: + path = getattr(route, "path", None) + if path is not None: + name = path + else: + name = _get_url(asgi_scope, "http" if ty == "http" else "ws", host=None) + source = SegmentSource.URL.value + + if name is None: + name = _DEFAULT_TRANSACTION_NAME + source = SegmentSource.ROUTE.value + return name, source + + return name, source diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 944e17e5d7..f44ef71f5b 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -259,6 +259,7 @@ def __init__( self._name: str = name self._active: bool = active self._attributes: "Attributes" = {} + if attributes: for attribute, value in attributes.items(): self.set_attribute(attribute, value) @@ -287,7 +288,6 @@ def __init__( self._span_id: "Optional[str]" = None self._status = SpanStatus.OK.value - self.set_attribute("sentry.span.source", SegmentSource.CUSTOM.value) self._update_active_thread() diff --git a/tests/conftest.py b/tests/conftest.py index 71f2431aac..2828dbc733 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ import brotli import gzip import io +from dataclasses import dataclass from threading import Thread from contextlib import contextmanager from http.server import BaseHTTPRequestHandler, HTTPServer @@ -320,6 +321,52 @@ def append_envelope(envelope): return inner +@dataclass +class UnwrappedItem: + type: str + payload: dict + + +@pytest.fixture +def capture_items(monkeypatch): + """ + Capture envelope payload, unfurling individual items. + + Makes it easier to work with both events and attribute-based telemetry in + one test. + """ + + def inner(*types): + telemetry = [] + test_client = sentry_sdk.get_client() + old_capture_envelope = test_client.transport.capture_envelope + + def append_envelope(envelope): + for item in envelope: + if types and item.type not in types: + continue + + if item.type in ("metric", "log", "span"): + for json in item.payload.json["items"]: + t = {k: v for k, v in json.items() if k != "attributes"} + t["attributes"] = { + k: v["value"] for k, v in json["attributes"].items() + } + telemetry.append(UnwrappedItem(type=item.type, payload=t)) + else: + telemetry.append( + UnwrappedItem(type=item.type, payload=item.payload.json) + ) + + return old_capture_envelope(envelope) + + monkeypatch.setattr(test_client.transport, "capture_envelope", append_envelope) + + return telemetry + + return inner + + @pytest.fixture def capture_record_lost_event_calls(monkeypatch): def inner(): diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index ec2796c140..7f44c9d00a 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -164,55 +164,117 @@ def test_invalid_transaction_style(asgi3_app): @pytest.mark.asyncio +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) async def test_capture_transaction( sentry_init, asgi3_app, capture_events, + capture_items, + span_streaming, ): - sentry_init(send_default_pii=True, traces_sample_rate=1.0) + sentry_init( + send_default_pii=True, + traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) app = SentryAsgiMiddleware(asgi3_app) async with TestClient(app) as client: - events = capture_events() + if span_streaming: + items = capture_items("span") + else: + events = capture_events() await client.get("/some_url?somevalue=123") - (transaction_event,) = events - - assert transaction_event["type"] == "transaction" - assert transaction_event["transaction"] == "/some_url" - assert transaction_event["transaction_info"] == {"source": "url"} - assert transaction_event["contexts"]["trace"]["op"] == "http.server" - assert transaction_event["request"] == { - "headers": { - "host": "localhost", - "remote-addr": "127.0.0.1", - "user-agent": "ASGI-Test-Client", - }, - "method": "GET", - "query_string": "somevalue=123", - "url": "http://localhost/some_url", - } + sentry_sdk.flush() + + if span_streaming: + assert len(items) == 1 + span = items[0].payload + + assert span["is_segment"] is True + assert span["name"] == "/some_url" + + assert span["attributes"]["sentry.span.source"] == "url" + assert span["attributes"]["sentry.op"] == "http.server" + + assert span["attributes"]["url.full"] == "http://localhost/some_url" + assert span["attributes"]["network.protocol.name"] == "http" + assert span["attributes"]["http.request.method"] == "GET" + assert span["attributes"]["http.query"] == "somevalue=123" + assert span["attributes"]["http.request.header.host"] == "localhost" + assert span["attributes"]["http.request.header.remote-addr"] == "127.0.0.1" + assert ( + span["attributes"]["http.request.header.user-agent"] == "ASGI-Test-Client" + ) + + else: + (transaction_event,) = events + + assert transaction_event["type"] == "transaction" + assert transaction_event["transaction"] == "/some_url" + assert transaction_event["transaction_info"] == {"source": "url"} + assert transaction_event["contexts"]["trace"]["op"] == "http.server" + assert transaction_event["request"] == { + "headers": { + "host": "localhost", + "remote-addr": "127.0.0.1", + "user-agent": "ASGI-Test-Client", + }, + "method": "GET", + "query_string": "somevalue=123", + "url": "http://localhost/some_url", + } @pytest.mark.asyncio +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) async def test_capture_transaction_with_error( sentry_init, asgi3_app_with_error, capture_events, + capture_items, DictionaryContaining, # noqa: N803 + span_streaming, ): - sentry_init(send_default_pii=True, traces_sample_rate=1.0) + sentry_init( + send_default_pii=True, + traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) + app = SentryAsgiMiddleware(asgi3_app_with_error) - events = capture_events() + if span_streaming: + items = capture_items("event", "span") + else: + events = capture_events() + with pytest.raises(ZeroDivisionError): async with TestClient(app) as client: await client.get("/some_url") - ( - error_event, - transaction_event, - ) = events + sentry_sdk.flush() + + if span_streaming: + assert len(items) == 2 + assert items[0].type == "event" + assert items[1].type == "span" + + error_event = items[0].payload + span_item = items[1].payload + else: + (error_event, transaction_event) = events assert error_event["transaction"] == "/some_url" assert error_event["transaction_info"] == {"source": "url"} @@ -222,45 +284,94 @@ async def test_capture_transaction_with_error( assert error_event["exception"]["values"][0]["mechanism"]["handled"] is False assert error_event["exception"]["values"][0]["mechanism"]["type"] == "asgi" - assert transaction_event["type"] == "transaction" - assert transaction_event["contexts"]["trace"] == DictionaryContaining( - error_event["contexts"]["trace"] - ) - assert transaction_event["contexts"]["trace"]["status"] == "internal_error" - assert transaction_event["transaction"] == error_event["transaction"] - assert transaction_event["request"] == error_event["request"] + if span_streaming: + assert span_item["trace_id"] == error_event["contexts"]["trace"]["trace_id"] + assert span_item["span_id"] == error_event["contexts"]["trace"]["span_id"] + assert span_item.get("parent_span_id") == error_event["contexts"]["trace"].get( + "parent_span_id" + ) + assert span_item["status"] == "error" + + else: + assert transaction_event["type"] == "transaction" + assert transaction_event["contexts"]["trace"] == DictionaryContaining( + error_event["contexts"]["trace"] + ) + assert transaction_event["contexts"]["trace"]["status"] == "internal_error" + assert transaction_event["transaction"] == error_event["transaction"] + assert transaction_event["request"] == error_event["request"] @pytest.mark.asyncio +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) async def test_has_trace_if_performance_enabled( sentry_init, asgi3_app_with_error_and_msg, capture_events, + capture_items, + span_streaming, ): - sentry_init(traces_sample_rate=1.0) + sentry_init( + traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) app = SentryAsgiMiddleware(asgi3_app_with_error_and_msg) with pytest.raises(ZeroDivisionError): async with TestClient(app) as client: - events = capture_events() + if span_streaming: + items = capture_items("event", "span") + else: + events = capture_events() await client.get("/") - msg_event, error_event, transaction_event = events + sentry_sdk.flush() - assert msg_event["contexts"]["trace"] - assert "trace_id" in msg_event["contexts"]["trace"] + if span_streaming: + msg_event, error_event, span = items - assert error_event["contexts"]["trace"] - assert "trace_id" in error_event["contexts"]["trace"] + assert msg_event.type == "event" + msg_event = msg_event.payload + assert msg_event["contexts"]["trace"] + assert "trace_id" in msg_event["contexts"]["trace"] - assert transaction_event["contexts"]["trace"] - assert "trace_id" in transaction_event["contexts"]["trace"] + assert error_event.type == "event" + error_event = error_event.payload + assert error_event["contexts"]["trace"] + assert "trace_id" in error_event["contexts"]["trace"] - assert ( - error_event["contexts"]["trace"]["trace_id"] - == transaction_event["contexts"]["trace"]["trace_id"] - == msg_event["contexts"]["trace"]["trace_id"] - ) + assert span.type == "span" + span = span.payload + assert span["trace_id"] is not None + + assert ( + error_event["contexts"]["trace"]["trace_id"] + == msg_event["contexts"]["trace"]["trace_id"] + == span["trace_id"] + ) + + else: + msg_event, error_event, transaction_event = events + + assert msg_event["contexts"]["trace"] + assert "trace_id" in msg_event["contexts"]["trace"] + + assert error_event["contexts"]["trace"] + assert "trace_id" in error_event["contexts"]["trace"] + + assert transaction_event["contexts"]["trace"] + assert "trace_id" in transaction_event["contexts"]["trace"] + + assert ( + error_event["contexts"]["trace"]["trace_id"] + == transaction_event["contexts"]["trace"]["trace_id"] + == msg_event["contexts"]["trace"]["trace_id"] + ) @pytest.mark.asyncio @@ -286,13 +397,24 @@ async def test_has_trace_if_performance_disabled( assert "trace_id" in error_event["contexts"]["trace"] +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) @pytest.mark.asyncio async def test_trace_from_headers_if_performance_enabled( sentry_init, asgi3_app_with_error_and_msg, capture_events, + capture_items, + span_streaming, ): - sentry_init(traces_sample_rate=1.0) + sentry_init( + traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) app = SentryAsgiMiddleware(asgi3_app_with_error_and_msg) trace_id = "582b43a4192642f0b136d5159a501701" @@ -300,23 +422,50 @@ async def test_trace_from_headers_if_performance_enabled( with pytest.raises(ZeroDivisionError): async with TestClient(app) as client: - events = capture_events() + if span_streaming: + items = capture_items("event", "span") + else: + events = capture_events() await client.get("/", headers={"sentry-trace": sentry_trace_header}) - msg_event, error_event, transaction_event = events + sentry_sdk.flush() - assert msg_event["contexts"]["trace"] - assert "trace_id" in msg_event["contexts"]["trace"] + if span_streaming: + msg_event, error_event, span = items - assert error_event["contexts"]["trace"] - assert "trace_id" in error_event["contexts"]["trace"] + assert msg_event.type == "event" + msg_event = msg_event.payload + assert msg_event["contexts"]["trace"] + assert "trace_id" in msg_event["contexts"]["trace"] - assert transaction_event["contexts"]["trace"] - assert "trace_id" in transaction_event["contexts"]["trace"] + assert error_event.type == "event" + error_event = error_event.payload + assert error_event["contexts"]["trace"] + assert "trace_id" in error_event["contexts"]["trace"] - assert msg_event["contexts"]["trace"]["trace_id"] == trace_id - assert error_event["contexts"]["trace"]["trace_id"] == trace_id - assert transaction_event["contexts"]["trace"]["trace_id"] == trace_id + assert span.type == "span" + span = span.payload + assert span["trace_id"] is not None + + assert msg_event["contexts"]["trace"]["trace_id"] == trace_id + assert error_event["contexts"]["trace"]["trace_id"] == trace_id + assert span["trace_id"] == trace_id + + else: + msg_event, error_event, transaction_event = events + + assert msg_event["contexts"]["trace"] + assert "trace_id" in msg_event["contexts"]["trace"] + + assert error_event["contexts"]["trace"] + assert "trace_id" in error_event["contexts"]["trace"] + + assert transaction_event["contexts"]["trace"] + assert "trace_id" in transaction_event["contexts"]["trace"] + + assert msg_event["contexts"]["trace"]["trace_id"] == trace_id + assert error_event["contexts"]["trace"]["trace_id"] == trace_id + assert transaction_event["contexts"]["trace"]["trace_id"] == trace_id @pytest.mark.asyncio @@ -348,10 +497,25 @@ async def test_trace_from_headers_if_performance_disabled( @pytest.mark.asyncio -async def test_websocket(sentry_init, asgi3_ws_app, capture_events, request): - sentry_init(send_default_pii=True, traces_sample_rate=1.0) - - events = capture_events() +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) +async def test_websocket( + sentry_init, + asgi3_ws_app, + capture_events, + capture_items, + request, + span_streaming, +): + sentry_init( + send_default_pii=True, + traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) asgi3_ws_app = SentryAsgiMiddleware(asgi3_ws_app) @@ -359,21 +523,48 @@ async def test_websocket(sentry_init, asgi3_ws_app, capture_events, request): with pytest.raises(ValueError): client = TestClient(asgi3_ws_app) + if span_streaming: + items = capture_items("event", "span") + else: + events = capture_events() async with client.websocket_connect(request_url) as ws: await ws.receive_text() - msg_event, error_event, transaction_event = events + sentry_sdk.flush() + + if span_streaming: + msg_event, error_event, span = items + + assert msg_event.type == "event" + msg_event = msg_event.payload + assert msg_event["transaction"] == request_url + assert msg_event["transaction_info"] == {"source": "url"} + assert msg_event["message"] == "Some message to the world!" + + assert error_event.type == "event" + error_event = error_event.payload + (exc,) = error_event["exception"]["values"] + assert exc["type"] == "ValueError" + assert exc["value"] == "Oh no" + + assert span.type == "span" + span = span.payload + assert span["name"] == request_url + assert span["attributes"]["sentry.span.source"] == "url" + + else: + msg_event, error_event, transaction_event = events - assert msg_event["transaction"] == request_url - assert msg_event["transaction_info"] == {"source": "url"} - assert msg_event["message"] == "Some message to the world!" + assert msg_event["transaction"] == request_url + assert msg_event["transaction_info"] == {"source": "url"} + assert msg_event["message"] == "Some message to the world!" - (exc,) = error_event["exception"]["values"] - assert exc["type"] == "ValueError" - assert exc["value"] == "Oh no" + (exc,) = error_event["exception"]["values"] + assert exc["type"] == "ValueError" + assert exc["value"] == "Oh no" - assert transaction_event["transaction"] == request_url - assert transaction_event["transaction_info"] == {"source": "url"} + assert transaction_event["transaction"] == request_url + assert transaction_event["transaction_info"] == {"source": "url"} @pytest.mark.asyncio @@ -431,17 +622,29 @@ async def test_auto_session_tracking_with_aggregates( ), ], ) +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) @pytest.mark.asyncio async def test_transaction_style( sentry_init, asgi3_app, capture_events, + capture_items, url, transaction_style, expected_transaction, expected_source, + span_streaming, ): - sentry_init(send_default_pii=True, traces_sample_rate=1.0) + sentry_init( + send_default_pii=True, + traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) app = SentryAsgiMiddleware(asgi3_app, transaction_style=transaction_style) scope = { @@ -451,13 +654,26 @@ async def test_transaction_style( } async with TestClient(app, scope=scope) as client: - events = capture_events() + if span_streaming: + items = capture_items("span") + else: + events = capture_events() await client.get(url) - (transaction_event,) = events + sentry_sdk.flush() + + if span_streaming: + assert len(items) == 1 + span = items[0].payload + + assert span["name"] == expected_transaction + assert span["attributes"]["sentry.span.source"] == expected_source + + else: + (transaction_event,) = events - assert transaction_event["transaction"] == expected_transaction - assert transaction_event["transaction_info"] == {"source": expected_source} + assert transaction_event["transaction"] == expected_transaction + assert transaction_event["transaction_info"] == {"source": expected_source} def mock_asgi2_app(): @@ -622,6 +838,10 @@ def test_get_headers(): ), ], ) +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) async def test_transaction_name( sentry_init, request_url, @@ -630,28 +850,47 @@ async def test_transaction_name( expected_transaction_source, asgi3_app, capture_envelopes, + capture_items, + span_streaming, ): """ Tests that the transaction name is something meaningful. """ sentry_init( traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, ) - envelopes = capture_envelopes() + if span_streaming: + items = capture_items("span") + else: + envelopes = capture_envelopes() app = SentryAsgiMiddleware(asgi3_app, transaction_style=transaction_style) async with TestClient(app) as client: await client.get(request_url) - (transaction_envelope,) = envelopes - transaction_event = transaction_envelope.get_transaction_event() + if span_streaming: + sentry_sdk.flush() - assert transaction_event["transaction"] == expected_transaction_name - assert ( - transaction_event["transaction_info"]["source"] == expected_transaction_source - ) + assert len(items) == 1 + span = items[0].payload + + assert span["name"] == expected_transaction_name + assert span["attributes"]["sentry.span.source"] == expected_transaction_source + + else: + (transaction_envelope,) = envelopes + transaction_event = transaction_envelope.get_transaction_event() + + assert transaction_event["transaction"] == expected_transaction_name + assert ( + transaction_event["transaction_info"]["source"] + == expected_transaction_source + ) @pytest.mark.asyncio @@ -672,6 +911,10 @@ async def test_transaction_name( ), ], ) +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) async def test_transaction_name_in_traces_sampler( sentry_init, request_url, @@ -679,6 +922,7 @@ async def test_transaction_name_in_traces_sampler( expected_transaction_name, expected_transaction_source, asgi3_app, + span_streaming, ): """ Tests that a custom traces_sampler has a meaningful transaction name. @@ -686,17 +930,28 @@ async def test_transaction_name_in_traces_sampler( """ def dummy_traces_sampler(sampling_context): - assert ( - sampling_context["transaction_context"]["name"] == expected_transaction_name - ) - assert ( - sampling_context["transaction_context"]["source"] - == expected_transaction_source - ) + if span_streaming: + assert sampling_context["span_context"]["name"] == expected_transaction_name + assert ( + sampling_context["span_context"]["attributes"]["sentry.span.source"] + == expected_transaction_source + ) + else: + assert ( + sampling_context["transaction_context"]["name"] + == expected_transaction_name + ) + assert ( + sampling_context["transaction_context"]["source"] + == expected_transaction_source + ) sentry_init( traces_sampler=dummy_traces_sampler, traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, ) app = SentryAsgiMiddleware(asgi3_app, transaction_style=transaction_style) @@ -706,17 +961,44 @@ def dummy_traces_sampler(sampling_context): @pytest.mark.asyncio +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) async def test_custom_transaction_name( - sentry_init, asgi3_custom_transaction_app, capture_events + sentry_init, + asgi3_custom_transaction_app, + capture_events, + capture_items, + span_streaming, ): - sentry_init(traces_sample_rate=1.0) - events = capture_events() + sentry_init( + traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) app = SentryAsgiMiddleware(asgi3_custom_transaction_app) async with TestClient(app) as client: + if span_streaming: + items = capture_items("span") + else: + events = capture_events() await client.get("/test") - (transaction_event,) = events - assert transaction_event["type"] == "transaction" - assert transaction_event["transaction"] == "foobar" - assert transaction_event["transaction_info"] == {"source": "custom"} + sentry_sdk.flush() + + if span_streaming: + assert len(items) == 1 + span = items[0].payload + + assert span["is_segment"] is True + assert span["name"] == "foobar" + assert span["attributes"]["sentry.span.source"] == "custom" + + else: + (transaction_event,) = events + assert transaction_event["type"] == "transaction" + assert transaction_event["transaction"] == "foobar" + assert transaction_event["transaction_info"] == {"source": "custom"} diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index 21c3d26ea3..445c8cfb99 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -2,8 +2,8 @@ import re import sys import time -from typing import Any from unittest import mock +from typing import Any import pytest @@ -11,6 +11,7 @@ from sentry_sdk.profiler.continuous_profiler import get_profiler_id from sentry_sdk.traces import NoOpStreamedSpan, SpanStatus, StreamedSpan + minimum_python_38 = pytest.mark.skipif( sys.version_info < (3, 8), reason="Asyncio tests need Python >= 3.8" )