about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/strawberry.py
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/sentry_sdk/integrations/strawberry.py
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-4a52a71956a8d46fcb7294ac71734504bb09bcc2.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/sentry_sdk/integrations/strawberry.py')
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/strawberry.py393
1 files changed, 393 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/strawberry.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/strawberry.py
new file mode 100644
index 00000000..ae7d2730
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/strawberry.py
@@ -0,0 +1,393 @@
+import functools
+import hashlib
+from inspect import isawaitable
+
+import sentry_sdk
+from sentry_sdk.consts import OP
+from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable
+from sentry_sdk.integrations.logging import ignore_logger
+from sentry_sdk.scope import should_send_default_pii
+from sentry_sdk.tracing import TransactionSource
+from sentry_sdk.utils import (
+    capture_internal_exceptions,
+    ensure_integration_enabled,
+    event_from_exception,
+    logger,
+    package_version,
+    _get_installed_modules,
+)
+
+try:
+    from functools import cached_property
+except ImportError:
+    # The strawberry integration requires Python 3.8+. functools.cached_property
+    # was added in 3.8, so this check is technically not needed, but since this
+    # is an auto-enabling integration, we might get to executing this import in
+    # lower Python versions, so we need to deal with it.
+    raise DidNotEnable("strawberry-graphql integration requires Python 3.8 or newer")
+
+try:
+    from strawberry import Schema
+    from strawberry.extensions import SchemaExtension
+    from strawberry.extensions.tracing.utils import (
+        should_skip_tracing as strawberry_should_skip_tracing,
+    )
+    from strawberry.http import async_base_view, sync_base_view
+except ImportError:
+    raise DidNotEnable("strawberry-graphql is not installed")
+
+try:
+    from strawberry.extensions.tracing import (
+        SentryTracingExtension as StrawberrySentryAsyncExtension,
+        SentryTracingExtensionSync as StrawberrySentrySyncExtension,
+    )
+except ImportError:
+    StrawberrySentryAsyncExtension = None
+    StrawberrySentrySyncExtension = None
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Any, Callable, Generator, List, Optional
+    from graphql import GraphQLError, GraphQLResolveInfo
+    from strawberry.http import GraphQLHTTPResponse
+    from strawberry.types import ExecutionContext
+    from sentry_sdk._types import Event, EventProcessor
+
+
+ignore_logger("strawberry.execution")
+
+
+class StrawberryIntegration(Integration):
+    identifier = "strawberry"
+    origin = f"auto.graphql.{identifier}"
+
+    def __init__(self, async_execution=None):
+        # type: (Optional[bool]) -> None
+        if async_execution not in (None, False, True):
+            raise ValueError(
+                'Invalid value for async_execution: "{}" (must be bool)'.format(
+                    async_execution
+                )
+            )
+        self.async_execution = async_execution
+
+    @staticmethod
+    def setup_once():
+        # type: () -> None
+        version = package_version("strawberry-graphql")
+        _check_minimum_version(StrawberryIntegration, version, "strawberry-graphql")
+
+        _patch_schema_init()
+        _patch_views()
+
+
+def _patch_schema_init():
+    # type: () -> None
+    old_schema_init = Schema.__init__
+
+    @functools.wraps(old_schema_init)
+    def _sentry_patched_schema_init(self, *args, **kwargs):
+        # type: (Schema, Any, Any) -> None
+        integration = sentry_sdk.get_client().get_integration(StrawberryIntegration)
+        if integration is None:
+            return old_schema_init(self, *args, **kwargs)
+
+        extensions = kwargs.get("extensions") or []
+
+        if integration.async_execution is not None:
+            should_use_async_extension = integration.async_execution
+        else:
+            # try to figure it out ourselves
+            should_use_async_extension = _guess_if_using_async(extensions)
+
+            logger.info(
+                "Assuming strawberry is running %s. If not, initialize it as StrawberryIntegration(async_execution=%s).",
+                "async" if should_use_async_extension else "sync",
+                "False" if should_use_async_extension else "True",
+            )
+
+        # remove the built in strawberry sentry extension, if present
+        extensions = [
+            extension
+            for extension in extensions
+            if extension
+            not in (StrawberrySentryAsyncExtension, StrawberrySentrySyncExtension)
+        ]
+
+        # add our extension
+        extensions.append(
+            SentryAsyncExtension if should_use_async_extension else SentrySyncExtension
+        )
+
+        kwargs["extensions"] = extensions
+
+        return old_schema_init(self, *args, **kwargs)
+
+    Schema.__init__ = _sentry_patched_schema_init  # type: ignore[method-assign]
+
+
+class SentryAsyncExtension(SchemaExtension):
+    def __init__(
+        self,
+        *,
+        execution_context=None,
+    ):
+        # type: (Any, Optional[ExecutionContext]) -> None
+        if execution_context:
+            self.execution_context = execution_context
+
+    @cached_property
+    def _resource_name(self):
+        # type: () -> str
+        query_hash = self.hash_query(self.execution_context.query)  # type: ignore
+
+        if self.execution_context.operation_name:
+            return "{}:{}".format(self.execution_context.operation_name, query_hash)
+
+        return query_hash
+
+    def hash_query(self, query):
+        # type: (str) -> str
+        return hashlib.md5(query.encode("utf-8")).hexdigest()
+
+    def on_operation(self):
+        # type: () -> Generator[None, None, None]
+        self._operation_name = self.execution_context.operation_name
+
+        operation_type = "query"
+        op = OP.GRAPHQL_QUERY
+
+        if self.execution_context.query is None:
+            self.execution_context.query = ""
+
+        if self.execution_context.query.strip().startswith("mutation"):
+            operation_type = "mutation"
+            op = OP.GRAPHQL_MUTATION
+        elif self.execution_context.query.strip().startswith("subscription"):
+            operation_type = "subscription"
+            op = OP.GRAPHQL_SUBSCRIPTION
+
+        description = operation_type
+        if self._operation_name:
+            description += " {}".format(self._operation_name)
+
+        sentry_sdk.add_breadcrumb(
+            category="graphql.operation",
+            data={
+                "operation_name": self._operation_name,
+                "operation_type": operation_type,
+            },
+        )
+
+        scope = sentry_sdk.get_isolation_scope()
+        event_processor = _make_request_event_processor(self.execution_context)
+        scope.add_event_processor(event_processor)
+
+        span = sentry_sdk.get_current_span()
+        if span:
+            self.graphql_span = span.start_child(
+                op=op,
+                name=description,
+                origin=StrawberryIntegration.origin,
+            )
+        else:
+            self.graphql_span = sentry_sdk.start_span(
+                op=op,
+                name=description,
+                origin=StrawberryIntegration.origin,
+            )
+
+        self.graphql_span.set_data("graphql.operation.type", operation_type)
+        self.graphql_span.set_data("graphql.operation.name", self._operation_name)
+        self.graphql_span.set_data("graphql.document", self.execution_context.query)
+        self.graphql_span.set_data("graphql.resource_name", self._resource_name)
+
+        yield
+
+        transaction = self.graphql_span.containing_transaction
+        if transaction and self.execution_context.operation_name:
+            transaction.name = self.execution_context.operation_name
+            transaction.source = TransactionSource.COMPONENT
+            transaction.op = op
+
+        self.graphql_span.finish()
+
+    def on_validate(self):
+        # type: () -> Generator[None, None, None]
+        self.validation_span = self.graphql_span.start_child(
+            op=OP.GRAPHQL_VALIDATE,
+            name="validation",
+            origin=StrawberryIntegration.origin,
+        )
+
+        yield
+
+        self.validation_span.finish()
+
+    def on_parse(self):
+        # type: () -> Generator[None, None, None]
+        self.parsing_span = self.graphql_span.start_child(
+            op=OP.GRAPHQL_PARSE,
+            name="parsing",
+            origin=StrawberryIntegration.origin,
+        )
+
+        yield
+
+        self.parsing_span.finish()
+
+    def should_skip_tracing(self, _next, info):
+        # type: (Callable[[Any, GraphQLResolveInfo, Any, Any], Any], GraphQLResolveInfo) -> bool
+        return strawberry_should_skip_tracing(_next, info)
+
+    async def _resolve(self, _next, root, info, *args, **kwargs):
+        # type: (Callable[[Any, GraphQLResolveInfo, Any, Any], Any], Any, GraphQLResolveInfo, str, Any) -> Any
+        result = _next(root, info, *args, **kwargs)
+
+        if isawaitable(result):
+            result = await result
+
+        return result
+
+    async def resolve(self, _next, root, info, *args, **kwargs):
+        # type: (Callable[[Any, GraphQLResolveInfo, Any, Any], Any], Any, GraphQLResolveInfo, str, Any) -> Any
+        if self.should_skip_tracing(_next, info):
+            return await self._resolve(_next, root, info, *args, **kwargs)
+
+        field_path = "{}.{}".format(info.parent_type, info.field_name)
+
+        with self.graphql_span.start_child(
+            op=OP.GRAPHQL_RESOLVE,
+            name="resolving {}".format(field_path),
+            origin=StrawberryIntegration.origin,
+        ) as span:
+            span.set_data("graphql.field_name", info.field_name)
+            span.set_data("graphql.parent_type", info.parent_type.name)
+            span.set_data("graphql.field_path", field_path)
+            span.set_data("graphql.path", ".".join(map(str, info.path.as_list())))
+
+            return await self._resolve(_next, root, info, *args, **kwargs)
+
+
+class SentrySyncExtension(SentryAsyncExtension):
+    def resolve(self, _next, root, info, *args, **kwargs):
+        # type: (Callable[[Any, Any, Any, Any], Any], Any, GraphQLResolveInfo, str, Any) -> Any
+        if self.should_skip_tracing(_next, info):
+            return _next(root, info, *args, **kwargs)
+
+        field_path = "{}.{}".format(info.parent_type, info.field_name)
+
+        with self.graphql_span.start_child(
+            op=OP.GRAPHQL_RESOLVE,
+            name="resolving {}".format(field_path),
+            origin=StrawberryIntegration.origin,
+        ) as span:
+            span.set_data("graphql.field_name", info.field_name)
+            span.set_data("graphql.parent_type", info.parent_type.name)
+            span.set_data("graphql.field_path", field_path)
+            span.set_data("graphql.path", ".".join(map(str, info.path.as_list())))
+
+            return _next(root, info, *args, **kwargs)
+
+
+def _patch_views():
+    # type: () -> None
+    old_async_view_handle_errors = async_base_view.AsyncBaseHTTPView._handle_errors
+    old_sync_view_handle_errors = sync_base_view.SyncBaseHTTPView._handle_errors
+
+    def _sentry_patched_async_view_handle_errors(self, errors, response_data):
+        # type: (Any, List[GraphQLError], GraphQLHTTPResponse) -> None
+        old_async_view_handle_errors(self, errors, response_data)
+        _sentry_patched_handle_errors(self, errors, response_data)
+
+    def _sentry_patched_sync_view_handle_errors(self, errors, response_data):
+        # type: (Any, List[GraphQLError], GraphQLHTTPResponse) -> None
+        old_sync_view_handle_errors(self, errors, response_data)
+        _sentry_patched_handle_errors(self, errors, response_data)
+
+    @ensure_integration_enabled(StrawberryIntegration)
+    def _sentry_patched_handle_errors(self, errors, response_data):
+        # type: (Any, List[GraphQLError], GraphQLHTTPResponse) -> None
+        if not errors:
+            return
+
+        scope = sentry_sdk.get_isolation_scope()
+        event_processor = _make_response_event_processor(response_data)
+        scope.add_event_processor(event_processor)
+
+        with capture_internal_exceptions():
+            for error in errors:
+                event, hint = event_from_exception(
+                    error,
+                    client_options=sentry_sdk.get_client().options,
+                    mechanism={
+                        "type": StrawberryIntegration.identifier,
+                        "handled": False,
+                    },
+                )
+                sentry_sdk.capture_event(event, hint=hint)
+
+    async_base_view.AsyncBaseHTTPView._handle_errors = (  # type: ignore[method-assign]
+        _sentry_patched_async_view_handle_errors
+    )
+    sync_base_view.SyncBaseHTTPView._handle_errors = (  # type: ignore[method-assign]
+        _sentry_patched_sync_view_handle_errors
+    )
+
+
+def _make_request_event_processor(execution_context):
+    # type: (ExecutionContext) -> EventProcessor
+
+    def inner(event, hint):
+        # type: (Event, dict[str, Any]) -> Event
+        with capture_internal_exceptions():
+            if should_send_default_pii():
+                request_data = event.setdefault("request", {})
+                request_data["api_target"] = "graphql"
+
+                if not request_data.get("data"):
+                    data = {"query": execution_context.query}  # type: dict[str, Any]
+                    if execution_context.variables:
+                        data["variables"] = execution_context.variables
+                    if execution_context.operation_name:
+                        data["operationName"] = execution_context.operation_name
+
+                    request_data["data"] = data
+
+            else:
+                try:
+                    del event["request"]["data"]
+                except (KeyError, TypeError):
+                    pass
+
+        return event
+
+    return inner
+
+
+def _make_response_event_processor(response_data):
+    # type: (GraphQLHTTPResponse) -> EventProcessor
+
+    def inner(event, hint):
+        # type: (Event, dict[str, Any]) -> Event
+        with capture_internal_exceptions():
+            if should_send_default_pii():
+                contexts = event.setdefault("contexts", {})
+                contexts["response"] = {"data": response_data}
+
+        return event
+
+    return inner
+
+
+def _guess_if_using_async(extensions):
+    # type: (List[SchemaExtension]) -> bool
+    if StrawberrySentryAsyncExtension in extensions:
+        return True
+    elif StrawberrySentrySyncExtension in extensions:
+        return False
+
+    return bool(
+        {"starlette", "starlite", "litestar", "fastapi"} & set(_get_installed_modules())
+    )