Skip to content

Commit d91b241

Browse files
antonpirkersentrivana
authored andcommitted
Cleanup ASGI integration (#2335)
This does not change behaviour/functionality. Some smaller refactoring to make it easier to work on ASGI (and probably Starlette) integration
1 parent 1f4fa1e commit d91b241

File tree

5 files changed

+196
-128
lines changed

5 files changed

+196
-128
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import urllib
2+
3+
from sentry_sdk.hub import _should_send_default_pii
4+
from sentry_sdk.integrations._wsgi_common import _filter_headers
5+
from sentry_sdk._types import TYPE_CHECKING
6+
7+
if TYPE_CHECKING:
8+
from typing import Any
9+
from typing import Dict
10+
from typing import Optional
11+
from typing_extensions import Literal
12+
13+
14+
def _get_headers(asgi_scope):
15+
# type: (Any) -> Dict[str, str]
16+
"""
17+
Extract headers from the ASGI scope, in the format that the Sentry protocol expects.
18+
"""
19+
headers = {} # type: Dict[str, str]
20+
for raw_key, raw_value in asgi_scope["headers"]:
21+
key = raw_key.decode("latin-1")
22+
value = raw_value.decode("latin-1")
23+
if key in headers:
24+
headers[key] = headers[key] + ", " + value
25+
else:
26+
headers[key] = value
27+
28+
return headers
29+
30+
31+
def _get_url(asgi_scope, default_scheme, host):
32+
# type: (Dict[str, Any], Literal["ws", "http"], Optional[str]) -> str
33+
"""
34+
Extract URL from the ASGI scope, without also including the querystring.
35+
"""
36+
scheme = asgi_scope.get("scheme", default_scheme)
37+
38+
server = asgi_scope.get("server", None)
39+
path = asgi_scope.get("root_path", "") + asgi_scope.get("path", "")
40+
41+
if host:
42+
return "%s://%s%s" % (scheme, host, path)
43+
44+
if server is not None:
45+
host, port = server
46+
default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme]
47+
if port != default_port:
48+
return "%s://%s:%s%s" % (scheme, host, port, path)
49+
return "%s://%s%s" % (scheme, host, path)
50+
return path
51+
52+
53+
def _get_query(asgi_scope):
54+
# type: (Any) -> Any
55+
"""
56+
Extract querystring from the ASGI scope, in the format that the Sentry protocol expects.
57+
"""
58+
qs = asgi_scope.get("query_string")
59+
if not qs:
60+
return None
61+
return urllib.parse.unquote(qs.decode("latin-1"))
62+
63+
64+
def _get_ip(asgi_scope):
65+
# type: (Any) -> str
66+
"""
67+
Extract IP Address from the ASGI scope based on request headers with fallback to scope client.
68+
"""
69+
headers = _get_headers(asgi_scope)
70+
try:
71+
return headers["x-forwarded-for"].split(",")[0].strip()
72+
except (KeyError, IndexError):
73+
pass
74+
75+
try:
76+
return headers["x-real-ip"]
77+
except KeyError:
78+
pass
79+
80+
return asgi_scope.get("client")[0]
81+
82+
83+
def _get_request_data(asgi_scope):
84+
# type: (Any) -> Dict[str, Any]
85+
"""
86+
Returns data related to the HTTP request from the ASGI scope.
87+
"""
88+
request_data = {} # type: Dict[str, Any]
89+
ty = asgi_scope["type"]
90+
if ty in ("http", "websocket"):
91+
request_data["method"] = asgi_scope.get("method")
92+
93+
request_data["headers"] = headers = _filter_headers(_get_headers(asgi_scope))
94+
request_data["query_string"] = _get_query(asgi_scope)
95+
96+
request_data["url"] = _get_url(
97+
asgi_scope, "http" if ty == "http" else "ws", headers.get("host")
98+
)
99+
100+
client = asgi_scope.get("client")
101+
if client and _should_send_default_pii():
102+
request_data["env"] = {"REMOTE_ADDR": _get_ip(asgi_scope)}
103+
104+
return request_data

sentry_sdk/integrations/asgi.py

Lines changed: 34 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@
66

77
import asyncio
88
import inspect
9-
import urllib
109
from copy import deepcopy
1110

1211
from sentry_sdk._functools import partial
1312
from sentry_sdk._types import TYPE_CHECKING
1413
from sentry_sdk.api import continue_trace
1514
from sentry_sdk.consts import OP
16-
from sentry_sdk.hub import Hub, _should_send_default_pii
17-
from sentry_sdk.integrations._wsgi_common import _filter_headers
15+
from sentry_sdk.hub import Hub
16+
17+
from sentry_sdk.integrations._asgi_common import (
18+
_get_headers,
19+
_get_request_data,
20+
)
1821
from sentry_sdk.integrations.modules import _get_installed_modules
1922
from sentry_sdk.sessions import auto_session_tracking
2023
from sentry_sdk.tracing import (
@@ -37,8 +40,6 @@
3740
from typing import Optional
3841
from typing import Callable
3942

40-
from typing_extensions import Literal
41-
4243
from sentry_sdk._types import Event, Hint
4344

4445

@@ -169,19 +170,32 @@ async def _run_app(self, scope, receive, send, asgi_version):
169170

170171
if ty in ("http", "websocket"):
171172
transaction = continue_trace(
172-
self._get_headers(scope),
173+
_get_headers(scope),
173174
op="{}.server".format(ty),
174175
)
176+
logger.debug(
177+
"[ASGI] Created transaction (continuing trace): %s",
178+
transaction,
179+
)
175180
else:
176181
transaction = Transaction(op=OP.HTTP_SERVER)
182+
logger.debug(
183+
"[ASGI] Created transaction (new): %s", transaction
184+
)
177185

178186
transaction.name = _DEFAULT_TRANSACTION_NAME
179187
transaction.source = TRANSACTION_SOURCE_ROUTE
180188
transaction.set_tag("asgi.type", ty)
189+
logger.debug(
190+
"[ASGI] Set transaction name and source on transaction: '%s' / '%s'",
191+
transaction.name,
192+
transaction.source,
193+
)
181194

182195
with hub.start_transaction(
183196
transaction, custom_sampling_context={"asgi_scope": scope}
184197
):
198+
logger.debug("[ASGI] Started transaction: %s", transaction)
185199
try:
186200

187201
async def _sentry_wrapped_send(event):
@@ -214,31 +228,15 @@ async def _sentry_wrapped_send(event):
214228

215229
def event_processor(self, event, hint, asgi_scope):
216230
# type: (Event, Hint, Any) -> Optional[Event]
217-
request_info = event.get("request", {})
218-
219-
ty = asgi_scope["type"]
220-
if ty in ("http", "websocket"):
221-
request_info["method"] = asgi_scope.get("method")
222-
request_info["headers"] = headers = _filter_headers(
223-
self._get_headers(asgi_scope)
224-
)
225-
request_info["query_string"] = self._get_query(asgi_scope)
226-
227-
request_info["url"] = self._get_url(
228-
asgi_scope, "http" if ty == "http" else "ws", headers.get("host")
229-
)
230-
231-
client = asgi_scope.get("client")
232-
if client and _should_send_default_pii():
233-
request_info["env"] = {"REMOTE_ADDR": self._get_ip(asgi_scope)}
231+
request_data = event.get("request", {})
232+
request_data.update(_get_request_data(asgi_scope))
233+
event["request"] = deepcopy(request_data)
234234

235235
self._set_transaction_name_and_source(event, self.transaction_style, asgi_scope)
236236

237-
event["request"] = deepcopy(request_info)
238-
239237
return event
240238

241-
# Helper functions for extracting request data.
239+
# Helper functions.
242240
#
243241
# Note: Those functions are not public API. If you want to mutate request
244242
# data to your liking it's recommended to use the `before_send` callback
@@ -275,71 +273,17 @@ def _set_transaction_name_and_source(self, event, transaction_style, asgi_scope)
275273
if not name:
276274
event["transaction"] = _DEFAULT_TRANSACTION_NAME
277275
event["transaction_info"] = {"source": TRANSACTION_SOURCE_ROUTE}
276+
logger.debug(
277+
"[ASGI] Set default transaction name and source on event: '%s' / '%s'",
278+
event["transaction"],
279+
event["transaction_info"]["source"],
280+
)
278281
return
279282

280283
event["transaction"] = name
281284
event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]}
282-
283-
def _get_url(self, scope, default_scheme, host):
284-
# type: (Dict[str, Any], Literal["ws", "http"], Optional[str]) -> str
285-
"""
286-
Extract URL from the ASGI scope, without also including the querystring.
287-
"""
288-
scheme = scope.get("scheme", default_scheme)
289-
290-
server = scope.get("server", None)
291-
path = scope.get("root_path", "") + scope.get("path", "")
292-
293-
if host:
294-
return "%s://%s%s" % (scheme, host, path)
295-
296-
if server is not None:
297-
host, port = server
298-
default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme]
299-
if port != default_port:
300-
return "%s://%s:%s%s" % (scheme, host, port, path)
301-
return "%s://%s%s" % (scheme, host, path)
302-
return path
303-
304-
def _get_query(self, scope):
305-
# type: (Any) -> Any
306-
"""
307-
Extract querystring from the ASGI scope, in the format that the Sentry protocol expects.
308-
"""
309-
qs = scope.get("query_string")
310-
if not qs:
311-
return None
312-
return urllib.parse.unquote(qs.decode("latin-1"))
313-
314-
def _get_ip(self, scope):
315-
# type: (Any) -> str
316-
"""
317-
Extract IP Address from the ASGI scope based on request headers with fallback to scope client.
318-
"""
319-
headers = self._get_headers(scope)
320-
try:
321-
return headers["x-forwarded-for"].split(",")[0].strip()
322-
except (KeyError, IndexError):
323-
pass
324-
325-
try:
326-
return headers["x-real-ip"]
327-
except KeyError:
328-
pass
329-
330-
return scope.get("client")[0]
331-
332-
def _get_headers(self, scope):
333-
# type: (Any) -> Dict[str, str]
334-
"""
335-
Extract headers from the ASGI scope, in the format that the Sentry protocol expects.
336-
"""
337-
headers = {} # type: Dict[str, str]
338-
for raw_key, raw_value in scope["headers"]:
339-
key = raw_key.decode("latin-1")
340-
value = raw_value.decode("latin-1")
341-
if key in headers:
342-
headers[key] = headers[key] + ", " + value
343-
else:
344-
headers[key] = value
345-
return headers
285+
logger.debug(
286+
"[ASGI] Set transaction name and source on event: '%s' / '%s'",
287+
event["transaction"],
288+
event["transaction_info"]["source"],
289+
)

sentry_sdk/integrations/fastapi.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from sentry_sdk.hub import Hub, _should_send_default_pii
66
from sentry_sdk.integrations import DidNotEnable
77
from sentry_sdk.tracing import SOURCE_FOR_STYLE, TRANSACTION_SOURCE_ROUTE
8-
from sentry_sdk.utils import transaction_from_function
8+
from sentry_sdk.utils import transaction_from_function, logger
99

1010
if TYPE_CHECKING:
1111
from typing import Any, Callable, Dict
@@ -60,6 +60,9 @@ def _set_transaction_name_and_source(scope, transaction_style, request):
6060
source = SOURCE_FOR_STYLE[transaction_style]
6161

6262
scope.set_transaction_name(name, source=source)
63+
logger.debug(
64+
"[FastAPI] Set transaction name and source on scope: %s / %s", name, source
65+
)
6366

6467

6568
def patch_get_request_handler():

sentry_sdk/integrations/starlette.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
AnnotatedValue,
2020
capture_internal_exceptions,
2121
event_from_exception,
22+
logger,
2223
parse_version,
2324
transaction_from_function,
2425
)
@@ -648,3 +649,6 @@ def _set_transaction_name_and_source(scope, transaction_style, request):
648649
source = SOURCE_FOR_STYLE[transaction_style]
649650

650651
scope.set_transaction_name(name, source=source)
652+
logger.debug(
653+
"[Starlette] Set transaction name and source on scope: %s / %s", name, source
654+
)

0 commit comments

Comments
 (0)