about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/aiohttp.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/sentry_sdk/integrations/aiohttp.py')
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/aiohttp.py357
1 files changed, 357 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/aiohttp.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/aiohttp.py
new file mode 100644
index 00000000..ad3202bf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/aiohttp.py
@@ -0,0 +1,357 @@
+import sys
+import weakref
+from functools import wraps
+
+import sentry_sdk
+from sentry_sdk.api import continue_trace
+from sentry_sdk.consts import OP, SPANSTATUS, SPANDATA
+from sentry_sdk.integrations import (
+    _DEFAULT_FAILED_REQUEST_STATUS_CODES,
+    _check_minimum_version,
+    Integration,
+    DidNotEnable,
+)
+from sentry_sdk.integrations.logging import ignore_logger
+from sentry_sdk.sessions import track_session
+from sentry_sdk.integrations._wsgi_common import (
+    _filter_headers,
+    request_body_within_bounds,
+)
+from sentry_sdk.tracing import (
+    BAGGAGE_HEADER_NAME,
+    SOURCE_FOR_STYLE,
+    TransactionSource,
+)
+from sentry_sdk.tracing_utils import should_propagate_trace
+from sentry_sdk.utils import (
+    capture_internal_exceptions,
+    ensure_integration_enabled,
+    event_from_exception,
+    logger,
+    parse_url,
+    parse_version,
+    reraise,
+    transaction_from_function,
+    HAS_REAL_CONTEXTVARS,
+    CONTEXTVARS_ERROR_MESSAGE,
+    SENSITIVE_DATA_SUBSTITUTE,
+    AnnotatedValue,
+)
+
+try:
+    import asyncio
+
+    from aiohttp import __version__ as AIOHTTP_VERSION
+    from aiohttp import ClientSession, TraceConfig
+    from aiohttp.web import Application, HTTPException, UrlDispatcher
+except ImportError:
+    raise DidNotEnable("AIOHTTP not installed")
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from aiohttp.web_request import Request
+    from aiohttp.web_urldispatcher import UrlMappingMatchInfo
+    from aiohttp import TraceRequestStartParams, TraceRequestEndParams
+
+    from collections.abc import Set
+    from types import SimpleNamespace
+    from typing import Any
+    from typing import Optional
+    from typing import Tuple
+    from typing import Union
+
+    from sentry_sdk.utils import ExcInfo
+    from sentry_sdk._types import Event, EventProcessor
+
+
+TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern")
+
+
+class AioHttpIntegration(Integration):
+    identifier = "aiohttp"
+    origin = f"auto.http.{identifier}"
+
+    def __init__(
+        self,
+        transaction_style="handler_name",  # type: str
+        *,
+        failed_request_status_codes=_DEFAULT_FAILED_REQUEST_STATUS_CODES,  # type: Set[int]
+    ):
+        # type: (...) -> None
+        if transaction_style not in TRANSACTION_STYLE_VALUES:
+            raise ValueError(
+                "Invalid value for transaction_style: %s (must be in %s)"
+                % (transaction_style, TRANSACTION_STYLE_VALUES)
+            )
+        self.transaction_style = transaction_style
+        self._failed_request_status_codes = failed_request_status_codes
+
+    @staticmethod
+    def setup_once():
+        # type: () -> None
+
+        version = parse_version(AIOHTTP_VERSION)
+        _check_minimum_version(AioHttpIntegration, version)
+
+        if not HAS_REAL_CONTEXTVARS:
+            # We better have contextvars or we're going to leak state between
+            # requests.
+            raise DidNotEnable(
+                "The aiohttp integration for Sentry requires Python 3.7+ "
+                " or aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE
+            )
+
+        ignore_logger("aiohttp.server")
+
+        old_handle = Application._handle
+
+        async def sentry_app_handle(self, request, *args, **kwargs):
+            # type: (Any, Request, *Any, **Any) -> Any
+            integration = sentry_sdk.get_client().get_integration(AioHttpIntegration)
+            if integration is None:
+                return await old_handle(self, request, *args, **kwargs)
+
+            weak_request = weakref.ref(request)
+
+            with sentry_sdk.isolation_scope() as scope:
+                with track_session(scope, session_mode="request"):
+                    # Scope data will not leak between requests because aiohttp
+                    # create a task to wrap each request.
+                    scope.generate_propagation_context()
+                    scope.clear_breadcrumbs()
+                    scope.add_event_processor(_make_request_processor(weak_request))
+
+                    headers = dict(request.headers)
+                    transaction = continue_trace(
+                        headers,
+                        op=OP.HTTP_SERVER,
+                        # If this transaction name makes it to the UI, AIOHTTP's
+                        # URL resolver did not find a route or died trying.
+                        name="generic AIOHTTP request",
+                        source=TransactionSource.ROUTE,
+                        origin=AioHttpIntegration.origin,
+                    )
+                    with sentry_sdk.start_transaction(
+                        transaction,
+                        custom_sampling_context={"aiohttp_request": request},
+                    ):
+                        try:
+                            response = await old_handle(self, request)
+                        except HTTPException as e:
+                            transaction.set_http_status(e.status_code)
+
+                            if (
+                                e.status_code
+                                in integration._failed_request_status_codes
+                            ):
+                                _capture_exception()
+
+                            raise
+                        except (asyncio.CancelledError, ConnectionResetError):
+                            transaction.set_status(SPANSTATUS.CANCELLED)
+                            raise
+                        except Exception:
+                            # This will probably map to a 500 but seems like we
+                            # have no way to tell. Do not set span status.
+                            reraise(*_capture_exception())
+
+                        try:
+                            # A valid response handler will return a valid response with a status. But, if the handler
+                            # returns an invalid response (e.g. None), the line below will raise an AttributeError.
+                            # Even though this is likely invalid, we need to handle this case to ensure we don't break
+                            # the application.
+                            response_status = response.status
+                        except AttributeError:
+                            pass
+                        else:
+                            transaction.set_http_status(response_status)
+
+                        return response
+
+        Application._handle = sentry_app_handle
+
+        old_urldispatcher_resolve = UrlDispatcher.resolve
+
+        @wraps(old_urldispatcher_resolve)
+        async def sentry_urldispatcher_resolve(self, request):
+            # type: (UrlDispatcher, Request) -> UrlMappingMatchInfo
+            rv = await old_urldispatcher_resolve(self, request)
+
+            integration = sentry_sdk.get_client().get_integration(AioHttpIntegration)
+            if integration is None:
+                return rv
+
+            name = None
+
+            try:
+                if integration.transaction_style == "handler_name":
+                    name = transaction_from_function(rv.handler)
+                elif integration.transaction_style == "method_and_path_pattern":
+                    route_info = rv.get_info()
+                    pattern = route_info.get("path") or route_info.get("formatter")
+                    name = "{} {}".format(request.method, pattern)
+            except Exception:
+                pass
+
+            if name is not None:
+                sentry_sdk.get_current_scope().set_transaction_name(
+                    name,
+                    source=SOURCE_FOR_STYLE[integration.transaction_style],
+                )
+
+            return rv
+
+        UrlDispatcher.resolve = sentry_urldispatcher_resolve
+
+        old_client_session_init = ClientSession.__init__
+
+        @ensure_integration_enabled(AioHttpIntegration, old_client_session_init)
+        def init(*args, **kwargs):
+            # type: (Any, Any) -> None
+            client_trace_configs = list(kwargs.get("trace_configs") or ())
+            trace_config = create_trace_config()
+            client_trace_configs.append(trace_config)
+
+            kwargs["trace_configs"] = client_trace_configs
+            return old_client_session_init(*args, **kwargs)
+
+        ClientSession.__init__ = init
+
+
+def create_trace_config():
+    # type: () -> TraceConfig
+
+    async def on_request_start(session, trace_config_ctx, params):
+        # type: (ClientSession, SimpleNamespace, TraceRequestStartParams) -> None
+        if sentry_sdk.get_client().get_integration(AioHttpIntegration) is None:
+            return
+
+        method = params.method.upper()
+
+        parsed_url = None
+        with capture_internal_exceptions():
+            parsed_url = parse_url(str(params.url), sanitize=False)
+
+        span = sentry_sdk.start_span(
+            op=OP.HTTP_CLIENT,
+            name="%s %s"
+            % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE),
+            origin=AioHttpIntegration.origin,
+        )
+        span.set_data(SPANDATA.HTTP_METHOD, method)
+        if parsed_url is not None:
+            span.set_data("url", parsed_url.url)
+            span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
+            span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
+
+        client = sentry_sdk.get_client()
+
+        if should_propagate_trace(client, str(params.url)):
+            for (
+                key,
+                value,
+            ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(
+                span=span
+            ):
+                logger.debug(
+                    "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format(
+                        key=key, value=value, url=params.url
+                    )
+                )
+                if key == BAGGAGE_HEADER_NAME and params.headers.get(
+                    BAGGAGE_HEADER_NAME
+                ):
+                    # do not overwrite any existing baggage, just append to it
+                    params.headers[key] += "," + value
+                else:
+                    params.headers[key] = value
+
+        trace_config_ctx.span = span
+
+    async def on_request_end(session, trace_config_ctx, params):
+        # type: (ClientSession, SimpleNamespace, TraceRequestEndParams) -> None
+        if trace_config_ctx.span is None:
+            return
+
+        span = trace_config_ctx.span
+        span.set_http_status(int(params.response.status))
+        span.set_data("reason", params.response.reason)
+        span.finish()
+
+    trace_config = TraceConfig()
+
+    trace_config.on_request_start.append(on_request_start)
+    trace_config.on_request_end.append(on_request_end)
+
+    return trace_config
+
+
+def _make_request_processor(weak_request):
+    # type: (weakref.ReferenceType[Request]) -> EventProcessor
+    def aiohttp_processor(
+        event,  # type: Event
+        hint,  # type: dict[str, Tuple[type, BaseException, Any]]
+    ):
+        # type: (...) -> Event
+        request = weak_request()
+        if request is None:
+            return event
+
+        with capture_internal_exceptions():
+            request_info = event.setdefault("request", {})
+
+            request_info["url"] = "%s://%s%s" % (
+                request.scheme,
+                request.host,
+                request.path,
+            )
+
+            request_info["query_string"] = request.query_string
+            request_info["method"] = request.method
+            request_info["env"] = {"REMOTE_ADDR": request.remote}
+            request_info["headers"] = _filter_headers(dict(request.headers))
+
+            # Just attach raw data here if it is within bounds, if available.
+            # Unfortunately there's no way to get structured data from aiohttp
+            # without awaiting on some coroutine.
+            request_info["data"] = get_aiohttp_request_data(request)
+
+        return event
+
+    return aiohttp_processor
+
+
+def _capture_exception():
+    # type: () -> ExcInfo
+    exc_info = sys.exc_info()
+    event, hint = event_from_exception(
+        exc_info,
+        client_options=sentry_sdk.get_client().options,
+        mechanism={"type": "aiohttp", "handled": False},
+    )
+    sentry_sdk.capture_event(event, hint=hint)
+    return exc_info
+
+
+BODY_NOT_READ_MESSAGE = "[Can't show request body due to implementation details.]"
+
+
+def get_aiohttp_request_data(request):
+    # type: (Request) -> Union[Optional[str], AnnotatedValue]
+    bytes_body = request._read_bytes
+
+    if bytes_body is not None:
+        # we have body to show
+        if not request_body_within_bounds(sentry_sdk.get_client(), len(bytes_body)):
+            return AnnotatedValue.removed_because_over_size_limit()
+
+        encoding = request.charset or "utf-8"
+        return bytes_body.decode(encoding, "replace")
+
+    if request.can_read_body:
+        # body exists but we can't show it
+        return BODY_NOT_READ_MESSAGE
+
+    # request has no body
+    return None