Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set the url as a transaction name ... #2264

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions sentry_sdk/integrations/_asgi_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import urllib

from sentry_sdk.hub import _should_send_default_pii
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk._types import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any
from typing import Dict
from typing import Optional
from typing_extensions import Literal


def _get_headers(asgi_scope):
# type: (Any) -> Dict[str, str]
"""
Extract headers from the ASGI scope, in the format that the Sentry protocol expects.
"""
headers = {} # type: Dict[str, str]
for raw_key, raw_value in asgi_scope["headers"]:
key = raw_key.decode("latin-1")
value = raw_value.decode("latin-1")
if key in headers:
headers[key] = headers[key] + ", " + value
else:
headers[key] = value

return headers


def _get_url(asgi_scope, default_scheme, host):
# type: (Dict[str, Any], Literal["ws", "http"], Optional[str]) -> str
"""
Extract URL from the ASGI scope, without also including the querystring.
"""
scheme = asgi_scope.get("scheme", default_scheme)

server = asgi_scope.get("server", None)
path = asgi_scope.get("root_path", "") + asgi_scope.get("path", "")

if host:
return "%s://%s%s" % (scheme, host, path)

if server is not None:
host, port = server
default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme]
if port != default_port:
return "%s://%s:%s%s" % (scheme, host, port, path)
return "%s://%s%s" % (scheme, host, path)
return path


def _get_query(asgi_scope):
# type: (Any) -> Any
"""
Extract querystring from the ASGI scope, in the format that the Sentry protocol expects.
"""
qs = asgi_scope.get("query_string")
if not qs:
return None
return urllib.parse.unquote(qs.decode("latin-1"))


def _get_ip(asgi_scope):
# type: (Any) -> str
"""
Extract IP Address from the ASGI scope based on request headers with fallback to scope client.
"""
headers = _get_headers(asgi_scope)
try:
return headers["x-forwarded-for"].split(",")[0].strip()
except (KeyError, IndexError):
pass

try:
return headers["x-real-ip"]
except KeyError:
pass

return asgi_scope.get("client")[0]


def _get_request_data(asgi_scope):
# type: (Any) -> Dict[str, Any]
"""
Returns data related to the HTTP request from the ASGI scope.
"""
request_data = {} # type: Dict[str, Any]
ty = asgi_scope["type"]
if ty in ("http", "websocket"):
request_data["method"] = asgi_scope.get("method")

request_data["headers"] = headers = _filter_headers(_get_headers(asgi_scope))
request_data["query_string"] = _get_query(asgi_scope)

request_data["url"] = _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():
request_data["env"] = {"REMOTE_ADDR": _get_ip(asgi_scope)}

return request_data
185 changes: 67 additions & 118 deletions sentry_sdk/integrations/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,25 @@

import asyncio
import inspect
import urllib
import logging
from copy import deepcopy

from sentry_sdk._functools import partial
from sentry_sdk._types import TYPE_CHECKING
from sentry_sdk.api import continue_trace
from sentry_sdk.consts import OP
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk.hub import Hub
from sentry_sdk.integrations._asgi_common import (
_get_headers,
_get_request_data,
_get_url,
)
from sentry_sdk.integrations.modules import _get_installed_modules
from sentry_sdk.sessions import auto_session_tracking
from sentry_sdk.tracing import (
SOURCE_FOR_STYLE,
TRANSACTION_SOURCE_ROUTE,
TRANSACTION_SOURCE_URL,
)
from sentry_sdk.utils import (
ContextVar,
Expand All @@ -37,8 +42,6 @@
from typing import Optional
from typing import Callable

from typing_extensions import Literal

from sentry_sdk._types import Event, Hint


Expand Down Expand Up @@ -168,17 +171,35 @@ async def _run_app(self, scope, receive, send, asgi_version):
ty = scope["type"]

if ty in ("http", "websocket"):
(
transaction_name,
transaction_source,
) = self._get_transaction_name_and_source(
self.transaction_style, scope
)
transaction = continue_trace(
self._get_headers(scope),
_get_headers(scope),
op="{}.server".format(ty),
name=transaction_name,
source=transaction_source,
)
logging.warning(
"[ASGI] Created Transaction %s, %s, %s",
transaction,
transaction.sampled,
transaction_name,
)
else:
transaction = Transaction(op=OP.HTTP_SERVER)

transaction.name = _DEFAULT_TRANSACTION_NAME
transaction.source = TRANSACTION_SOURCE_ROUTE
transaction.set_tag("asgi.type", ty)

logging.warning(
"[ASGI] Starting Transaction %s, %s, %s",
transaction,
transaction.sampled,
transaction.name,
)
with hub.start_transaction(
transaction, custom_sampling_context={"asgi_scope": scope}
):
Expand Down Expand Up @@ -214,46 +235,22 @@ async def _sentry_wrapped_send(event):

def event_processor(self, event, hint, asgi_scope):
# type: (Event, Hint, Any) -> Optional[Event]
request_info = event.get("request", {})

ty = asgi_scope["type"]
if ty in ("http", "websocket"):
request_info["method"] = asgi_scope.get("method")
request_info["headers"] = headers = _filter_headers(
self._get_headers(asgi_scope)
)
request_info["query_string"] = self._get_query(asgi_scope)

request_info["url"] = self._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():
request_info["env"] = {"REMOTE_ADDR": self._get_ip(asgi_scope)}

self._set_transaction_name_and_source(event, self.transaction_style, asgi_scope)
request_data = event.get("request", {})
request_data.update(_get_request_data(asgi_scope))
event["request"] = deepcopy(request_data)

event["request"] = deepcopy(request_info)
transaction_name, transaction_source = self._get_transaction_name_and_source(
self.transaction_style, asgi_scope
)
event["transaction"] = transaction_name
event["transaction_info"] = {"source": transaction_source}

return event

# Helper functions for extracting request data.
#
# Note: Those functions are not public API. If you want to mutate request
# data to your liking it's recommended to use the `before_send` callback
# for that.

def _set_transaction_name_and_source(self, event, transaction_style, asgi_scope):
def _get_transaction_name_and_source(self, transaction_style, asgi_scope):
# type: (Event, str, Any) -> None
transaction_name_already_set = (
event.get("transaction", _DEFAULT_TRANSACTION_NAME)
!= _DEFAULT_TRANSACTION_NAME
)
if transaction_name_already_set:
return

name = ""
name = None
source = None

if transaction_style == "endpoint":
endpoint = asgi_scope.get("endpoint")
Expand All @@ -262,84 +259,36 @@ def _set_transaction_name_and_source(self, event, transaction_style, asgi_scope)
# an endpoint, overwrite our generic transaction name.
if endpoint:
name = transaction_from_function(endpoint) or ""
source = SOURCE_FOR_STYLE[transaction_style]
else:
ty = asgi_scope.get("type")
if ty in ("http", "websocket"):
name = _get_url(
asgi_scope, "http" if ty == "http" else "ws", host=None
)
source = TRANSACTION_SOURCE_URL
else:
name = _DEFAULT_TRANSACTION_NAME
source = TRANSACTION_SOURCE_ROUTE

elif transaction_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

if not name:
event["transaction"] = _DEFAULT_TRANSACTION_NAME
event["transaction_info"] = {"source": TRANSACTION_SOURCE_ROUTE}
return

event["transaction"] = name
event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]}

def _get_url(self, scope, default_scheme, host):
# type: (Dict[str, Any], Literal["ws", "http"], Optional[str]) -> str
"""
Extract URL from the ASGI scope, without also including the querystring.
"""
scheme = scope.get("scheme", default_scheme)

server = scope.get("server", None)
path = scope.get("root_path", "") + scope.get("path", "")

if host:
return "%s://%s%s" % (scheme, host, path)

if server is not None:
host, port = server
default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme]
if port != default_port:
return "%s://%s:%s%s" % (scheme, host, port, path)
return "%s://%s%s" % (scheme, host, path)
return path

def _get_query(self, scope):
# type: (Any) -> Any
"""
Extract querystring from the ASGI scope, in the format that the Sentry protocol expects.
"""
qs = scope.get("query_string")
if not qs:
return None
return urllib.parse.unquote(qs.decode("latin-1"))

def _get_ip(self, scope):
# type: (Any) -> str
"""
Extract IP Address from the ASGI scope based on request headers with fallback to scope client.
"""
headers = self._get_headers(scope)
try:
return headers["x-forwarded-for"].split(",")[0].strip()
except (KeyError, IndexError):
pass

try:
return headers["x-real-ip"]
except KeyError:
pass
name = route
source = SOURCE_FOR_STYLE[transaction_style]
else:
ty = asgi_scope.get("type")
if ty in ("http", "websocket"):
name = _get_url(
asgi_scope, "http" if ty == "http" else "ws", host=None
)
source = TRANSACTION_SOURCE_URL
else:
name = _DEFAULT_TRANSACTION_NAME
source = TRANSACTION_SOURCE_ROUTE

return scope.get("client")[0]
if name is None:
name = _DEFAULT_TRANSACTION_NAME
source = TRANSACTION_SOURCE_ROUTE

def _get_headers(self, scope):
# type: (Any) -> Dict[str, str]
"""
Extract headers from the ASGI scope, in the format that the Sentry protocol expects.
"""
headers = {} # type: Dict[str, str]
for raw_key, raw_value in scope["headers"]:
key = raw_key.decode("latin-1")
value = raw_value.decode("latin-1")
if key in headers:
headers[key] = headers[key] + ", " + value
else:
headers[key] = value
return headers
return name, source
10 changes: 6 additions & 4 deletions sentry_sdk/integrations/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def setup_once():
patch_get_request_handler()


def _set_transaction_name_and_source(scope, transaction_style, request):
def _get_transaction_name_and_source(scope, transaction_style, request):
# type: (Scope, str, Any) -> None
name = ""

Expand All @@ -59,7 +59,7 @@ def _set_transaction_name_and_source(scope, transaction_style, request):
else:
source = SOURCE_FOR_STYLE[transaction_style]

scope.set_transaction_name(name, source=source)
return name, source


def patch_get_request_handler():
Expand Down Expand Up @@ -97,10 +97,12 @@ async def _sentry_app(*args, **kwargs):

with hub.configure_scope() as sentry_scope:
request = args[0]

_set_transaction_name_and_source(
transaction_name, transaction_source = _get_transaction_name_and_source(
sentry_scope, integration.transaction_style, request
)
sentry_scope.set_transaction_name(
transaction_name, source=transaction_source
)

extractor = StarletteRequestExtractor(request)
info = await extractor.extract_request_info()
Expand Down
Loading