diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 8d8ee0d682..168de1cfcb 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -20,7 +20,18 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - clickhouse-driver integration: The query is now available under the `db.query.text` span attribute (only if `send_default_pii` is `True`). - `sentry_sdk.init` now returns `None` instead of a context manager. - The `sampling_context` argument of `traces_sampler` now additionally contains all span attributes known at span start. -- The `sampling_context` argument of `traces_sampler` doesn't contain the `asgi_scope` object anymore for ASGI frameworks. Instead, the individual properties, if available, are accessible as `asgi_scope.endpoint`, `asgi_scope.path`, `asgi_scope.root_path`, `asgi_scope.route`, `asgi_scope.scheme`, `asgi_scope.server` and `asgi_scope.type`. +- The `sampling_context` argument of `traces_sampler` doesn't contain the `asgi_scope` object anymore for ASGI frameworks. Instead, the individual properties on the scope, if available, are accessible as follows: + + | Scope property | Sampling context key(s) | + | -------------- | ------------------------------- | + | `type` | `network.protocol.name` | + | `scheme` | `url.scheme` | + | `path` | `url.path` | + | `http_version` | `network.protocol.version` | + | `method` | `http.request.method` | + | `server` | `server.address`, `server.port` | + | `client` | `client.address`, `client.port` | + | full URL | `url.full` | ### Removed diff --git a/sentry_sdk/integrations/_asgi_common.py b/sentry_sdk/integrations/_asgi_common.py index ca030d6f45..52ecdbfd58 100644 --- a/sentry_sdk/integrations/_asgi_common.py +++ b/sentry_sdk/integrations/_asgi_common.py @@ -32,8 +32,8 @@ def _get_headers(asgi_scope): return headers -def _get_url(asgi_scope, default_scheme, host): - # type: (Dict[str, Any], Literal["ws", "http"], Optional[Union[AnnotatedValue, str]]) -> str +def _get_url(asgi_scope, default_scheme=None, host=None): + # type: (Dict[str, Any], Optional[Literal["ws", "http"]], Optional[Union[AnnotatedValue, str]]) -> str """ Extract URL from the ASGI scope, without also including the querystring. """ diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 73801ed102..b2ecfe23b7 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -15,6 +15,7 @@ from sentry_sdk.integrations._asgi_common import ( _get_headers, + _get_query, _get_request_data, _get_url, ) @@ -57,6 +58,14 @@ TRANSACTION_STYLE_VALUES = ("endpoint", "url") +ASGI_SCOPE_PROPERTY_TO_ATTRIBUTE = { + "http_version": "network.protocol.version", + "method": "http.request.method", + "path": "url.path", + "scheme": "url.scheme", + "type": "network.protocol.name", +} + def _capture_exception(exc, mechanism_type="asgi"): # type: (Any, str) -> None @@ -213,23 +222,21 @@ async def _run_app(self, scope, receive, send, asgi_version): ) if should_trace else nullcontext() - ) as transaction: - if transaction is not None: - logger.debug( - "[ASGI] Started transaction: %s", transaction - ) - transaction.set_tag("asgi.type", ty) + ) as span: + if span is not None: + logger.debug("[ASGI] Started transaction: %s", span) + span.set_tag("asgi.type", ty) try: async def _sentry_wrapped_send(event): # type: (Dict[str, Any]) -> Any is_http_response = ( event.get("type") == "http.response.start" - and transaction is not None + and span is not None and "status" in event ) if is_http_response: - transaction.set_http_status(event["status"]) + span.set_http_status(event["status"]) return await send(event) @@ -328,12 +335,31 @@ def _get_transaction_name_and_source(self, transaction_style, asgi_scope): def _prepopulate_attributes(scope): # type: (Any) -> dict[str, Any] - """Unpack asgi_scope into serializable attributes.""" + """Unpack ASGI scope into serializable OTel attributes.""" scope = scope or {} attributes = {} - for attr in ("endpoint", "path", "root_path", "route", "scheme", "server", "type"): + for attr, key in ASGI_SCOPE_PROPERTY_TO_ATTRIBUTE.items(): if scope.get(attr): - attributes[f"asgi_scope.{attr}"] = scope[attr] + attributes[key] = scope[attr] + + for attr in ("client", "server"): + if scope.get(attr): + try: + host, port = scope[attr] + attributes[f"{attr}.address"] = host + attributes[f"{attr}.port"] = port + except Exception: + pass + + try: + full_url = _get_url(scope) + query = _get_query(scope) + if query: + full_url = f"{full_url}?{query}" + + attributes["url.full"] = full_url + except Exception: + pass return attributes diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index fb97c385a0..74f6d8cc49 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -726,8 +726,12 @@ async def test_custom_transaction_name( @pytest.mark.asyncio async def test_asgi_scope_in_traces_sampler(sentry_init, asgi3_app): def dummy_traces_sampler(sampling_context): - assert sampling_context["asgi_scope.path"] == "/test" - assert sampling_context["asgi_scope.scheme"] == "http" + assert sampling_context["url.path"] == "/test" + assert sampling_context["url.scheme"] == "http" + assert sampling_context["url.full"] == "/test?hello=there" + assert sampling_context["http.request.method"] == "GET" + assert sampling_context["network.protocol.version"] == "1.1" + assert sampling_context["network.protocol.name"] == "http" sentry_init( traces_sampler=dummy_traces_sampler, @@ -737,4 +741,4 @@ def dummy_traces_sampler(sampling_context): app = SentryAsgiMiddleware(asgi3_app) async with TestClient(app) as client: - await client.get("/test") + await client.get("/test?hello=there") diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 97aea06344..b425ceebe6 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -246,7 +246,6 @@ async def _error(request: Request): assert event["request"]["headers"]["authorization"] == "[Filtered]" -@pytest.mark.asyncio def test_response_status_code_ok_in_transaction_context(sentry_init, capture_envelopes): """ Tests that the response status code is added to the transaction "response" context. @@ -275,7 +274,6 @@ def test_response_status_code_ok_in_transaction_context(sentry_init, capture_env assert transaction["contexts"]["response"]["status_code"] == 200 -@pytest.mark.asyncio def test_response_status_code_error_in_transaction_context( sentry_init, capture_envelopes, @@ -312,7 +310,6 @@ def test_response_status_code_error_in_transaction_context( assert transaction["contexts"]["response"]["status_code"] == 500 -@pytest.mark.asyncio def test_response_status_code_not_found_in_transaction_context( sentry_init, capture_envelopes,