Skip to content

Commit 571c5cd

Browse files
authored
Unpack WSGI environ into span attrs accessible in traces sampler (#3775)
1 parent e8c1813 commit 571c5cd

File tree

5 files changed

+66
-17
lines changed

5 files changed

+66
-17
lines changed

MIGRATION_GUIDE.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,27 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh
2121
- clickhouse-driver integration: The query is now available under the `db.query.text` span attribute (only if `send_default_pii` is `True`).
2222
- `sentry_sdk.init` now returns `None` instead of a context manager.
2323
- The `sampling_context` argument of `traces_sampler` now additionally contains all span attributes known at span start.
24-
- 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:
24+
- 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:
25+
26+
| Env property | Sampling context key(s) |
27+
| ----------------- | ------------------------------------------------- |
28+
| `PATH_INFO` | `url.path` |
29+
| `QUERY_STRING` | `url.query` |
30+
| `REQUEST_METHOD` | `http.request.method` |
31+
| `SERVER_NAME` | `server.address` |
32+
| `SERVER_PORT` | `server.port` |
33+
| `SERVER_PROTOCOL` | `server.protocol.name`, `server.protocol.version` |
34+
| `wsgi.url_scheme` | `url.scheme` |
35+
| full URL | `url.full` |
36+
37+
- 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:
2538

2639
| Scope property | Sampling context key(s) |
2740
| -------------- | ------------------------------- |
2841
| `type` | `network.protocol.name` |
2942
| `scheme` | `url.scheme` |
3043
| `path` | `url.path` |
44+
| `query` | `url.query` |
3145
| `http_version` | `network.protocol.version` |
3246
| `method` | `http.request.method` |
3347
| `server` | `server.address`, `server.port` |

sentry_sdk/integrations/asgi.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ def _prepopulate_attributes(scope):
356356
full_url = _get_url(scope)
357357
query = _get_query(scope)
358358
if query:
359+
attributes["url.query"] = query
359360
full_url = f"{full_url}?{query}"
360361

361362
attributes["url.full"] = full_url

sentry_sdk/integrations/wsgi.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ def __call__(self, status, response_headers, exc_info=None): # type: ignore
4848

4949
DEFAULT_TRANSACTION_NAME = "generic WSGI request"
5050

51+
ENVIRON_TO_ATTRIBUTE = {
52+
"PATH_INFO": "url.path",
53+
"QUERY_STRING": "url.query",
54+
"REQUEST_METHOD": "http.request.method",
55+
"SERVER_NAME": "server.address",
56+
"SERVER_PORT": "server.port",
57+
"wsgi.url_scheme": "url.scheme",
58+
}
59+
5160

5261
def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
5362
# type: (str, str, str) -> str
@@ -120,7 +129,9 @@ def __call__(self, environ, start_response):
120129
name=DEFAULT_TRANSACTION_NAME,
121130
source=TRANSACTION_SOURCE_ROUTE,
122131
origin=self.span_origin,
123-
custom_sampling_context={"wsgi_environ": environ},
132+
attributes=_prepopulate_attributes(
133+
environ, self.use_x_forwarded_for
134+
),
124135
)
125136
if should_trace
126137
else nullcontext()
@@ -309,3 +320,29 @@ def event_processor(event, hint):
309320
return event
310321

311322
return event_processor
323+
324+
325+
def _prepopulate_attributes(wsgi_environ, use_x_forwarded_for=False):
326+
"""Extract span attributes from the WSGI environment."""
327+
attributes = {}
328+
329+
for property, attr in ENVIRON_TO_ATTRIBUTE.items():
330+
if wsgi_environ.get(property) is not None:
331+
attributes[attr] = wsgi_environ[property]
332+
333+
if wsgi_environ.get("SERVER_PROTOCOL") is not None:
334+
try:
335+
proto, version = wsgi_environ["SERVER_PROTOCOL"].split("/")
336+
attributes["network.protocol.name"] = proto
337+
attributes["network.protocol.version"] = version
338+
except Exception:
339+
attributes["network.protocol.name"] = wsgi_environ["SERVER_PROTOCOL"]
340+
341+
try:
342+
url = get_request_url(wsgi_environ, use_x_forwarded_for)
343+
query = wsgi_environ.get("QUERY_STRING")
344+
attributes["url.full"] = f"{url}?{query}"
345+
except Exception:
346+
pass
347+
348+
return attributes

tests/integrations/asgi/test_asgi.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,7 @@ async def test_asgi_scope_in_traces_sampler(sentry_init, asgi3_app):
728728
def dummy_traces_sampler(sampling_context):
729729
assert sampling_context["url.path"] == "/test"
730730
assert sampling_context["url.scheme"] == "http"
731+
assert sampling_context["url.query"] == "hello=there"
731732
assert sampling_context["url.full"] == "/test?hello=there"
732733
assert sampling_context["http.request.method"] == "GET"
733734
assert sampling_context["network.protocol.version"] == "1.1"

tests/integrations/wsgi/test_wsgi.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -334,25 +334,21 @@ def app(environ, start_response):
334334
start_response("200 OK", [])
335335
return ["Go get the ball! Good dog!"]
336336

337-
traces_sampler = mock.Mock(return_value=True)
337+
def traces_sampler(sampling_context):
338+
assert sampling_context["http.request.method"] == "GET"
339+
assert sampling_context["url.path"] == "/dogs/are/great/"
340+
assert sampling_context["url.query"] == "cats=too"
341+
assert sampling_context["url.scheme"] == "http"
342+
assert (
343+
sampling_context["url.full"] == "http://localhost/dogs/are/great/?cats=too"
344+
)
345+
return True
346+
338347
sentry_init(send_default_pii=True, traces_sampler=traces_sampler)
339348
app = SentryWsgiMiddleware(app)
340349
client = Client(app)
341350

342-
client.get("/dogs/are/great/")
343-
344-
traces_sampler.assert_any_call(
345-
DictionaryContaining(
346-
{
347-
"wsgi_environ": DictionaryContaining(
348-
{
349-
"PATH_INFO": "/dogs/are/great/",
350-
"REQUEST_METHOD": "GET",
351-
},
352-
),
353-
}
354-
)
355-
)
351+
client.get("/dogs/are/great/?cats=too")
356352

357353

358354
def test_session_mode_defaults_to_request_mode_in_wsgi_handler(

0 commit comments

Comments
 (0)