diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index e2f3dd8803..da84dc1758 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -21,13 +21,27 @@ 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 on the scope, if available, are accessible as follows: +- The `sampling_context` argument of `traces_sampler` doesn't contain the `wsgi_environ` object anymore for WSGI frameworks. Instead, the individual properties of the environment are accessible, if available, as follows: + + | Env property | Sampling context key(s) | + | ----------------- | ------------------------------------------------- | + | `PATH_INFO` | `url.path` | + | `QUERY_STRING` | `url.query` | + | `REQUEST_METHOD` | `http.request.method` | + | `SERVER_NAME` | `server.address` | + | `SERVER_PORT` | `server.port` | + | `SERVER_PROTOCOL` | `server.protocol.name`, `server.protocol.version` | + | `wsgi.url_scheme` | `url.scheme` | + | full URL | `url.full` | + +- The `sampling_context` argument of `traces_sampler` doesn't contain the `asgi_scope` object anymore for ASGI frameworks. Instead, the individual properties of the scope, if available, are accessible as follows: | Scope property | Sampling context key(s) | | -------------- | ------------------------------- | | `type` | `network.protocol.name` | | `scheme` | `url.scheme` | | `path` | `url.path` | + | `query` | `url.query` | | `http_version` | `network.protocol.version` | | `method` | `http.request.method` | | `server` | `server.address`, `server.port` | diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index b2ecfe23b7..80c24b8cb6 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -356,6 +356,7 @@ def _prepopulate_attributes(scope): full_url = _get_url(scope) query = _get_query(scope) if query: + attributes["url.query"] = query full_url = f"{full_url}?{query}" attributes["url.full"] = full_url diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 3aebff17d5..70324a3641 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -48,6 +48,15 @@ def __call__(self, status, response_headers, exc_info=None): # type: ignore DEFAULT_TRANSACTION_NAME = "generic WSGI request" +ENVIRON_TO_ATTRIBUTE = { + "PATH_INFO": "url.path", + "QUERY_STRING": "url.query", + "REQUEST_METHOD": "http.request.method", + "SERVER_NAME": "server.address", + "SERVER_PORT": "server.port", + "wsgi.url_scheme": "url.scheme", +} + def wsgi_decoding_dance(s, charset="utf-8", errors="replace"): # type: (str, str, str) -> str @@ -120,7 +129,9 @@ def __call__(self, environ, start_response): name=DEFAULT_TRANSACTION_NAME, source=TRANSACTION_SOURCE_ROUTE, origin=self.span_origin, - custom_sampling_context={"wsgi_environ": environ}, + attributes=_prepopulate_attributes( + environ, self.use_x_forwarded_for + ), ) if should_trace else nullcontext() @@ -309,3 +320,29 @@ def event_processor(event, hint): return event return event_processor + + +def _prepopulate_attributes(wsgi_environ, use_x_forwarded_for=False): + """Extract span attributes from the WSGI environment.""" + attributes = {} + + for property, attr in ENVIRON_TO_ATTRIBUTE.items(): + if wsgi_environ.get(property) is not None: + attributes[attr] = wsgi_environ[property] + + if wsgi_environ.get("SERVER_PROTOCOL") is not None: + try: + proto, version = wsgi_environ["SERVER_PROTOCOL"].split("/") + attributes["network.protocol.name"] = proto + attributes["network.protocol.version"] = version + except Exception: + attributes["network.protocol.name"] = wsgi_environ["SERVER_PROTOCOL"] + + try: + url = get_request_url(wsgi_environ, use_x_forwarded_for) + query = wsgi_environ.get("QUERY_STRING") + attributes["url.full"] = f"{url}?{query}" + except Exception: + pass + + return attributes diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index 74f6d8cc49..adfd798c72 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -728,6 +728,7 @@ async def test_asgi_scope_in_traces_sampler(sentry_init, asgi3_app): def dummy_traces_sampler(sampling_context): assert sampling_context["url.path"] == "/test" assert sampling_context["url.scheme"] == "http" + assert sampling_context["url.query"] == "hello=there" assert sampling_context["url.full"] == "/test?hello=there" assert sampling_context["http.request.method"] == "GET" assert sampling_context["network.protocol.version"] == "1.1" diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index 656fc1757f..0652a775d7 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -334,25 +334,21 @@ def app(environ, start_response): start_response("200 OK", []) return ["Go get the ball! Good dog!"] - traces_sampler = mock.Mock(return_value=True) + def traces_sampler(sampling_context): + assert sampling_context["http.request.method"] == "GET" + assert sampling_context["url.path"] == "/dogs/are/great/" + assert sampling_context["url.query"] == "cats=too" + assert sampling_context["url.scheme"] == "http" + assert ( + sampling_context["url.full"] == "http://localhost/dogs/are/great/?cats=too" + ) + return True + sentry_init(send_default_pii=True, traces_sampler=traces_sampler) app = SentryWsgiMiddleware(app) client = Client(app) - client.get("/dogs/are/great/") - - traces_sampler.assert_any_call( - DictionaryContaining( - { - "wsgi_environ": DictionaryContaining( - { - "PATH_INFO": "/dogs/are/great/", - "REQUEST_METHOD": "GET", - }, - ), - } - ) - ) + client.get("/dogs/are/great/?cats=too") def test_session_mode_defaults_to_request_mode_in_wsgi_handler(