From 80c33dcd2475e85e74e4d4996df27a64467f7c58 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Mon, 25 Sep 2023 16:09:44 +0200 Subject: [PATCH 01/37] add to tox.ini --- .../workflows/test-integration-strawberry.yml | 83 +++++++++++++++++++ tox.ini | 7 ++ 2 files changed, 90 insertions(+) create mode 100644 .github/workflows/test-integration-strawberry.yml diff --git a/.github/workflows/test-integration-strawberry.yml b/.github/workflows/test-integration-strawberry.yml new file mode 100644 index 0000000000..b0e30a8f5b --- /dev/null +++ b/.github/workflows/test-integration-strawberry.yml @@ -0,0 +1,83 @@ +name: Test strawberry + +on: + push: + branches: + - master + - release/** + + pull_request: + +# Cancel in progress workflows on pull_requests. +# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +env: + BUILD_CACHE_KEY: ${{ github.sha }} + CACHED_BUILD_PATHS: | + ${{ github.workspace }}/dist-serverless + +jobs: + test: + name: strawberry, python ${{ matrix.python-version }}, ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + + strategy: + fail-fast: false + matrix: + python-version: ["3.8","3.9","3.10","3.11"] + # python3.6 reached EOL and is no longer being supported on + # new versions of hosted runners on Github Actions + # ubuntu-20.04 is the last version that supported python3.6 + # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 + os: [ubuntu-20.04] + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Setup Test Env + run: | + pip install coverage "tox>=3,<4" + + - name: Test strawberry + uses: nick-fields/retry@v2 + with: + timeout_minutes: 15 + max_attempts: 2 + retry_wait_seconds: 5 + shell: bash + command: | + set -x # print commands that are executed + coverage erase + + # Run tests + ./scripts/runtox.sh "py${{ matrix.python-version }}-strawberry" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch && + coverage combine .coverage* && + coverage xml -i + + - uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + + + check_required_tests: + name: All strawberry tests passed or skipped + needs: test + # Always run this, even if a dependent job failed + if: always() + runs-on: ubuntu-20.04 + steps: + - name: Check for failures + if: contains(needs.test.result, 'failure') + run: | + echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 diff --git a/tox.ini b/tox.ini index 9e1c7a664f..ee03f40158 100644 --- a/tox.ini +++ b/tox.ini @@ -157,6 +157,9 @@ envlist = {py2.7,py3.7,py3.8,py3.9,py3.10,py3.11}-sqlalchemy-v{1.2,1.3,1.4} {py3.7,py3.8,py3.9,py3.10,py3.11}-sqlalchemy-v{2.0} + # Strawberry + {py3.8,py3.9,py3.10,py3.11}-strawberry + # Tornado {py3.7,py3.8,py3.9}-tornado-v{5} {py3.7,py3.8,py3.9,py3.10,py3.11}-tornado-v{6} @@ -457,6 +460,9 @@ deps = sqlalchemy-v1.4: sqlalchemy>=1.4,<2.0 sqlalchemy-v2.0: sqlalchemy>=2.0,<2.1 + # Strawberry + strawberry-graphql: strawberry-graphql>=0.208 + # Tornado tornado-v5: tornado>=5,<6 tornado-v6: tornado>=6.0a1 @@ -507,6 +513,7 @@ setenv = starlette: TESTPATH=tests/integrations/starlette starlite: TESTPATH=tests/integrations/starlite sqlalchemy: TESTPATH=tests/integrations/sqlalchemy + strawberry: TESTPATH=tests/integrations/strawberry tornado: TESTPATH=tests/integrations/tornado trytond: TESTPATH=tests/integrations/trytond socket: TESTPATH=tests/integrations/socket From 016218f75d87ee63f50bf60341875816238e2f56 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Mon, 25 Sep 2023 17:20:51 +0200 Subject: [PATCH 02/37] strawberry errors --- sentry_sdk/integrations/strawberry.py | 326 ++++++++++++++++++ tests/integrations/strawberry/__init__.py | 0 .../strawberry/test_strawberry_py3.py | 190 ++++++++++ tox.ini | 5 +- 4 files changed, 520 insertions(+), 1 deletion(-) create mode 100644 sentry_sdk/integrations/strawberry.py create mode 100644 tests/integrations/strawberry/__init__.py create mode 100644 tests/integrations/strawberry/test_strawberry_py3.py diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py new file mode 100644 index 0000000000..e25955d0d9 --- /dev/null +++ b/sentry_sdk/integrations/strawberry.py @@ -0,0 +1,326 @@ +from sentry_sdk import configure_scope, start_span +from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations.modules import _get_installed_modules +from sentry_sdk.hub import Hub, _should_send_default_pii +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + logger, + parse_version, +) +from sentry_sdk._types import TYPE_CHECKING + +try: + import strawberry.schema.schema as strawberry_schema + from strawberry import Schema + from strawberry.extensions import SchemaExtension + from strawberry.extensions.tracing.utils import should_skip_tracing + from strawberry.extensions.tracing import ( + SentryTracingExtension as StrawberrySentryAsyncExtension, + SentryTracingExtensionSync as StrawberrySentrySyncExtension, + ) +except ImportError: + raise DidNotEnable("strawberry-graphql is not installed") + +import hashlib +from functools import cached_property +from inspect import isawaitable +from typing import Any, Callable, Generator, Optional + + +if TYPE_CHECKING: + from graphql import GraphQLResolveInfo + from strawberry.types.execution import ExecutionContext + + +class StrawberryIntegration(Integration): + identifier = "strawberry" + + 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: %s (must be bool)" + % (async_execution) + ) + self.async_execution = async_execution + + @staticmethod + def setup_once(): + # type: () -> None + installed_packages = _get_installed_modules() + version = parse_version(installed_packages["strawberry-graphql"]) + + if version is None: + raise DidNotEnable( + "Unparsable strawberry-graphql version: {}".format(version) + ) + + if version < (0, 208): + raise DidNotEnable("strawberry-graphql 0.208 or newer required.") + + # _patch_schema_init() + _patch_execute() + + +def _patch_schema_init(): + # type: () -> None + old_schema = Schema.__init__ + + def _sentry_patched_schema_init(self, *args, **kwargs): + integration = Hub.current.get_integration(StrawberryIntegration) + if integration is None: + return old_schema(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 = bool( + {"starlette", "starlite", "litestar", "fastapi"} + & set(_get_installed_modules()) + ) + + logger.info( + "Assuming strawberry is running in %s context. If not, initialize the integration with async_execution=%s.", + "async" if should_use_async_extension else "sync", + "False" if should_use_async_extension else "True", + ) + + # remove the strawberry sentry extension, if present, to avoid double + # tracing + 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(self, *args, **kwargs) + + # XXX + not_yet_patched = old_schema.__name__ != "_sentry_patched_schema_init" + if not_yet_patched: + Schema.__init__ = _sentry_patched_schema_init + + +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): + assert self.execution_context.query + + query_hash = self.hash_query(self.execution_context.query) + + if self.execution_context.operation_name: + return f"{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 + name = f"{self._operation_name}" if self._operation_name else "Anonymous Query" + + with configure_scope() as scope: + if scope.span: + self.gql_span = scope.span.start_child( + op="gql", + description=name, + ) + else: + self.gql_span = start_span( + op="gql", + ) + + operation_type = "query" + + assert self.execution_context.query + + if self.execution_context.query.strip().startswith("mutation"): + operation_type = "mutation" + if self.execution_context.query.strip().startswith("subscription"): + operation_type = "subscription" + + self.gql_span.set_tag("graphql.operation_type", operation_type) + self.gql_span.set_tag("graphql.resource_name", self._resource_name) + self.gql_span.set_data("graphql.query", self.execution_context.query) + + yield + + self.gql_span.finish() + + def on_validate(self): + # type: () -> Generator[None, None, None] + self.validation_span = self.gql_span.start_child( + op="validation", description="Validation" + ) + + yield + + self.validation_span.finish() + + def on_parse(self): + # type: () -> Generator[None, None, None] + self.parsing_span = self.gql_span.start_child( + op="parsing", description="Parsing" + ) + + yield + + self.parsing_span.finish() + + def should_skip_tracing(self, _next, info): + # type: (Callable, GraphQLResolveInfo) -> bool + return should_skip_tracing(_next, info) + + async def resolve(self, _next, root, info, *args, **kwargs): + # type: (Callable, Any, GraphQLResolveInfo, str, Any) -> Any + if self.should_skip_tracing(_next, info): + result = _next(root, info, *args, **kwargs) + + if isawaitable(result): # pragma: no cover + result = await result + + return result + + field_path = f"{info.parent_type}.{info.field_name}" + + with self.gql_span.start_child( + op="resolve", description=f"Resolving: {field_path}" + ) as span: + span.set_tag("graphql.field_name", info.field_name) + span.set_tag("graphql.parent_type", info.parent_type.name) + span.set_tag("graphql.field_path", field_path) + span.set_tag("graphql.path", ".".join(map(str, info.path.as_list()))) + + result = _next(root, info, *args, **kwargs) + + if isawaitable(result): + result = await result + + return result + + +class SentrySyncExtension(SentryAsyncExtension): + def resolve(self, _next, root, info, *args, **kwargs): + # type: (Callable, Any, GraphQLResolveInfo, str, Any) -> Any + if self.should_skip_tracing(_next, info): + return _next(root, info, *args, **kwargs) + + field_path = f"{info.parent_type}.{info.field_name}" + + with self.gql_span.start_child( + op="resolve", description=f"Resolving: {field_path}" + ) as span: + span.set_tag("graphql.field_name", info.field_name) + span.set_tag("graphql.parent_type", info.parent_type.name) + span.set_tag("graphql.field_path", field_path) + span.set_tag("graphql.path", ".".join(map(str, info.path.as_list()))) + + return _next(root, info, *args, **kwargs) + + +def _patch_execute(): + old_execute_async = strawberry_schema.execute + old_execute_sync = strawberry_schema.execute_sync + + async def _sentry_patched_execute_async(*args, **kwargs): + hub = Hub.current + integration = hub.get_integration(StrawberryIntegration) + if integration is None: + return await old_execute_async(*args, **kwargs) + + result = await old_execute_async(*args, **kwargs) + + if "execution_context" in kwargs and result.errors: + with hub.configure_scope() as scope: + event_processor = _make_event_processor( + kwargs["execution_context"].query + ) + scope.add_event_processor(event_processor) + + with capture_internal_exceptions(): + for error in result.errors or []: + event, hint = event_from_exception( + error, + client_options=hub.client.options if hub.client else None, + mechanism={ + "type": integration.identifier, + "handled": False, + }, + ) + hub.capture_event(event, hint=hint) + + return result + + def _sentry_patched_execute_sync(*args, **kwargs): + hub = Hub.current + integration = hub.get_integration(StrawberryIntegration) + if integration is None: + return old_execute_sync(*args, **kwargs) + + result = old_execute_sync(*args, **kwargs) + + if "execution_context" in kwargs and result.errors: + with hub.configure_scope() as scope: + event_processor = _make_event_processor( + kwargs["execution_context"].query + ) + scope.add_event_processor(event_processor) + + with capture_internal_exceptions(): + for error in result.errors or []: + event, hint = event_from_exception( + error, + client_options=hub.client.options if hub.client else None, + mechanism={ + "type": integration.identifier, + "handled": False, + }, + ) + hub.capture_event(event, hint=hint) + + return result + + strawberry_schema.execute = _sentry_patched_execute_async + strawberry_schema.execute_sync = _sentry_patched_execute_sync + + +def _make_event_processor(query): + def inner(event, hint): + with capture_internal_exceptions(): + # XXX + if _should_send_default_pii(): + request_data = event.setdefault("request", {}) + request_data["api_target"] = "graphql" + if not request_data.get("data"): + request_data["data"] = {"query": query} # XXX + + elif event.get("request", {}).get("data"): + del event["request"]["data"] + + return event + + return inner diff --git a/tests/integrations/strawberry/__init__.py b/tests/integrations/strawberry/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/strawberry/test_strawberry_py3.py b/tests/integrations/strawberry/test_strawberry_py3.py new file mode 100644 index 0000000000..0291eacc70 --- /dev/null +++ b/tests/integrations/strawberry/test_strawberry_py3.py @@ -0,0 +1,190 @@ +import pytest + +strawberry = pytest.importorskip("strawberry") +pytest.importorskip("fastapi") +pytest.importorskip("flask") + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from flask import Flask +from strawberry.fastapi import GraphQLRouter +from strawberry.flask.views import GraphQLView + +from sentry_sdk.integrations.fastapi import FastApiIntegration +from sentry_sdk.integrations.flask import FlaskIntegration +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.integrations.starlette import StarletteIntegration +from sentry_sdk.integrations.strawberry import StrawberryIntegration + +ignore_logger("strawberry*") + + +@strawberry.type +class Query: + @strawberry.field + def hello(self) -> str: + return "Hello World" + + @strawberry.field + def error(self) -> str: + raise RuntimeError("oh no!") + + +def test_capture_request_if_available_and_send_pii_is_on_async( + sentry_init, capture_events +): + sentry_init( + send_default_pii=True, + integrations=[ + StrawberryIntegration(), + FastApiIntegration(), + StarletteIntegration(), + ], + ) + events = capture_events() + + schema = strawberry.Schema(Query) + + async_app = FastAPI() + async_app.include_router(GraphQLRouter(schema), prefix="/graphql") + + query = {"query": "query ErrorQuery { error }"} + client = TestClient(async_app) + client.post("/graphql", json=query) + + assert len(events) == 1 + + (error_event,) = events + assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" + assert error_event["request"]["api_target"] == "graphql" + assert error_event["request"]["data"] == query + + +def test_capture_request_if_available_and_send_pii_is_on_sync( + sentry_init, capture_events +): + sentry_init( + send_default_pii=True, + integrations=[StrawberryIntegration(), FlaskIntegration()], + ) + events = capture_events() + + schema = strawberry.Schema(Query) + + sync_app = Flask(__name__) + sync_app.add_url_rule( + "/graphql", + view_func=GraphQLView.as_view("graphql_view", schema=schema), + ) + + query = {"query": "query ErrorQuery { error }"} + client = sync_app.test_client() + client.post("/graphql", json=query) + + assert len(events) == 1 + + (error_event,) = events + assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" + assert error_event["request"]["api_target"] == "graphql" + assert error_event["request"]["data"] == query + + +def test_do_not_capture_request_if_send_pii_is_off_async(sentry_init, capture_events): + sentry_init( + integrations=[ + StrawberryIntegration(), + FastApiIntegration(), + StarletteIntegration(), + ], + ) + events = capture_events() + + schema = strawberry.Schema(Query) + + async_app = FastAPI() + async_app.include_router(GraphQLRouter(schema), prefix="/graphql") + + query = {"query": "query ErrorQuery { error }"} + client = TestClient(async_app) + client.post("/graphql", json=query) + + assert len(events) == 1 + + (error_event,) = events + assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" + assert "data" not in error_event["request"] + assert "response" not in error_event["contexts"] + + +def test_do_not_capture_request_if_send_pii_is_off_sync(sentry_init, capture_events): + sentry_init( + integrations=[StrawberryIntegration(), FlaskIntegration()], + ) + events = capture_events() + + schema = strawberry.Schema(Query) + + sync_app = Flask(__name__) + sync_app.add_url_rule( + "/graphql", + view_func=GraphQLView.as_view("graphql_view", schema=schema), + ) + + query = {"query": "query ErrorQuery { error }"} + client = sync_app.test_client() + client.post("/graphql", json=query) + + assert len(events) == 1 + + (error_event,) = events + assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" + assert "data" not in error_event["request"] + assert "response" not in error_event["contexts"] + + +def test_no_event_if_no_errors_async(sentry_init, capture_events): + sentry_init( + integrations=[ + StrawberryIntegration(), + FastApiIntegration(), + StarletteIntegration(), + ], + ) + events = capture_events() + + schema = strawberry.Schema(Query) + + async_app = FastAPI() + async_app.include_router(GraphQLRouter(schema), prefix="/graphql") + + query = {"query": "query GreetingQuery { hello }"} + client = TestClient(async_app) + client.post("/graphql", json=query) + + assert len(events) == 0 + + +def test_no_event_if_no_errors_sync(sentry_init, capture_events): + sentry_init( + integrations=[ + StrawberryIntegration(), + FlaskIntegration(), + ], + ) + events = capture_events() + + schema = strawberry.Schema(Query) + + sync_app = Flask(__name__) + sync_app.add_url_rule( + "/graphql", + view_func=GraphQLView.as_view("graphql_view", schema=schema), + ) + + query = { + "query": "query GreetingQuery { hello }", + } + client = sync_app.test_client() + client.post("/graphql", json=query) + + assert len(events) == 0 diff --git a/tox.ini b/tox.ini index ee03f40158..92afa6df3a 100644 --- a/tox.ini +++ b/tox.ini @@ -461,7 +461,10 @@ deps = sqlalchemy-v2.0: sqlalchemy>=2.0,<2.1 # Strawberry - strawberry-graphql: strawberry-graphql>=0.208 + strawberry: strawberry-graphql[fastapi,flask]>=0.208 + strawberry: fastapi + strawberry: flask + strawberry: httpx # Tornado tornado-v5: tornado>=5,<6 From f97c8eb3799a5927b97c5f166a220ec972ab453d Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Mon, 25 Sep 2023 18:39:53 +0200 Subject: [PATCH 03/37] wip --- sentry_sdk/integrations/strawberry.py | 62 ++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index e25955d0d9..262e31d388 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -11,6 +11,7 @@ from sentry_sdk._types import TYPE_CHECKING try: + import strawberry.http as strawberry_http import strawberry.schema.schema as strawberry_schema from strawberry import Schema from strawberry.extensions import SchemaExtension @@ -25,12 +26,13 @@ import hashlib from functools import cached_property from inspect import isawaitable -from typing import Any, Callable, Generator, Optional if TYPE_CHECKING: + from typing import Any, Callable, Dict, Generator, Optional from graphql import GraphQLResolveInfo from strawberry.types.execution import ExecutionContext + from sentry_sdk._types import EventProcessor class StrawberryIntegration(Integration): @@ -61,6 +63,7 @@ def setup_once(): # _patch_schema_init() _patch_execute() + _patch_process_result() def _patch_schema_init(): @@ -243,8 +246,10 @@ def resolve(self, _next, root, info, *args, **kwargs): def _patch_execute(): + # type: () -> None old_execute_async = strawberry_schema.execute old_execute_sync = strawberry_schema.execute_sync + # XXX capture response, see create_response or run async def _sentry_patched_execute_async(*args, **kwargs): hub = Hub.current @@ -256,8 +261,8 @@ async def _sentry_patched_execute_async(*args, **kwargs): if "execution_context" in kwargs and result.errors: with hub.configure_scope() as scope: - event_processor = _make_event_processor( - kwargs["execution_context"].query + event_processor = _make_request_event_processor( + kwargs["execution_context"] ) scope.add_event_processor(event_processor) @@ -285,8 +290,8 @@ def _sentry_patched_execute_sync(*args, **kwargs): if "execution_context" in kwargs and result.errors: with hub.configure_scope() as scope: - event_processor = _make_event_processor( - kwargs["execution_context"].query + event_processor = _make_request_event_processor( + kwargs["execution_context"] ) scope.add_event_processor(event_processor) @@ -308,10 +313,40 @@ def _sentry_patched_execute_sync(*args, **kwargs): strawberry_schema.execute_sync = _sentry_patched_execute_sync -def _make_event_processor(query): +def _patch_process_result(): + # type: () -> None + old_process_result = strawberry_http.process_result + + async def _sentry_patched_process_result(*args, **kwargs): + hub = Hub.current + integration = hub.get_integration(StrawberryIntegration) + if integration is None: + return old_process_result(*args, **kwargs) + + result = old_process_result(*args, **kwargs) + + with hub.configure_scope() as scope: + event_processor = _make_response_event_processor(result) + scope.add_event_processor(event_processor) + + return result + + strawberry_http.process_result = _sentry_patched_process_result + + +# XXX check size before attaching req/resp + + +def _make_request_event_processor(execution_context): + # type: (ExecutionContext) -> EventProcessor + # XXX query type def inner(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] with capture_internal_exceptions(): # XXX + query = execution_context.query + variables = execution_context.variables + if _should_send_default_pii(): request_data = event.setdefault("request", {}) request_data["api_target"] = "graphql" @@ -324,3 +359,18 @@ def inner(event, hint): return event return inner + + +def _make_response_event_processor(data): + # type: (Dict[str, Any]) -> EventProcessor + + def inner(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] + with capture_internal_exceptions(): + if _should_send_default_pii(): + contexts = event.setdefault("contexts", {}) + contexts["response"] = {"data": data} + + return event + + return inner From 408481f6727d7b6bd298458f4832b4a8d0d6dbbd Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 26 Sep 2023 13:17:26 +0200 Subject: [PATCH 04/37] tweaks --- sentry_sdk/integrations/strawberry.py | 35 ++++++++++++++------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 262e31d388..7ed1187819 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -249,7 +249,6 @@ def _patch_execute(): # type: () -> None old_execute_async = strawberry_schema.execute old_execute_sync = strawberry_schema.execute_sync - # XXX capture response, see create_response or run async def _sentry_patched_execute_async(*args, **kwargs): hub = Hub.current @@ -317,41 +316,43 @@ def _patch_process_result(): # type: () -> None old_process_result = strawberry_http.process_result - async def _sentry_patched_process_result(*args, **kwargs): + async def _sentry_patched_process_result(result, *args, **kwargs): hub = Hub.current integration = hub.get_integration(StrawberryIntegration) if integration is None: - return old_process_result(*args, **kwargs) + return old_process_result(result, *args, **kwargs) - result = old_process_result(*args, **kwargs) + processed_result = old_process_result(result, *args, **kwargs) - with hub.configure_scope() as scope: - event_processor = _make_response_event_processor(result) - scope.add_event_processor(event_processor) + if result.errors: + with hub.configure_scope() as scope: + event_processor = _make_response_event_processor(processed_result) + scope.add_event_processor(event_processor) - return result + return processed_result strawberry_http.process_result = _sentry_patched_process_result -# XXX check size before attaching req/resp - - def _make_request_event_processor(execution_context): # type: (ExecutionContext) -> EventProcessor - # XXX query type + def inner(event, hint): # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] with capture_internal_exceptions(): - # XXX - query = execution_context.query - variables = execution_context.variables - if _should_send_default_pii(): request_data = event.setdefault("request", {}) request_data["api_target"] = "graphql" + if not request_data.get("data"): - request_data["data"] = {"query": query} # XXX + request_data["data"] = {"query": execution_context.query} + + if execution_context.variables: + request_data["data"]["variables"] = execution_context.variables + if execution_context.operation_name: + request_data["data"][ + "operationName" + ] = execution_context.operation_name elif event.get("request", {}).get("data"): del event["request"]["data"] From e589eb2d4af2a470082f0d066b6262adad428236 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 26 Sep 2023 13:44:46 +0200 Subject: [PATCH 05/37] more tests --- sentry_sdk/integrations/strawberry.py | 26 +++-- .../strawberry/test_strawberry_py3.py | 106 +++++++++++++++++- 2 files changed, 117 insertions(+), 15 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 7ed1187819..c13a9644a0 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -61,19 +61,19 @@ def setup_once(): if version < (0, 208): raise DidNotEnable("strawberry-graphql 0.208 or newer required.") - # _patch_schema_init() + _patch_schema_init() _patch_execute() _patch_process_result() def _patch_schema_init(): # type: () -> None - old_schema = Schema.__init__ + old_schema_init = Schema.__init__ def _sentry_patched_schema_init(self, *args, **kwargs): integration = Hub.current.get_integration(StrawberryIntegration) if integration is None: - return old_schema(self, *args, **kwargs) + return old_schema_init(self, *args, **kwargs) extensions = kwargs.get("extensions") or [] @@ -81,10 +81,15 @@ def _sentry_patched_schema_init(self, *args, **kwargs): should_use_async_extension = integration.async_execution else: # try to figure it out ourselves - should_use_async_extension = bool( - {"starlette", "starlite", "litestar", "fastapi"} - & set(_get_installed_modules()) - ) + if StrawberrySentryAsyncExtension in extensions: + should_use_async_extension = True + elif StrawberrySentrySyncExtension in extensions: + should_use_async_extension = False + else: + should_use_async_extension = bool( + {"starlette", "starlite", "litestar", "fastapi"} + & set(_get_installed_modules()) + ) logger.info( "Assuming strawberry is running in %s context. If not, initialize the integration with async_execution=%s.", @@ -108,12 +113,9 @@ def _sentry_patched_schema_init(self, *args, **kwargs): kwargs["extensions"] = extensions - return old_schema(self, *args, **kwargs) + return old_schema_init(self, *args, **kwargs) - # XXX - not_yet_patched = old_schema.__name__ != "_sentry_patched_schema_init" - if not_yet_patched: - Schema.__init__ = _sentry_patched_schema_init + Schema.__init__ = _sentry_patched_schema_init class SentryAsyncExtension(SchemaExtension): diff --git a/tests/integrations/strawberry/test_strawberry_py3.py b/tests/integrations/strawberry/test_strawberry_py3.py index 0291eacc70..3b45b6998a 100644 --- a/tests/integrations/strawberry/test_strawberry_py3.py +++ b/tests/integrations/strawberry/test_strawberry_py3.py @@ -7,16 +7,26 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from flask import Flask +from strawberry.extensions.tracing import ( # XXX conditional on strawberry version + SentryTracingExtension, + SentryTracingExtensionSync, +) from strawberry.fastapi import GraphQLRouter from strawberry.flask.views import GraphQLView from sentry_sdk.integrations.fastapi import FastApiIntegration from sentry_sdk.integrations.flask import FlaskIntegration -from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk.integrations.starlette import StarletteIntegration -from sentry_sdk.integrations.strawberry import StrawberryIntegration +from sentry_sdk.integrations.strawberry import ( + StrawberryIntegration, + SentryAsyncExtension, + SentrySyncExtension, +) -ignore_logger("strawberry*") +try: + from unittest import mock # python 3.3 and above +except ImportError: + import mock # python < 3.3 @strawberry.type @@ -30,6 +40,96 @@ def error(self) -> str: raise RuntimeError("oh no!") +def test_async_execution_uses_async_extension(sentry_init): + sentry_init( + integrations=[ + StrawberryIntegration(async_execution=True), + ], + ) + + with mock.patch( + "sentry_sdk.integrations.strawberry._get_installed_modules", + return_value={"flask": "2.3.3"}, + ): + # actual installed modules should not matter, the explicit option takes + # precedence + schema = strawberry.Schema(Query) + assert SentryAsyncExtension in schema.extensions + + +def test_sync_execution_uses_sync_extension(sentry_init): + sentry_init( + integrations=[ + StrawberryIntegration(async_execution=False), + ], + ) + + with mock.patch( + "sentry_sdk.integrations.strawberry._get_installed_modules", + return_value={"fastapi": "0.103.1", "starlette": "0.27.0"}, + ): + # actual installed modules should not matter, the explicit option takes + # precedence + schema = strawberry.Schema(Query) + assert SentrySyncExtension in schema.extensions + + +def test_infer_execution_type_from_installed_packages_async(sentry_init): + sentry_init( + integrations=[ + StrawberryIntegration(), + ], + ) + + with mock.patch( + "sentry_sdk.integrations.strawberry._get_installed_modules", + return_value={"fastapi": "0.103.1", "starlette": "0.27.0"}, + ): + schema = strawberry.Schema(Query) + assert SentryAsyncExtension in schema.extensions + + +def test_infer_execution_type_from_installed_packages_sync(sentry_init): + sentry_init( + integrations=[ + StrawberryIntegration(), + ], + ) + + with mock.patch( + "sentry_sdk.integrations.strawberry._get_installed_modules", + return_value={"flask": "2.3.3"}, + ): + schema = strawberry.Schema(Query) + assert SentrySyncExtension in schema.extensions + + +def test_replace_existing_sentry_async_extension(sentry_init): + sentry_init( + send_default_pii=True, + integrations=[ + StrawberryIntegration(), + ], + ) + + schema = strawberry.Schema(Query, extensions=[SentryTracingExtension]) + assert SentryTracingExtension not in schema.extensions + assert SentryAsyncExtension in schema.extensions + + +def test_replace_existing_sentry_sync_extension(sentry_init): + sentry_init( + send_default_pii=True, + integrations=[ + StrawberryIntegration(), + ], + ) + + schema = strawberry.Schema(Query, extensions=[SentryTracingExtensionSync]) + assert SentryTracingExtensionSync not in schema.extensions + assert SentrySyncExtension in schema.extensions + + def test_capture_request_if_available_and_send_pii_is_on_async( sentry_init, capture_events ): From af1e23bc066e5cdb86de2466703a6a644c3c9b9e Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 26 Sep 2023 16:28:48 +0200 Subject: [PATCH 06/37] fixes --- sentry_sdk/integrations/strawberry.py | 55 +++++----- .../strawberry/test_strawberry_py3.py | 102 +++++++++--------- 2 files changed, 79 insertions(+), 78 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index c13a9644a0..4d25626cd1 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -1,5 +1,9 @@ +import hashlib +from functools import cached_property +from inspect import isawaitable from sentry_sdk import configure_scope, start_span from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk.integrations.modules import _get_installed_modules from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.utils import ( @@ -20,14 +24,11 @@ SentryTracingExtension as StrawberrySentryAsyncExtension, SentryTracingExtensionSync as StrawberrySentrySyncExtension, ) + from strawberry.fastapi import router as fastapi_router + from strawberry.http import async_base_view, sync_base_view except ImportError: raise DidNotEnable("strawberry-graphql is not installed") -import hashlib -from functools import cached_property -from inspect import isawaitable - - if TYPE_CHECKING: from typing import Any, Callable, Dict, Generator, Optional from graphql import GraphQLResolveInfo @@ -35,6 +36,9 @@ from sentry_sdk._types import EventProcessor +ignore_logger("strawberry.execution") + + class StrawberryIntegration(Integration): identifier = "strawberry" @@ -267,18 +271,6 @@ async def _sentry_patched_execute_async(*args, **kwargs): ) scope.add_event_processor(event_processor) - with capture_internal_exceptions(): - for error in result.errors or []: - event, hint = event_from_exception( - error, - client_options=hub.client.options if hub.client else None, - mechanism={ - "type": integration.identifier, - "handled": False, - }, - ) - hub.capture_event(event, hint=hint) - return result def _sentry_patched_execute_sync(*args, **kwargs): @@ -296,18 +288,6 @@ def _sentry_patched_execute_sync(*args, **kwargs): ) scope.add_event_processor(event_processor) - with capture_internal_exceptions(): - for error in result.errors or []: - event, hint = event_from_exception( - error, - client_options=hub.client.options if hub.client else None, - mechanism={ - "type": integration.identifier, - "handled": False, - }, - ) - hub.capture_event(event, hint=hint) - return result strawberry_schema.execute = _sentry_patched_execute_async @@ -318,7 +298,7 @@ def _patch_process_result(): # type: () -> None old_process_result = strawberry_http.process_result - async def _sentry_patched_process_result(result, *args, **kwargs): + def _sentry_patched_process_result(result, *args, **kwargs): hub = Hub.current integration = hub.get_integration(StrawberryIntegration) if integration is None: @@ -331,9 +311,24 @@ async def _sentry_patched_process_result(result, *args, **kwargs): event_processor = _make_response_event_processor(processed_result) scope.add_event_processor(event_processor) + with capture_internal_exceptions(): + for error in result.errors or []: + event, hint = event_from_exception( + error, + client_options=hub.client.options if hub.client else None, + mechanism={ + "type": integration.identifier, + "handled": False, + }, + ) + hub.capture_event(event, hint=hint) + return processed_result strawberry_http.process_result = _sentry_patched_process_result + async_base_view.process_result = _sentry_patched_process_result + sync_base_view.process_result = _sentry_patched_process_result + fastapi_router.process_result = _sentry_patched_process_result def _make_request_event_processor(execution_context): diff --git a/tests/integrations/strawberry/test_strawberry_py3.py b/tests/integrations/strawberry/test_strawberry_py3.py index 3b45b6998a..4d2356c2d7 100644 --- a/tests/integrations/strawberry/test_strawberry_py3.py +++ b/tests/integrations/strawberry/test_strawberry_py3.py @@ -37,15 +37,11 @@ def hello(self) -> str: @strawberry.field def error(self) -> str: - raise RuntimeError("oh no!") + return 1 / 0 def test_async_execution_uses_async_extension(sentry_init): - sentry_init( - integrations=[ - StrawberryIntegration(async_execution=True), - ], - ) + sentry_init(integrations=[StrawberryIntegration(async_execution=True)]) with mock.patch( "sentry_sdk.integrations.strawberry._get_installed_modules", @@ -58,11 +54,7 @@ def test_async_execution_uses_async_extension(sentry_init): def test_sync_execution_uses_sync_extension(sentry_init): - sentry_init( - integrations=[ - StrawberryIntegration(async_execution=False), - ], - ) + sentry_init(integrations=[StrawberryIntegration(async_execution=False)]) with mock.patch( "sentry_sdk.integrations.strawberry._get_installed_modules", @@ -75,11 +67,7 @@ def test_sync_execution_uses_sync_extension(sentry_init): def test_infer_execution_type_from_installed_packages_async(sentry_init): - sentry_init( - integrations=[ - StrawberryIntegration(), - ], - ) + sentry_init(integrations=[StrawberryIntegration()]) with mock.patch( "sentry_sdk.integrations.strawberry._get_installed_modules", @@ -90,11 +78,7 @@ def test_infer_execution_type_from_installed_packages_async(sentry_init): def test_infer_execution_type_from_installed_packages_sync(sentry_init): - sentry_init( - integrations=[ - StrawberryIntegration(), - ], - ) + sentry_init(integrations=[StrawberryIntegration()]) with mock.patch( "sentry_sdk.integrations.strawberry._get_installed_modules", @@ -105,12 +89,7 @@ def test_infer_execution_type_from_installed_packages_sync(sentry_init): def test_replace_existing_sentry_async_extension(sentry_init): - sentry_init( - send_default_pii=True, - integrations=[ - StrawberryIntegration(), - ], - ) + sentry_init(integrations=[StrawberryIntegration()]) schema = strawberry.Schema(Query, extensions=[SentryTracingExtension]) assert SentryTracingExtension not in schema.extensions @@ -118,12 +97,7 @@ def test_replace_existing_sentry_async_extension(sentry_init): def test_replace_existing_sentry_sync_extension(sentry_init): - sentry_init( - send_default_pii=True, - integrations=[ - StrawberryIntegration(), - ], - ) + sentry_init(integrations=[StrawberryIntegration()]) schema = strawberry.Schema(Query, extensions=[SentryTracingExtensionSync]) assert SentryTracingExtensionSync not in schema.extensions @@ -140,6 +114,7 @@ def test_capture_request_if_available_and_send_pii_is_on_async( FastApiIntegration(), StarletteIntegration(), ], + traces_sample_rate=1, ) events = capture_events() @@ -152,12 +127,24 @@ def test_capture_request_if_available_and_send_pii_is_on_async( client = TestClient(async_app) client.post("/graphql", json=query) - assert len(events) == 1 + assert len(events) == 2 - (error_event,) = events + (error_event, transaction_event) = events assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" assert error_event["request"]["api_target"] == "graphql" assert error_event["request"]["data"] == query + assert error_event["contexts"]["response"] == { + "data": { + "data": None, + "errors": [ + { + "message": "division by zero", + "locations": [{"line": 1, "column": 20}], + "path": ["error"], + } + ], + } + } def test_capture_request_if_available_and_send_pii_is_on_sync( @@ -165,7 +152,8 @@ def test_capture_request_if_available_and_send_pii_is_on_sync( ): sentry_init( send_default_pii=True, - integrations=[StrawberryIntegration(), FlaskIntegration()], + integrations=[StrawberryIntegration(async_execution=False), FlaskIntegration()], + traces_sample_rate=1, ) events = capture_events() @@ -181,12 +169,24 @@ def test_capture_request_if_available_and_send_pii_is_on_sync( client = sync_app.test_client() client.post("/graphql", json=query) - assert len(events) == 1 + assert len(events) == 2 - (error_event,) = events + (error_event, transaction_event) = events assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" assert error_event["request"]["api_target"] == "graphql" assert error_event["request"]["data"] == query + assert error_event["contexts"]["response"] == { + "data": { + "data": None, + "errors": [ + { + "message": "division by zero", + "locations": [{"line": 1, "column": 20}], + "path": ["error"], + } + ], + } + } def test_do_not_capture_request_if_send_pii_is_off_async(sentry_init, capture_events): @@ -196,6 +196,7 @@ def test_do_not_capture_request_if_send_pii_is_off_async(sentry_init, capture_ev FastApiIntegration(), StarletteIntegration(), ], + traces_sample_rate=1, ) events = capture_events() @@ -208,9 +209,9 @@ def test_do_not_capture_request_if_send_pii_is_off_async(sentry_init, capture_ev client = TestClient(async_app) client.post("/graphql", json=query) - assert len(events) == 1 + assert len(events) == 2 - (error_event,) = events + (error_event, transaction_event) = events assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" assert "data" not in error_event["request"] assert "response" not in error_event["contexts"] @@ -218,7 +219,8 @@ def test_do_not_capture_request_if_send_pii_is_off_async(sentry_init, capture_ev def test_do_not_capture_request_if_send_pii_is_off_sync(sentry_init, capture_events): sentry_init( - integrations=[StrawberryIntegration(), FlaskIntegration()], + integrations=[StrawberryIntegration(async_execution=False), FlaskIntegration()], + traces_sample_rate=1, ) events = capture_events() @@ -234,21 +236,22 @@ def test_do_not_capture_request_if_send_pii_is_off_sync(sentry_init, capture_eve client = sync_app.test_client() client.post("/graphql", json=query) - assert len(events) == 1 + assert len(events) == 2 - (error_event,) = events + (error_event, transaction_event) = events assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" assert "data" not in error_event["request"] assert "response" not in error_event["contexts"] -def test_no_event_if_no_errors_async(sentry_init, capture_events): +def test_no_errors_async(sentry_init, capture_events): sentry_init( integrations=[ StrawberryIntegration(), FastApiIntegration(), StarletteIntegration(), ], + traces_sample_rate=1, ) events = capture_events() @@ -261,15 +264,17 @@ def test_no_event_if_no_errors_async(sentry_init, capture_events): client = TestClient(async_app) client.post("/graphql", json=query) - assert len(events) == 0 + assert len(events) == 1 + assert False -def test_no_event_if_no_errors_sync(sentry_init, capture_events): +def test_no_errors_sync(sentry_init, capture_events): sentry_init( integrations=[ - StrawberryIntegration(), + StrawberryIntegration(async_execution=False), FlaskIntegration(), ], + traces_sample_rate=1, ) events = capture_events() @@ -287,4 +292,5 @@ def test_no_event_if_no_errors_sync(sentry_init, capture_events): client = sync_app.test_client() client.post("/graphql", json=query) - assert len(events) == 0 + assert len(events) == 1 + assert False From fe036c103f0d0b796d0141d89cd4dede721e0406 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 26 Sep 2023 16:31:41 +0200 Subject: [PATCH 07/37] better info message --- sentry_sdk/integrations/strawberry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 4d25626cd1..b28dc8cce3 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -96,7 +96,7 @@ def _sentry_patched_schema_init(self, *args, **kwargs): ) logger.info( - "Assuming strawberry is running in %s context. If not, initialize the integration with async_execution=%s.", + "Assuming strawberry is running in %s context. 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", ) From 169ffd3bf99b6077d0b974629cb53d2bc2460836 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 26 Sep 2023 17:39:45 +0200 Subject: [PATCH 08/37] add graphql ops --- sentry_sdk/consts.py | 7 ++ sentry_sdk/integrations/strawberry.py | 94 +++++++++++++++------------ 2 files changed, 60 insertions(+), 41 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index d15cf3f569..44b90cbb5e 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -169,6 +169,13 @@ class OP: FUNCTION = "function" FUNCTION_AWS = "function.aws" FUNCTION_GCP = "function.gcp" + GRAPHQL_EXECUTE = "graphql.execute" + GRAPHQL_MUTATION = "graphql.mutation" + GRAPHQL_PARSE = "graphql.parse" + GRAPHQL_RESOLVE = "graphql.resolve" + GRAPHQL_SUBSCRIPTION = "graphql.subscription" + GRAPHQL_QUERY = "graphql.query" + GRAPHQL_VALIDATE = "graphql.validate" GRPC_CLIENT = "grpc.client" GRPC_SERVER = "grpc.server" HTTP_CLIENT = "http.client" diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index b28dc8cce3..367416da11 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -2,6 +2,7 @@ from functools import cached_property from inspect import isawaitable from sentry_sdk import configure_scope, start_span +from sentry_sdk.consts import OP from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk.integrations.modules import _get_installed_modules @@ -85,18 +86,10 @@ def _sentry_patched_schema_init(self, *args, **kwargs): should_use_async_extension = integration.async_execution else: # try to figure it out ourselves - if StrawberrySentryAsyncExtension in extensions: - should_use_async_extension = True - elif StrawberrySentrySyncExtension in extensions: - should_use_async_extension = False - else: - should_use_async_extension = bool( - {"starlette", "starlite", "litestar", "fastapi"} - & set(_get_installed_modules()) - ) + should_use_async_extension = _guess_if_using_async(extensions) logger.info( - "Assuming strawberry is running in %s context. If not, initialize it as StrawberryIntegration(async_execution=%s).", + "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", ) @@ -134,6 +127,7 @@ def __init__( @cached_property def _resource_name(self): + # type: () -> Any assert self.execution_context.query query_hash = self.hash_query(self.execution_context.query) @@ -150,40 +144,46 @@ def hash_query(self, query): def on_operation(self): # type: () -> Generator[None, None, None] self._operation_name = self.execution_context.operation_name - name = f"{self._operation_name}" if self._operation_name else "Anonymous Query" - - with configure_scope() as scope: - if scope.span: - self.gql_span = scope.span.start_child( - op="gql", - description=name, - ) - else: - self.gql_span = start_span( - op="gql", - ) + name = self._operation_name if self._operation_name else "" operation_type = "query" + op = OP.GRAPHQL_QUERY assert self.execution_context.query if self.execution_context.query.strip().startswith("mutation"): operation_type = "mutation" + op = OP.GRAPHQL_MUTATION if self.execution_context.query.strip().startswith("subscription"): operation_type = "subscription" + op = OP.GRAPHQL_SUBSCRIPTION - self.gql_span.set_tag("graphql.operation_type", operation_type) - self.gql_span.set_tag("graphql.resource_name", self._resource_name) - self.gql_span.set_data("graphql.query", self.execution_context.query) + with configure_scope() as scope: + if scope.span: + self.graphql_span = scope.span.start_child( + op=op, + description="{} {}".format(operation_type, name), + ) + else: + # XXX start transaction? + self.graphql_span = start_span( + op=op, + description="{} {}".format(operation_type, name), + ) + + 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.resource_name", self._resource_name) # XXX + self.graphql_span.set_data("graphql.document", self.execution_context.query) yield - self.gql_span.finish() + self.graphql_span.finish() def on_validate(self): # type: () -> Generator[None, None, None] - self.validation_span = self.gql_span.start_child( - op="validation", description="Validation" + self.validation_span = self.graphql_span.start_child( + op=OP.GRAPHQL_VALIDATE, description="validation" ) yield @@ -192,8 +192,8 @@ def on_validate(self): def on_parse(self): # type: () -> Generator[None, None, None] - self.parsing_span = self.gql_span.start_child( - op="parsing", description="Parsing" + self.parsing_span = self.graphql_span.start_child( + op=OP.GRAPHQL_PARSE, description="parsing" ) yield @@ -216,13 +216,13 @@ async def resolve(self, _next, root, info, *args, **kwargs): field_path = f"{info.parent_type}.{info.field_name}" - with self.gql_span.start_child( - op="resolve", description=f"Resolving: {field_path}" + with self.graphql_span.start_child( + op=OP.GRAPHQL_RESOLVE, description=f"resolving: {field_path}" ) as span: - span.set_tag("graphql.field_name", info.field_name) - span.set_tag("graphql.parent_type", info.parent_type.name) - span.set_tag("graphql.field_path", field_path) - span.set_tag("graphql.path", ".".join(map(str, info.path.as_list()))) + 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()))) result = _next(root, info, *args, **kwargs) @@ -241,12 +241,12 @@ def resolve(self, _next, root, info, *args, **kwargs): field_path = f"{info.parent_type}.{info.field_name}" with self.gql_span.start_child( - op="resolve", description=f"Resolving: {field_path}" + op=OP.GRAPHQL_RESOLVE, description=f"resolving: {field_path}" ) as span: - span.set_tag("graphql.field_name", info.field_name) - span.set_tag("graphql.parent_type", info.parent_type.name) - span.set_tag("graphql.field_path", field_path) - span.set_tag("graphql.path", ".".join(map(str, info.path.as_list()))) + 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) @@ -372,3 +372,15 @@ def inner(event, hint): return event return inner + + +def _guess_if_using_async(extensions): + # (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()) + ) From 5ed4f4a2191a271ce11254c864e409e9e8690029 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Sep 2023 10:23:31 +0200 Subject: [PATCH 09/37] compat --- sentry_sdk/integrations/strawberry.py | 43 ++++++++++++--------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 367416da11..e2334c16a6 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -47,8 +47,9 @@ 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: %s (must be bool)" - % (async_execution) + 'Invalid value for async_execution: "{}" (must be bool)'.format( + async_execution + ) ) self.async_execution = async_execution @@ -94,8 +95,7 @@ def _sentry_patched_schema_init(self, *args, **kwargs): "False" if should_use_async_extension else "True", ) - # remove the strawberry sentry extension, if present, to avoid double - # tracing + # remove the built in strawberry sentry extension, if present extensions = [ extension for extension in extensions @@ -127,13 +127,11 @@ def __init__( @cached_property def _resource_name(self): - # type: () -> Any - assert self.execution_context.query - + # type: () -> str query_hash = self.hash_query(self.execution_context.query) if self.execution_context.operation_name: - return f"{self.execution_context.operation_name}:{query_hash}" + return "{}:{}".format(self.execution_context.operation_name, query_hash) return query_hash @@ -144,13 +142,10 @@ def hash_query(self, query): def on_operation(self): # type: () -> Generator[None, None, None] self._operation_name = self.execution_context.operation_name - name = self._operation_name if self._operation_name else "" operation_type = "query" op = OP.GRAPHQL_QUERY - assert self.execution_context.query - if self.execution_context.query.strip().startswith("mutation"): operation_type = "mutation" op = OP.GRAPHQL_MUTATION @@ -158,23 +153,23 @@ def on_operation(self): operation_type = "subscription" op = OP.GRAPHQL_SUBSCRIPTION + if self._operation_name: + description = "{} {}".format(operation_type, self._operation_name) + else: + description = operation_type + with configure_scope() as scope: if scope.span: self.graphql_span = scope.span.start_child( - op=op, - description="{} {}".format(operation_type, name), + op=op, description=description ) else: - # XXX start transaction? - self.graphql_span = start_span( - op=op, - description="{} {}".format(operation_type, name), - ) + self.graphql_span = start_span(op=op, description=description) 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.resource_name", self._resource_name) # XXX self.graphql_span.set_data("graphql.document", self.execution_context.query) + self.graphql_span.set_data("graphql.resource_name", self._resource_name) yield @@ -209,15 +204,15 @@ async def resolve(self, _next, root, info, *args, **kwargs): if self.should_skip_tracing(_next, info): result = _next(root, info, *args, **kwargs) - if isawaitable(result): # pragma: no cover + if isawaitable(result): result = await result return result - field_path = f"{info.parent_type}.{info.field_name}" + field_path = "{}.{}".format(info.parent_type, info.field_name) with self.graphql_span.start_child( - op=OP.GRAPHQL_RESOLVE, description=f"resolving: {field_path}" + op=OP.GRAPHQL_RESOLVE, description="resolving {}".format(field_path) ) as span: span.set_data("graphql.field_name", info.field_name) span.set_data("graphql.parent_type", info.parent_type.name) @@ -238,10 +233,10 @@ def resolve(self, _next, root, info, *args, **kwargs): if self.should_skip_tracing(_next, info): return _next(root, info, *args, **kwargs) - field_path = f"{info.parent_type}.{info.field_name}" + field_path = "{}.{}".format(info.parent_type, info.field_name) with self.gql_span.start_child( - op=OP.GRAPHQL_RESOLVE, description=f"resolving: {field_path}" + op=OP.GRAPHQL_RESOLVE, description="resolving {}".format(field_path) ) as span: span.set_data("graphql.field_name", info.field_name) span.set_data("graphql.parent_type", info.parent_type.name) From f51e99208337c546a74353e37556fb26f5797a3a Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Sep 2023 11:56:42 +0200 Subject: [PATCH 10/37] fixes, more tests --- sentry_sdk/integrations/strawberry.py | 2 +- .../strawberry/test_strawberry_py3.py | 244 +++++++++++++++++- 2 files changed, 237 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index e2334c16a6..fcaf58fd69 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -235,7 +235,7 @@ def resolve(self, _next, root, info, *args, **kwargs): field_path = "{}.{}".format(info.parent_type, info.field_name) - with self.gql_span.start_child( + with self.graphql_span.start_child( op=OP.GRAPHQL_RESOLVE, description="resolving {}".format(field_path) ) as span: span.set_data("graphql.field_name", info.field_name) diff --git a/tests/integrations/strawberry/test_strawberry_py3.py b/tests/integrations/strawberry/test_strawberry_py3.py index 4d2356c2d7..a8fe6f076a 100644 --- a/tests/integrations/strawberry/test_strawberry_py3.py +++ b/tests/integrations/strawberry/test_strawberry_py3.py @@ -14,6 +14,7 @@ from strawberry.fastapi import GraphQLRouter from strawberry.flask.views import GraphQLView +from sentry_sdk.consts import OP from sentry_sdk.integrations.fastapi import FastApiIntegration from sentry_sdk.integrations.flask import FlaskIntegration from sentry_sdk.integrations.starlette import StarletteIntegration @@ -129,7 +130,8 @@ def test_capture_request_if_available_and_send_pii_is_on_async( assert len(events) == 2 - (error_event, transaction_event) = events + (error_event, _) = events + assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" assert error_event["request"]["api_target"] == "graphql" assert error_event["request"]["data"] == query @@ -171,7 +173,7 @@ def test_capture_request_if_available_and_send_pii_is_on_sync( assert len(events) == 2 - (error_event, transaction_event) = events + (error_event, _) = events assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" assert error_event["request"]["api_target"] == "graphql" assert error_event["request"]["data"] == query @@ -211,7 +213,7 @@ def test_do_not_capture_request_if_send_pii_is_off_async(sentry_init, capture_ev assert len(events) == 2 - (error_event, transaction_event) = events + (error_event, _) = events assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" assert "data" not in error_event["request"] assert "response" not in error_event["contexts"] @@ -238,13 +240,151 @@ def test_do_not_capture_request_if_send_pii_is_off_sync(sentry_init, capture_eve assert len(events) == 2 - (error_event, transaction_event) = events + (error_event, _) = events assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" assert "data" not in error_event["request"] assert "response" not in error_event["contexts"] -def test_no_errors_async(sentry_init, capture_events): +def test_capture_transaction_on_error_async(sentry_init, capture_events): + sentry_init( + send_default_pii=True, + integrations=[ + StrawberryIntegration(), + FastApiIntegration(), + StarletteIntegration(), + ], + traces_sample_rate=1, + ) + events = capture_events() + + schema = strawberry.Schema(Query) + + async_app = FastAPI() + async_app.include_router(GraphQLRouter(schema), prefix="/graphql") + + query = {"query": "query ErrorQuery { error }"} + client = TestClient(async_app) + client.post("/graphql", json=query) + + assert len(events) == 2 + (_, transaction_event) = events + + assert transaction_event["transaction"] == "/graphql" + assert transaction_event["spans"] + + query_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_QUERY + ] + assert len(query_spans) == 1, "exactly one query span expected" + query_span = query_spans[0] + assert query_span["description"] == "query" + assert query_span["data"]["graphql.operation.type"] == "query" + assert query_span["data"]["graphql.operation.name"] is None + assert query_span["data"]["graphql.document"] == query["query"] + assert query_span["data"]["graphql.resource_name"] + + parse_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE + ] + assert len(parse_spans) == 1, "exactly one parse span expected" + parse_span = parse_spans[0] + assert parse_span["parent_span_id"] == query_span["span_id"] + assert parse_span["description"] == "parsing" + + validate_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE + ] + assert len(validate_spans) == 1, "exactly one validate span expected" + validate_span = validate_spans[0] + assert validate_span["parent_span_id"] == query_span["span_id"] + assert validate_span["description"] == "validation" + + resolve_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE + ] + assert len(resolve_spans) == 1, "exactly one resolve span expected" + resolve_span = resolve_spans[0] + assert resolve_span["parent_span_id"] == query_span["span_id"] + assert resolve_span["description"] == "resolving Query.error" + assert resolve_span["data"] == { + "graphql.field_name": "error", + "graphql.parent_type": "Query", + "graphql.field_path": "Query.error", + "graphql.path": "error", + } + + +def test_capture_transaction_on_error_sync(sentry_init, capture_events): + sentry_init( + send_default_pii=True, + integrations=[StrawberryIntegration(async_execution=False), FlaskIntegration()], + traces_sample_rate=1, + ) + events = capture_events() + + schema = strawberry.Schema(Query) + + sync_app = Flask(__name__) + sync_app.add_url_rule( + "/graphql", + view_func=GraphQLView.as_view("graphql_view", schema=schema), + ) + + query = {"query": "query ErrorQuery { error }"} + client = sync_app.test_client() + client.post("/graphql", json=query) + + assert len(events) == 2 + + (_, transaction_event) = events + + assert transaction_event["transaction"] == "graphql_view" + assert transaction_event["spans"] + + query_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_QUERY + ] + assert len(query_spans) == 1, "exactly one query span expected" + query_span = query_spans[0] + assert query_span["description"] == "query" + assert query_span["data"]["graphql.operation.type"] == "query" + assert query_span["data"]["graphql.operation.name"] is None + assert query_span["data"]["graphql.document"] == query["query"] + assert query_span["data"]["graphql.resource_name"] + + parse_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE + ] + assert len(parse_spans) == 1, "exactly one parse span expected" + parse_span = parse_spans[0] + assert parse_span["parent_span_id"] == query_span["span_id"] + assert parse_span["description"] == "parsing" + + validate_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE + ] + assert len(validate_spans) == 1, "exactly one validate span expected" + validate_span = validate_spans[0] + assert validate_span["parent_span_id"] == query_span["span_id"] + assert validate_span["description"] == "validation" + + resolve_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE + ] + assert len(resolve_spans) == 1, "exactly one resolve span expected" + resolve_span = resolve_spans[0] + assert resolve_span["parent_span_id"] == query_span["span_id"] + assert resolve_span["description"] == "resolving Query.error" + assert resolve_span["data"] == { + "graphql.field_name": "error", + "graphql.parent_type": "Query", + "graphql.field_path": "Query.error", + "graphql.path": "error", + } + + +def test_capture_transaction_on_success_async(sentry_init, capture_events): sentry_init( integrations=[ StrawberryIntegration(), @@ -265,10 +405,54 @@ def test_no_errors_async(sentry_init, capture_events): client.post("/graphql", json=query) assert len(events) == 1 - assert False + (transaction_event,) = events + + assert transaction_event["transaction"] == "/graphql" + assert transaction_event["spans"] + + query_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_QUERY + ] + assert len(query_spans) == 1, "exactly one query span expected" + query_span = query_spans[0] + assert query_span["description"] == "query" + assert query_span["data"]["graphql.operation.type"] == "query" + assert query_span["data"]["graphql.operation.name"] is None + assert query_span["data"]["graphql.document"] == query["query"] + assert query_span["data"]["graphql.resource_name"] + + parse_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE + ] + assert len(parse_spans) == 1, "exactly one parse span expected" + parse_span = parse_spans[0] + assert parse_span["parent_span_id"] == query_span["span_id"] + assert parse_span["description"] == "parsing" + + validate_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE + ] + assert len(validate_spans) == 1, "exactly one validate span expected" + validate_span = validate_spans[0] + assert validate_span["parent_span_id"] == query_span["span_id"] + assert validate_span["description"] == "validation" + + resolve_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE + ] + assert len(resolve_spans) == 1, "exactly one resolve span expected" + resolve_span = resolve_spans[0] + assert resolve_span["parent_span_id"] == query_span["span_id"] + assert resolve_span["description"] == "resolving Query.hello" + assert resolve_span["data"] == { + "graphql.field_name": "hello", + "graphql.parent_type": "Query", + "graphql.field_path": "Query.hello", + "graphql.path": "hello", + } -def test_no_errors_sync(sentry_init, capture_events): +def test_capture_transaction_on_success_sync(sentry_init, capture_events): sentry_init( integrations=[ StrawberryIntegration(async_execution=False), @@ -293,4 +477,48 @@ def test_no_errors_sync(sentry_init, capture_events): client.post("/graphql", json=query) assert len(events) == 1 - assert False + (transaction_event,) = events + + assert transaction_event["transaction"] == "graphql_view" + assert transaction_event["spans"] + + query_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_QUERY + ] + assert len(query_spans) == 1, "exactly one query span expected" + query_span = query_spans[0] + assert query_span["description"] == "query" + assert query_span["data"]["graphql.operation.type"] == "query" + assert query_span["data"]["graphql.operation.name"] is None + assert query_span["data"]["graphql.document"] == query["query"] + assert query_span["data"]["graphql.resource_name"] + + parse_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE + ] + assert len(parse_spans) == 1, "exactly one parse span expected" + parse_span = parse_spans[0] + assert parse_span["parent_span_id"] == query_span["span_id"] + assert parse_span["description"] == "parsing" + + validate_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE + ] + assert len(validate_spans) == 1, "exactly one validate span expected" + validate_span = validate_spans[0] + assert validate_span["parent_span_id"] == query_span["span_id"] + assert validate_span["description"] == "validation" + + resolve_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE + ] + assert len(resolve_spans) == 1, "exactly one resolve span expected" + resolve_span = resolve_spans[0] + assert resolve_span["parent_span_id"] == query_span["span_id"] + assert resolve_span["description"] == "resolving Query.hello" + assert resolve_span["data"] == { + "graphql.field_name": "hello", + "graphql.parent_type": "Query", + "graphql.field_path": "Query.hello", + "graphql.path": "hello", + } From 0a97dc7c42deb91051a3eb00f6bb1c72ac37563b Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Sep 2023 13:52:56 +0200 Subject: [PATCH 11/37] add breadcrumb --- sentry_sdk/integrations/strawberry.py | 8 ++++ .../strawberry/test_strawberry_py3.py | 37 +++++++++---------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index fcaf58fd69..b697015ac9 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -158,6 +158,14 @@ def on_operation(self): else: description = operation_type + Hub.current.add_breadcrumb( + category="graphql", + data={ + "operation_name": self._operation_name, + "operation_type": operation_type, + }, + ) + with configure_scope() as scope: if scope.span: self.graphql_span = scope.span.start_child( diff --git a/tests/integrations/strawberry/test_strawberry_py3.py b/tests/integrations/strawberry/test_strawberry_py3.py index a8fe6f076a..ece5511f53 100644 --- a/tests/integrations/strawberry/test_strawberry_py3.py +++ b/tests/integrations/strawberry/test_strawberry_py3.py @@ -37,7 +37,7 @@ def hello(self) -> str: return "Hello World" @strawberry.field - def error(self) -> str: + def error(self) -> int: return 1 / 0 @@ -115,7 +115,6 @@ def test_capture_request_if_available_and_send_pii_is_on_async( FastApiIntegration(), StarletteIntegration(), ], - traces_sample_rate=1, ) events = capture_events() @@ -128,9 +127,9 @@ def test_capture_request_if_available_and_send_pii_is_on_async( client = TestClient(async_app) client.post("/graphql", json=query) - assert len(events) == 2 + assert len(events) == 1 - (error_event, _) = events + (error_event,) = events assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" assert error_event["request"]["api_target"] == "graphql" @@ -155,7 +154,6 @@ def test_capture_request_if_available_and_send_pii_is_on_sync( sentry_init( send_default_pii=True, integrations=[StrawberryIntegration(async_execution=False), FlaskIntegration()], - traces_sample_rate=1, ) events = capture_events() @@ -171,9 +169,9 @@ def test_capture_request_if_available_and_send_pii_is_on_sync( client = sync_app.test_client() client.post("/graphql", json=query) - assert len(events) == 2 + assert len(events) == 1 - (error_event, _) = events + (error_event,) = events assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" assert error_event["request"]["api_target"] == "graphql" assert error_event["request"]["data"] == query @@ -198,7 +196,6 @@ def test_do_not_capture_request_if_send_pii_is_off_async(sentry_init, capture_ev FastApiIntegration(), StarletteIntegration(), ], - traces_sample_rate=1, ) events = capture_events() @@ -211,9 +208,9 @@ def test_do_not_capture_request_if_send_pii_is_off_async(sentry_init, capture_ev client = TestClient(async_app) client.post("/graphql", json=query) - assert len(events) == 2 + assert len(events) == 1 - (error_event, _) = events + (error_event,) = events assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" assert "data" not in error_event["request"] assert "response" not in error_event["contexts"] @@ -238,9 +235,9 @@ def test_do_not_capture_request_if_send_pii_is_off_sync(sentry_init, capture_eve client = sync_app.test_client() client.post("/graphql", json=query) - assert len(events) == 2 + assert len(events) == 1 - (error_event, _) = events + (error_event,) = events assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" assert "data" not in error_event["request"] assert "response" not in error_event["contexts"] @@ -278,9 +275,9 @@ def test_capture_transaction_on_error_async(sentry_init, capture_events): ] assert len(query_spans) == 1, "exactly one query span expected" query_span = query_spans[0] - assert query_span["description"] == "query" + assert query_span["description"] == "query ErrorQuery" assert query_span["data"]["graphql.operation.type"] == "query" - assert query_span["data"]["graphql.operation.name"] is None + assert query_span["data"]["graphql.operation.name"] == "ErrorQuery" assert query_span["data"]["graphql.document"] == query["query"] assert query_span["data"]["graphql.resource_name"] @@ -347,9 +344,9 @@ def test_capture_transaction_on_error_sync(sentry_init, capture_events): ] assert len(query_spans) == 1, "exactly one query span expected" query_span = query_spans[0] - assert query_span["description"] == "query" + assert query_span["description"] == "query ErrorQuery" assert query_span["data"]["graphql.operation.type"] == "query" - assert query_span["data"]["graphql.operation.name"] is None + assert query_span["data"]["graphql.operation.name"] == "ErrorQuery" assert query_span["data"]["graphql.document"] == query["query"] assert query_span["data"]["graphql.resource_name"] @@ -415,9 +412,9 @@ def test_capture_transaction_on_success_async(sentry_init, capture_events): ] assert len(query_spans) == 1, "exactly one query span expected" query_span = query_spans[0] - assert query_span["description"] == "query" + assert query_span["description"] == "query GreetingQuery" assert query_span["data"]["graphql.operation.type"] == "query" - assert query_span["data"]["graphql.operation.name"] is None + assert query_span["data"]["graphql.operation.name"] == "GreetingQuery" assert query_span["data"]["graphql.document"] == query["query"] assert query_span["data"]["graphql.resource_name"] @@ -487,9 +484,9 @@ def test_capture_transaction_on_success_sync(sentry_init, capture_events): ] assert len(query_spans) == 1, "exactly one query span expected" query_span = query_spans[0] - assert query_span["description"] == "query" + assert query_span["description"] == "query GreetingQuery" assert query_span["data"]["graphql.operation.type"] == "query" - assert query_span["data"]["graphql.operation.name"] is None + assert query_span["data"]["graphql.operation.name"] == "GreetingQuery" assert query_span["data"]["graphql.document"] == query["query"] assert query_span["data"]["graphql.resource_name"] From cfba1335f99ce50fa928344c488d92d436129e76 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Sep 2023 14:02:28 +0200 Subject: [PATCH 12/37] fix tests --- sentry_sdk/integrations/strawberry.py | 2 +- .../strawberry/test_strawberry_py3.py | 27 +++++++++---------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index b697015ac9..c653f89977 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -159,7 +159,7 @@ def on_operation(self): description = operation_type Hub.current.add_breadcrumb( - category="graphql", + category="graphql.operation", data={ "operation_name": self._operation_name, "operation_type": operation_type, diff --git a/tests/integrations/strawberry/test_strawberry_py3.py b/tests/integrations/strawberry/test_strawberry_py3.py index ece5511f53..18e2e97bf4 100644 --- a/tests/integrations/strawberry/test_strawberry_py3.py +++ b/tests/integrations/strawberry/test_strawberry_py3.py @@ -219,7 +219,6 @@ def test_do_not_capture_request_if_send_pii_is_off_async(sentry_init, capture_ev def test_do_not_capture_request_if_send_pii_is_off_sync(sentry_init, capture_events): sentry_init( integrations=[StrawberryIntegration(async_execution=False), FlaskIntegration()], - traces_sample_rate=1, ) events = capture_events() @@ -260,9 +259,9 @@ def test_capture_transaction_on_error_async(sentry_init, capture_events): async_app = FastAPI() async_app.include_router(GraphQLRouter(schema), prefix="/graphql") - query = {"query": "query ErrorQuery { error }"} + query = "query ErrorQuery { error }" client = TestClient(async_app) - client.post("/graphql", json=query) + client.post("/graphql", json={"query": query, "operationName": "ErrorQuery"}) assert len(events) == 2 (_, transaction_event) = events @@ -278,7 +277,7 @@ def test_capture_transaction_on_error_async(sentry_init, capture_events): assert query_span["description"] == "query ErrorQuery" assert query_span["data"]["graphql.operation.type"] == "query" assert query_span["data"]["graphql.operation.name"] == "ErrorQuery" - assert query_span["data"]["graphql.document"] == query["query"] + assert query_span["data"]["graphql.document"] == query assert query_span["data"]["graphql.resource_name"] parse_spans = [ @@ -328,9 +327,9 @@ def test_capture_transaction_on_error_sync(sentry_init, capture_events): view_func=GraphQLView.as_view("graphql_view", schema=schema), ) - query = {"query": "query ErrorQuery { error }"} + query = "query ErrorQuery { error }" client = sync_app.test_client() - client.post("/graphql", json=query) + client.post("/graphql", json={"query": query, "operationName": "ErrorQuery"}) assert len(events) == 2 @@ -347,7 +346,7 @@ def test_capture_transaction_on_error_sync(sentry_init, capture_events): assert query_span["description"] == "query ErrorQuery" assert query_span["data"]["graphql.operation.type"] == "query" assert query_span["data"]["graphql.operation.name"] == "ErrorQuery" - assert query_span["data"]["graphql.document"] == query["query"] + assert query_span["data"]["graphql.document"] == query assert query_span["data"]["graphql.resource_name"] parse_spans = [ @@ -397,9 +396,9 @@ def test_capture_transaction_on_success_async(sentry_init, capture_events): async_app = FastAPI() async_app.include_router(GraphQLRouter(schema), prefix="/graphql") - query = {"query": "query GreetingQuery { hello }"} + query = "query GreetingQuery { hello }" client = TestClient(async_app) - client.post("/graphql", json=query) + client.post("/graphql", json={"query": query, "operationName": "GreetingQuery"}) assert len(events) == 1 (transaction_event,) = events @@ -415,7 +414,7 @@ def test_capture_transaction_on_success_async(sentry_init, capture_events): assert query_span["description"] == "query GreetingQuery" assert query_span["data"]["graphql.operation.type"] == "query" assert query_span["data"]["graphql.operation.name"] == "GreetingQuery" - assert query_span["data"]["graphql.document"] == query["query"] + assert query_span["data"]["graphql.document"] == query assert query_span["data"]["graphql.resource_name"] parse_spans = [ @@ -467,11 +466,9 @@ def test_capture_transaction_on_success_sync(sentry_init, capture_events): view_func=GraphQLView.as_view("graphql_view", schema=schema), ) - query = { - "query": "query GreetingQuery { hello }", - } + query = "query GreetingQuery { hello }" client = sync_app.test_client() - client.post("/graphql", json=query) + client.post("/graphql", json={"query": query, "operationName": "GreetingQuery"}) assert len(events) == 1 (transaction_event,) = events @@ -487,7 +484,7 @@ def test_capture_transaction_on_success_sync(sentry_init, capture_events): assert query_span["description"] == "query GreetingQuery" assert query_span["data"]["graphql.operation.type"] == "query" assert query_span["data"]["graphql.operation.name"] == "GreetingQuery" - assert query_span["data"]["graphql.document"] == query["query"] + assert query_span["data"]["graphql.document"] == query assert query_span["data"]["graphql.resource_name"] parse_spans = [ From bdbf83ea7e6e8eeadc0f9139182cc7152679cf18 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Sep 2023 14:10:30 +0200 Subject: [PATCH 13/37] more test fixes --- .../strawberry/test_strawberry_py3.py | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/tests/integrations/strawberry/test_strawberry_py3.py b/tests/integrations/strawberry/test_strawberry_py3.py index 18e2e97bf4..1ba310b083 100644 --- a/tests/integrations/strawberry/test_strawberry_py3.py +++ b/tests/integrations/strawberry/test_strawberry_py3.py @@ -123,9 +123,9 @@ def test_capture_request_if_available_and_send_pii_is_on_async( async_app = FastAPI() async_app.include_router(GraphQLRouter(schema), prefix="/graphql") - query = {"query": "query ErrorQuery { error }"} + query = "query ErrorQuery { error }" client = TestClient(async_app) - client.post("/graphql", json=query) + client.post("/graphql", json={"query": query, "operationName": "ErrorQuery"}) assert len(events) == 1 @@ -133,7 +133,10 @@ def test_capture_request_if_available_and_send_pii_is_on_async( assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" assert error_event["request"]["api_target"] == "graphql" - assert error_event["request"]["data"] == query + assert error_event["request"]["data"] == { + "query": query, + "operationName": "ErrorQuery", + } assert error_event["contexts"]["response"] == { "data": { "data": None, @@ -146,6 +149,12 @@ def test_capture_request_if_available_and_send_pii_is_on_async( ], } } + assert len(error_event["breadcrumbs"]["values"]) == 1 + assert error_event["breadcrumbs"]["values"][0]["category"] == "graphql.operation" + assert error_event["breadcrumbs"]["values"][0]["data"] == { + "operation_name": "ErrorQuery", + "operation_type": "query", + } def test_capture_request_if_available_and_send_pii_is_on_sync( @@ -165,16 +174,19 @@ def test_capture_request_if_available_and_send_pii_is_on_sync( view_func=GraphQLView.as_view("graphql_view", schema=schema), ) - query = {"query": "query ErrorQuery { error }"} + query = "query ErrorQuery { error }" client = sync_app.test_client() - client.post("/graphql", json=query) + client.post("/graphql", json={"query": query, "operationName": "ErrorQuery"}) assert len(events) == 1 (error_event,) = events assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" assert error_event["request"]["api_target"] == "graphql" - assert error_event["request"]["data"] == query + assert error_event["request"]["data"] == { + "query": query, + "operationName": "ErrorQuery", + } assert error_event["contexts"]["response"] == { "data": { "data": None, @@ -187,6 +199,12 @@ def test_capture_request_if_available_and_send_pii_is_on_sync( ], } } + assert len(error_event["breadcrumbs"]["values"]) == 1 + assert error_event["breadcrumbs"]["values"][0]["category"] == "graphql.operation" + assert error_event["breadcrumbs"]["values"][0]["data"] == { + "operation_name": "ErrorQuery", + "operation_type": "query", + } def test_do_not_capture_request_if_send_pii_is_off_async(sentry_init, capture_events): @@ -204,9 +222,9 @@ def test_do_not_capture_request_if_send_pii_is_off_async(sentry_init, capture_ev async_app = FastAPI() async_app.include_router(GraphQLRouter(schema), prefix="/graphql") - query = {"query": "query ErrorQuery { error }"} + query = "query ErrorQuery { error }" client = TestClient(async_app) - client.post("/graphql", json=query) + client.post("/graphql", json={"query": query, "operationName": "ErrorQuery"}) assert len(events) == 1 @@ -215,6 +233,13 @@ def test_do_not_capture_request_if_send_pii_is_off_async(sentry_init, capture_ev assert "data" not in error_event["request"] assert "response" not in error_event["contexts"] + assert len(error_event["breadcrumbs"]["values"]) == 1 + assert error_event["breadcrumbs"]["values"][0]["category"] == "graphql.operation" + assert error_event["breadcrumbs"]["values"][0]["data"] == { + "operation_name": "ErrorQuery", + "operation_type": "query", + } + def test_do_not_capture_request_if_send_pii_is_off_sync(sentry_init, capture_events): sentry_init( @@ -230,9 +255,9 @@ def test_do_not_capture_request_if_send_pii_is_off_sync(sentry_init, capture_eve view_func=GraphQLView.as_view("graphql_view", schema=schema), ) - query = {"query": "query ErrorQuery { error }"} + query = "query ErrorQuery { error }" client = sync_app.test_client() - client.post("/graphql", json=query) + client.post("/graphql", json={"query": query, "operationName": "ErrorQuery"}) assert len(events) == 1 @@ -241,6 +266,13 @@ def test_do_not_capture_request_if_send_pii_is_off_sync(sentry_init, capture_eve assert "data" not in error_event["request"] assert "response" not in error_event["contexts"] + assert len(error_event["breadcrumbs"]["values"]) == 1 + assert error_event["breadcrumbs"]["values"][0]["category"] == "graphql.operation" + assert error_event["breadcrumbs"]["values"][0]["data"] == { + "operation_name": "ErrorQuery", + "operation_type": "query", + } + def test_capture_transaction_on_error_async(sentry_init, capture_events): sentry_init( From 6e0830837facd8956ad385cec083a80ac4dd1058 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Sep 2023 14:17:03 +0200 Subject: [PATCH 14/37] some mypy fixes --- sentry_sdk/integrations/strawberry.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index c653f89977..26c5618baf 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -16,24 +16,25 @@ from sentry_sdk._types import TYPE_CHECKING try: - import strawberry.http as strawberry_http - import strawberry.schema.schema as strawberry_schema - from strawberry import Schema - from strawberry.extensions import SchemaExtension - from strawberry.extensions.tracing.utils import should_skip_tracing - from strawberry.extensions.tracing import ( + import strawberry.http as strawberry_http # type: ignore + import strawberry.schema.schema as strawberry_schema # type: ignore + from strawberry import Schema # type: ignore + from strawberry.extensions import SchemaExtension # type: ignore + from strawberry.extensions.tracing.utils import should_skip_tracing # type: ignore + from strawberry.extensions.tracing import ( # type: ignore SentryTracingExtension as StrawberrySentryAsyncExtension, SentryTracingExtensionSync as StrawberrySentrySyncExtension, ) - from strawberry.fastapi import router as fastapi_router - from strawberry.http import async_base_view, sync_base_view + from strawberry.fastapi import router as fastapi_router # type: ignore + from strawberry.http import async_base_view, sync_base_view # type: ignore except ImportError: raise DidNotEnable("strawberry-graphql is not installed") if TYPE_CHECKING: from typing import Any, Callable, Dict, Generator, Optional - from graphql import GraphQLResolveInfo - from strawberry.types.execution import ExecutionContext + from graphql import GraphQLResolveInfo # type: ignore + from strawberry.http import GraphQLHTTPResponse # type: ignore + from strawberry.types import ExecutionContext, ExecutionResult # type: ignore from sentry_sdk._types import EventProcessor @@ -77,6 +78,7 @@ def _patch_schema_init(): old_schema_init = Schema.__init__ def _sentry_patched_schema_init(self, *args, **kwargs): + # type: (Any, Any, Any) -> None integration = Hub.current.get_integration(StrawberryIntegration) if integration is None: return old_schema_init(self, *args, **kwargs) @@ -260,6 +262,7 @@ def _patch_execute(): old_execute_sync = strawberry_schema.execute_sync async def _sentry_patched_execute_async(*args, **kwargs): + # type: (Any, Any) -> ExecutionResult hub = Hub.current integration = hub.get_integration(StrawberryIntegration) if integration is None: @@ -277,6 +280,7 @@ async def _sentry_patched_execute_async(*args, **kwargs): return result def _sentry_patched_execute_sync(*args, **kwargs): + # type: (Any, Any) -> ExecutionResult hub = Hub.current integration = hub.get_integration(StrawberryIntegration) if integration is None: @@ -302,6 +306,7 @@ def _patch_process_result(): old_process_result = strawberry_http.process_result def _sentry_patched_process_result(result, *args, **kwargs): + # type: (ExecutionResult, Any, Any) -> GraphQLHTTPResponse hub = Hub.current integration = hub.get_integration(StrawberryIntegration) if integration is None: From 5894c6c4158a724d2b5dad00d9705d55347e368c Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Sep 2023 14:28:39 +0200 Subject: [PATCH 15/37] more mypy fixes --- sentry_sdk/integrations/strawberry.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 26c5618baf..d495a0c7c6 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -18,22 +18,22 @@ try: import strawberry.http as strawberry_http # type: ignore import strawberry.schema.schema as strawberry_schema # type: ignore - from strawberry import Schema # type: ignore - from strawberry.extensions import SchemaExtension # type: ignore + from strawberry import Schema + from strawberry.extensions import SchemaExtension from strawberry.extensions.tracing.utils import should_skip_tracing # type: ignore from strawberry.extensions.tracing import ( # type: ignore SentryTracingExtension as StrawberrySentryAsyncExtension, SentryTracingExtensionSync as StrawberrySentrySyncExtension, ) from strawberry.fastapi import router as fastapi_router # type: ignore - from strawberry.http import async_base_view, sync_base_view # type: ignore + from strawberry.http import async_base_view, sync_base_view except ImportError: raise DidNotEnable("strawberry-graphql is not installed") if TYPE_CHECKING: - from typing import Any, Callable, Dict, Generator, Optional + from typing import Any, Callable, Dict, Generator, List, Optional from graphql import GraphQLResolveInfo # type: ignore - from strawberry.http import GraphQLHTTPResponse # type: ignore + from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionContext, ExecutionResult # type: ignore from sentry_sdk._types import EventProcessor @@ -368,7 +368,7 @@ def inner(event, hint): def _make_response_event_processor(data): - # type: (Dict[str, Any]) -> EventProcessor + # type: (GraphQLHTTPResponse) -> EventProcessor def inner(event, hint): # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] @@ -383,7 +383,7 @@ def inner(event, hint): def _guess_if_using_async(extensions): - # (List[SchemaExtension]) -> bool + # type: (List[SchemaExtension]) -> bool if StrawberrySentryAsyncExtension in extensions: return True elif StrawberrySentrySyncExtension in extensions: From 30fd7792719951023a01c6eab91304ef51ac3a29 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Sep 2023 14:32:54 +0200 Subject: [PATCH 16/37] more mypy --- sentry_sdk/integrations/strawberry.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index d495a0c7c6..c0516fb727 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -19,7 +19,7 @@ import strawberry.http as strawberry_http # type: ignore import strawberry.schema.schema as strawberry_schema # type: ignore from strawberry import Schema - from strawberry.extensions import SchemaExtension + from strawberry.extensions import SchemaExtension # type: ignore from strawberry.extensions.tracing.utils import should_skip_tracing # type: ignore from strawberry.extensions.tracing import ( # type: ignore SentryTracingExtension as StrawberrySentryAsyncExtension, @@ -117,7 +117,7 @@ def _sentry_patched_schema_init(self, *args, **kwargs): Schema.__init__ = _sentry_patched_schema_init -class SentryAsyncExtension(SchemaExtension): +class SentryAsyncExtension(SchemaExtension): # type: ignore def __init__( self, *, @@ -206,11 +206,11 @@ def on_parse(self): self.parsing_span.finish() def should_skip_tracing(self, _next, info): - # type: (Callable, GraphQLResolveInfo) -> bool + # type: (Callable[[Any, GraphQLResolveInfo, Any], Any], GraphQLResolveInfo) -> bool return should_skip_tracing(_next, info) async def resolve(self, _next, root, info, *args, **kwargs): - # type: (Callable, Any, GraphQLResolveInfo, str, Any) -> Any + # type: (Callable[[Any, GraphQLResolveInfo, Any], Any], Any, GraphQLResolveInfo, str, Any) -> Any if self.should_skip_tracing(_next, info): result = _next(root, info, *args, **kwargs) @@ -239,7 +239,7 @@ async def resolve(self, _next, root, info, *args, **kwargs): class SentrySyncExtension(SentryAsyncExtension): def resolve(self, _next, root, info, *args, **kwargs): - # type: (Callable, Any, GraphQLResolveInfo, str, Any) -> Any + # type: (Callable[[Any, GraphQLResolveInfo, Any, Any], Any], Any, GraphQLResolveInfo, str, Any) -> Any if self.should_skip_tracing(_next, info): return _next(root, info, *args, **kwargs) From 5d251c2e9ec07f8aac85f6b2de769c000ef50858 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Sep 2023 14:46:10 +0200 Subject: [PATCH 17/37] mypy? --- sentry_sdk/integrations/strawberry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index c0516fb727..79465b4aa6 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -210,7 +210,7 @@ def should_skip_tracing(self, _next, info): return should_skip_tracing(_next, info) async def resolve(self, _next, root, info, *args, **kwargs): - # type: (Callable[[Any, GraphQLResolveInfo, Any], Any], Any, GraphQLResolveInfo, str, Any) -> Any + # type: (Callable[[Any, GraphQLResolveInfo, str, Any], Any], Any, GraphQLResolveInfo, str, Any) -> Any if self.should_skip_tracing(_next, info): result = _next(root, info, *args, **kwargs) @@ -239,7 +239,7 @@ async def resolve(self, _next, root, info, *args, **kwargs): class SentrySyncExtension(SentryAsyncExtension): def resolve(self, _next, root, info, *args, **kwargs): - # type: (Callable[[Any, GraphQLResolveInfo, Any, Any], Any], Any, GraphQLResolveInfo, str, Any) -> Any + # type: (Callable[[Any, Any, str, Any], Any], Any, GraphQLResolveInfo, str, Any) -> Any if self.should_skip_tracing(_next, info): return _next(root, info, *args, **kwargs) From e4a868f756b1b5eeda6973af70532cc4132ff3cf Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Sep 2023 14:48:34 +0200 Subject: [PATCH 18/37] mypy? --- sentry_sdk/integrations/strawberry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 79465b4aa6..9eff4e6346 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -210,7 +210,7 @@ def should_skip_tracing(self, _next, info): return should_skip_tracing(_next, info) async def resolve(self, _next, root, info, *args, **kwargs): - # type: (Callable[[Any, GraphQLResolveInfo, str, Any], Any], Any, GraphQLResolveInfo, str, Any) -> Any + # type: (Callable[[Any, GraphQLResolveInfo, Any, Any], Any], Any, GraphQLResolveInfo, str, Any) -> Any if self.should_skip_tracing(_next, info): result = _next(root, info, *args, **kwargs) @@ -239,7 +239,7 @@ async def resolve(self, _next, root, info, *args, **kwargs): class SentrySyncExtension(SentryAsyncExtension): def resolve(self, _next, root, info, *args, **kwargs): - # type: (Callable[[Any, Any, str, Any], Any], Any, GraphQLResolveInfo, str, Any) -> Any + # 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) From e2b32226b4e427772651a72573f5d8b643e2e238 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Sep 2023 14:51:21 +0200 Subject: [PATCH 19/37] mypy pls --- sentry_sdk/integrations/strawberry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 9eff4e6346..bdd461a9f9 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -206,7 +206,7 @@ def on_parse(self): self.parsing_span.finish() def should_skip_tracing(self, _next, info): - # type: (Callable[[Any, GraphQLResolveInfo, Any], Any], GraphQLResolveInfo) -> bool + # type: (Callable[[Any, GraphQLResolveInfo, Any, Any], Any], GraphQLResolveInfo) -> bool return should_skip_tracing(_next, info) async def resolve(self, _next, root, info, *args, **kwargs): From e7e9677e1accf1716b78e0f546252617a118a389 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Sep 2023 15:27:42 +0200 Subject: [PATCH 20/37] more tests --- .../strawberry/test_strawberry_py3.py | 344 ++++++++++++++++++ 1 file changed, 344 insertions(+) diff --git a/tests/integrations/strawberry/test_strawberry_py3.py b/tests/integrations/strawberry/test_strawberry_py3.py index 1ba310b083..db361ff73f 100644 --- a/tests/integrations/strawberry/test_strawberry_py3.py +++ b/tests/integrations/strawberry/test_strawberry_py3.py @@ -41,6 +41,13 @@ def error(self) -> int: return 1 / 0 +@strawberry.type +class Mutation: + @strawberry.mutation + def change(self, attribute: str) -> str: + return attribute + + def test_async_execution_uses_async_extension(sentry_init): sentry_init(integrations=[StrawberryIntegration(async_execution=True)]) @@ -274,6 +281,67 @@ def test_do_not_capture_request_if_send_pii_is_off_sync(sentry_init, capture_eve } +def test_breadcrumb_no_operation_name_async(sentry_init, capture_events): + sentry_init( + integrations=[ + StrawberryIntegration(), + FastApiIntegration(), + StarletteIntegration(), + ], + ) + events = capture_events() + + schema = strawberry.Schema(Query) + + async_app = FastAPI() + async_app.include_router(GraphQLRouter(schema), prefix="/graphql") + + query = "{ error }" + client = TestClient(async_app) + client.post("/graphql", json={"query": query}) + + assert len(events) == 1 + + (error_event,) = events + + assert len(error_event["breadcrumbs"]["values"]) == 1 + assert error_event["breadcrumbs"]["values"][0]["category"] == "graphql.operation" + assert error_event["breadcrumbs"]["values"][0]["data"] == { + "operation_name": None, + "operation_type": "query", + } + + +def test_breadcrumb_no_operation_name_sync(sentry_init, capture_events): + sentry_init( + integrations=[StrawberryIntegration(async_execution=False), FlaskIntegration()], + ) + events = capture_events() + + schema = strawberry.Schema(Query) + + sync_app = Flask(__name__) + sync_app.add_url_rule( + "/graphql", + view_func=GraphQLView.as_view("graphql_view", schema=schema), + ) + + query = "{ error }" + client = sync_app.test_client() + client.post("/graphql", json={"query": query}) + + assert len(events) == 1 + + (error_event,) = events + + assert len(error_event["breadcrumbs"]["values"]) == 1 + assert error_event["breadcrumbs"]["values"][0]["category"] == "graphql.operation" + assert error_event["breadcrumbs"]["values"][0]["data"] == { + "operation_name": None, + "operation_type": "query", + } + + def test_capture_transaction_on_error_async(sentry_init, capture_events): sentry_init( send_default_pii=True, @@ -548,3 +616,279 @@ def test_capture_transaction_on_success_sync(sentry_init, capture_events): "graphql.field_path": "Query.hello", "graphql.path": "hello", } + + +def test_transaction_no_operation_name_async(sentry_init, capture_events): + sentry_init( + integrations=[ + StrawberryIntegration(), + FastApiIntegration(), + StarletteIntegration(), + ], + traces_sample_rate=1, + ) + events = capture_events() + + schema = strawberry.Schema(Query) + + async_app = FastAPI() + async_app.include_router(GraphQLRouter(schema), prefix="/graphql") + + query = "{ hello }" + client = TestClient(async_app) + client.post("/graphql", json={"query": query}) + + assert len(events) == 1 + (transaction_event,) = events + + assert transaction_event["transaction"] == "/graphql" + assert transaction_event["spans"] + + query_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_QUERY + ] + assert len(query_spans) == 1, "exactly one query span expected" + query_span = query_spans[0] + assert query_span["description"] == "query" + assert query_span["data"]["graphql.operation.type"] == "query" + assert query_span["data"]["graphql.operation.name"] is None + assert query_span["data"]["graphql.document"] == query + assert query_span["data"]["graphql.resource_name"] + + parse_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE + ] + assert len(parse_spans) == 1, "exactly one parse span expected" + parse_span = parse_spans[0] + assert parse_span["parent_span_id"] == query_span["span_id"] + assert parse_span["description"] == "parsing" + + validate_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE + ] + assert len(validate_spans) == 1, "exactly one validate span expected" + validate_span = validate_spans[0] + assert validate_span["parent_span_id"] == query_span["span_id"] + assert validate_span["description"] == "validation" + + resolve_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE + ] + assert len(resolve_spans) == 1, "exactly one resolve span expected" + resolve_span = resolve_spans[0] + assert resolve_span["parent_span_id"] == query_span["span_id"] + assert resolve_span["description"] == "resolving Query.hello" + assert resolve_span["data"] == { + "graphql.field_name": "hello", + "graphql.parent_type": "Query", + "graphql.field_path": "Query.hello", + "graphql.path": "hello", + } + + +def test_transaction_no_operation_name_sync(sentry_init, capture_events): + sentry_init( + integrations=[ + StrawberryIntegration(async_execution=False), + FlaskIntegration(), + ], + traces_sample_rate=1, + ) + events = capture_events() + + schema = strawberry.Schema(Query) + + sync_app = Flask(__name__) + sync_app.add_url_rule( + "/graphql", + view_func=GraphQLView.as_view("graphql_view", schema=schema), + ) + + query = "{ hello }" + client = sync_app.test_client() + client.post("/graphql", json={"query": query}) + + assert len(events) == 1 + (transaction_event,) = events + + assert transaction_event["transaction"] == "graphql_view" + assert transaction_event["spans"] + + query_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_QUERY + ] + assert len(query_spans) == 1, "exactly one query span expected" + query_span = query_spans[0] + assert query_span["description"] == "query" + assert query_span["data"]["graphql.operation.type"] == "query" + assert query_span["data"]["graphql.operation.name"] is None + assert query_span["data"]["graphql.document"] == query + assert query_span["data"]["graphql.resource_name"] + + parse_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE + ] + assert len(parse_spans) == 1, "exactly one parse span expected" + parse_span = parse_spans[0] + assert parse_span["parent_span_id"] == query_span["span_id"] + assert parse_span["description"] == "parsing" + + validate_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE + ] + assert len(validate_spans) == 1, "exactly one validate span expected" + validate_span = validate_spans[0] + assert validate_span["parent_span_id"] == query_span["span_id"] + assert validate_span["description"] == "validation" + + resolve_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE + ] + assert len(resolve_spans) == 1, "exactly one resolve span expected" + resolve_span = resolve_spans[0] + assert resolve_span["parent_span_id"] == query_span["span_id"] + assert resolve_span["description"] == "resolving Query.hello" + assert resolve_span["data"] == { + "graphql.field_name": "hello", + "graphql.parent_type": "Query", + "graphql.field_path": "Query.hello", + "graphql.path": "hello", + } + + +def test_transaction_mutation_async(sentry_init, capture_events): + sentry_init( + integrations=[ + StrawberryIntegration(), + FastApiIntegration(), + StarletteIntegration(), + ], + traces_sample_rate=1, + ) + events = capture_events() + + schema = strawberry.Schema(Query, mutation=Mutation) + + async_app = FastAPI() + async_app.include_router(GraphQLRouter(schema), prefix="/graphql") + + query = 'mutation Change { change(attribute: "something") }' + client = TestClient(async_app) + client.post("/graphql", json={"query": query}) + + assert len(events) == 1 + (transaction_event,) = events + + assert transaction_event["transaction"] == "/graphql" + assert transaction_event["spans"] + + query_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_MUTATION + ] + assert len(query_spans) == 1, "exactly one mutation span expected" + query_span = query_spans[0] + assert query_span["description"] == "mutation" + assert query_span["data"]["graphql.operation.type"] == "mutation" + assert query_span["data"]["graphql.operation.name"] is None + assert query_span["data"]["graphql.document"] == query + assert query_span["data"]["graphql.resource_name"] + + parse_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE + ] + assert len(parse_spans) == 1, "exactly one parse span expected" + parse_span = parse_spans[0] + assert parse_span["parent_span_id"] == query_span["span_id"] + assert parse_span["description"] == "parsing" + + validate_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE + ] + assert len(validate_spans) == 1, "exactly one validate span expected" + validate_span = validate_spans[0] + assert validate_span["parent_span_id"] == query_span["span_id"] + assert validate_span["description"] == "validation" + + resolve_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE + ] + assert len(resolve_spans) == 1, "exactly one resolve span expected" + resolve_span = resolve_spans[0] + assert resolve_span["parent_span_id"] == query_span["span_id"] + assert resolve_span["description"] == "resolving Mutation.change" + assert resolve_span["data"] == { + "graphql.field_name": "change", + "graphql.parent_type": "Mutation", + "graphql.field_path": "Mutation.change", + "graphql.path": "change", + } + + +def test_transaction_mutation_sync(sentry_init, capture_events): + sentry_init( + integrations=[ + StrawberryIntegration(async_execution=False), + FlaskIntegration(), + ], + traces_sample_rate=1, + ) + events = capture_events() + + schema = strawberry.Schema(Query, mutation=Mutation) + + sync_app = Flask(__name__) + sync_app.add_url_rule( + "/graphql", + view_func=GraphQLView.as_view("graphql_view", schema=schema), + ) + + query = 'mutation Change { change(attribute: "something") }' + client = sync_app.test_client() + client.post("/graphql", json={"query": query}) + + assert len(events) == 1 + (transaction_event,) = events + + assert transaction_event["transaction"] == "graphql_view" + assert transaction_event["spans"] + + query_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_MUTATION + ] + assert len(query_spans) == 1, "exactly one mutation span expected" + query_span = query_spans[0] + assert query_span["description"] == "mutation" + assert query_span["data"]["graphql.operation.type"] == "mutation" + assert query_span["data"]["graphql.operation.name"] is None + assert query_span["data"]["graphql.document"] == query + assert query_span["data"]["graphql.resource_name"] + + parse_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE + ] + assert len(parse_spans) == 1, "exactly one parse span expected" + parse_span = parse_spans[0] + assert parse_span["parent_span_id"] == query_span["span_id"] + assert parse_span["description"] == "parsing" + + validate_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE + ] + assert len(validate_spans) == 1, "exactly one validate span expected" + validate_span = validate_spans[0] + assert validate_span["parent_span_id"] == query_span["span_id"] + assert validate_span["description"] == "validation" + + resolve_spans = [ + span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE + ] + assert len(resolve_spans) == 1, "exactly one resolve span expected" + resolve_span = resolve_spans[0] + assert resolve_span["parent_span_id"] == query_span["span_id"] + assert resolve_span["description"] == "resolving Mutation.change" + assert resolve_span["data"] == { + "graphql.field_name": "change", + "graphql.parent_type": "Mutation", + "graphql.field_path": "Mutation.change", + "graphql.path": "change", + } From 9787172e9b700daa652fd3c63b3f7aabeadf3b08 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 27 Sep 2023 15:35:43 +0200 Subject: [PATCH 21/37] remove comment --- tests/integrations/strawberry/test_strawberry_py3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/strawberry/test_strawberry_py3.py b/tests/integrations/strawberry/test_strawberry_py3.py index db361ff73f..dcbc569913 100644 --- a/tests/integrations/strawberry/test_strawberry_py3.py +++ b/tests/integrations/strawberry/test_strawberry_py3.py @@ -7,7 +7,7 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from flask import Flask -from strawberry.extensions.tracing import ( # XXX conditional on strawberry version +from strawberry.extensions.tracing import ( SentryTracingExtension, SentryTracingExtensionSync, ) From a1a7a37c7d973e2c3b9dc9d7b9e2d9e8617a6393 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 3 Oct 2023 11:03:43 +0200 Subject: [PATCH 22/37] use new strawberry hook --- sentry_sdk/integrations/strawberry.py | 45 +++++++++++---------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index bdd461a9f9..f3b1f3de23 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -16,7 +16,6 @@ from sentry_sdk._types import TYPE_CHECKING try: - import strawberry.http as strawberry_http # type: ignore import strawberry.schema.schema as strawberry_schema # type: ignore from strawberry import Schema from strawberry.extensions import SchemaExtension # type: ignore @@ -25,14 +24,13 @@ SentryTracingExtension as StrawberrySentryAsyncExtension, SentryTracingExtensionSync as StrawberrySentrySyncExtension, ) - from strawberry.fastapi import router as fastapi_router # type: ignore from strawberry.http import async_base_view, sync_base_view except ImportError: raise DidNotEnable("strawberry-graphql is not installed") if TYPE_CHECKING: from typing import Any, Callable, Dict, Generator, List, Optional - from graphql import GraphQLResolveInfo # type: ignore + from graphql import GraphQLError, GraphQLResolveInfo # type: ignore from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionContext, ExecutionResult # type: ignore from sentry_sdk._types import EventProcessor @@ -65,12 +63,12 @@ def setup_once(): "Unparsable strawberry-graphql version: {}".format(version) ) - if version < (0, 208): - raise DidNotEnable("strawberry-graphql 0.208 or newer required.") + if version < (0, 209, 3): + raise DidNotEnable("strawberry-graphql 0.209.3 or newer required.") _patch_schema_init() _patch_execute() - _patch_process_result() + _patch_views() def _patch_schema_init(): @@ -301,26 +299,23 @@ def _sentry_patched_execute_sync(*args, **kwargs): strawberry_schema.execute_sync = _sentry_patched_execute_sync -def _patch_process_result(): - # type: () -> None - old_process_result = strawberry_http.process_result - - def _sentry_patched_process_result(result, *args, **kwargs): - # type: (ExecutionResult, Any, Any) -> GraphQLHTTPResponse +def _patch_views(): + def _sentry_patched_handle_errors(self, errors, response_data): + # type: (Any, List[GraphQLError], GraphQLHTTPResponse) -> None hub = Hub.current integration = hub.get_integration(StrawberryIntegration) if integration is None: - return old_process_result(result, *args, **kwargs) + return - processed_result = old_process_result(result, *args, **kwargs) + if not errors: + return - if result.errors: - with hub.configure_scope() as scope: - event_processor = _make_response_event_processor(processed_result) - scope.add_event_processor(event_processor) + with hub.configure_scope() as scope: + event_processor = _make_response_event_processor(response_data) + scope.add_event_processor(event_processor) with capture_internal_exceptions(): - for error in result.errors or []: + for error in errors: event, hint = event_from_exception( error, client_options=hub.client.options if hub.client else None, @@ -331,12 +326,8 @@ def _sentry_patched_process_result(result, *args, **kwargs): ) hub.capture_event(event, hint=hint) - return processed_result - - strawberry_http.process_result = _sentry_patched_process_result - async_base_view.process_result = _sentry_patched_process_result - sync_base_view.process_result = _sentry_patched_process_result - fastapi_router.process_result = _sentry_patched_process_result + async_base_view.AsyncBaseHTTPView._handle_errors = _sentry_patched_handle_errors + sync_base_view.SyncBaseHTTPView._handle_errors = _sentry_patched_handle_errors def _make_request_event_processor(execution_context): @@ -367,7 +358,7 @@ def inner(event, hint): return inner -def _make_response_event_processor(data): +def _make_response_event_processor(response_data): # type: (GraphQLHTTPResponse) -> EventProcessor def inner(event, hint): @@ -375,7 +366,7 @@ def inner(event, hint): with capture_internal_exceptions(): if _should_send_default_pii(): contexts = event.setdefault("contexts", {}) - contexts["response"] = {"data": data} + contexts["response"] = {"data": response_data} return event From ea69abe0d0ca1c7ae8bdf626d1738fbbe50da8f0 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 3 Oct 2023 12:41:13 +0200 Subject: [PATCH 23/37] temp: test with strawberry prerelease --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 07fd7e4032..9f6f264429 100644 --- a/tox.ini +++ b/tox.ini @@ -468,7 +468,7 @@ deps = sqlalchemy-v2.0: sqlalchemy>=2.0,<2.1 # Strawberry - strawberry: strawberry-graphql[fastapi,flask]>=0.208 + strawberry: strawberry-graphql[fastapi,flask]==0.209.3.dev.1696259772 strawberry: fastapi strawberry: flask strawberry: httpx From 6ca71836b7b7936d93a33b85bd1ded9c839f6c59 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 3 Oct 2023 12:59:43 +0200 Subject: [PATCH 24/37] mypy fixes --- sentry_sdk/integrations/strawberry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index f3b1f3de23..257a836096 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -24,7 +24,7 @@ SentryTracingExtension as StrawberrySentryAsyncExtension, SentryTracingExtensionSync as StrawberrySentrySyncExtension, ) - from strawberry.http import async_base_view, sync_base_view + from strawberry.http import async_base_view, sync_base_view # type: ignore except ImportError: raise DidNotEnable("strawberry-graphql is not installed") @@ -300,6 +300,7 @@ def _sentry_patched_execute_sync(*args, **kwargs): def _patch_views(): + # type: () -> None def _sentry_patched_handle_errors(self, errors, response_data): # type: (Any, List[GraphQLError], GraphQLHTTPResponse) -> None hub = Hub.current From d9863ecf783d4113d95b099baaa54fedfb6effd0 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 3 Oct 2023 14:28:05 +0200 Subject: [PATCH 25/37] update strawberry version --- sentry_sdk/integrations/strawberry.py | 4 ++-- tox.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 257a836096..2c0b416333 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -63,8 +63,8 @@ def setup_once(): "Unparsable strawberry-graphql version: {}".format(version) ) - if version < (0, 209, 3): - raise DidNotEnable("strawberry-graphql 0.209.3 or newer required.") + if version < (0, 209, 5): + raise DidNotEnable("strawberry-graphql 0.209.5 or newer required.") _patch_schema_init() _patch_execute() diff --git a/tox.ini b/tox.ini index 104974d1ff..a37cc890dd 100644 --- a/tox.ini +++ b/tox.ini @@ -487,7 +487,7 @@ deps = sqlalchemy-v2.0: sqlalchemy>=2.0,<2.1 # Strawberry - strawberry: strawberry-graphql[fastapi,flask]==0.209.3.dev.1696259772 + strawberry: strawberry-graphql[fastapi,flask] strawberry: fastapi strawberry: flask strawberry: httpx From 65a2bceb50f340d754d174dc01b6325094025320 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 3 Oct 2023 14:34:30 +0200 Subject: [PATCH 26/37] dont override the hook completely --- sentry_sdk/integrations/strawberry.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 2c0b416333..4977c6ce2a 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -301,6 +301,19 @@ def _sentry_patched_execute_sync(*args, **kwargs): def _patch_views(): # type: () -> None + old_handle_errors_async = async_base_view.AsyncBaseHTTPView._handle_errors + old_handle_errors_sync = sync_base_view.SyncBaseHTTPView._handle_errors + + def _sentry_patched_handle_errors_async(self, errors, response_data): + # type: (Any, List[GraphQLError], GraphQLHTTPResponse) -> None + old_handle_errors_async(self, errors, response_data) + _sentry_patched_handle_errors(self, errors, response_data) + + def _sentry_patched_handle_errors_sync(self, errors, response_data): + # type: (Any, List[GraphQLError], GraphQLHTTPResponse) -> None + old_handle_errors_sync(self, errors, response_data) + _sentry_patched_handle_errors(self, errors, response_data) + def _sentry_patched_handle_errors(self, errors, response_data): # type: (Any, List[GraphQLError], GraphQLHTTPResponse) -> None hub = Hub.current @@ -327,8 +340,10 @@ def _sentry_patched_handle_errors(self, errors, response_data): ) hub.capture_event(event, hint=hint) - async_base_view.AsyncBaseHTTPView._handle_errors = _sentry_patched_handle_errors - sync_base_view.SyncBaseHTTPView._handle_errors = _sentry_patched_handle_errors + async_base_view.AsyncBaseHTTPView._handle_errors = ( + _sentry_patched_handle_errors_async + ) + sync_base_view.SyncBaseHTTPView._handle_errors = _sentry_patched_handle_errors_sync def _make_request_event_processor(execution_context): From 5c03e8fc76630500e68ac6d75117065f6573917c Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 10 Oct 2023 09:43:56 +0200 Subject: [PATCH 27/37] Update sentry_sdk/integrations/strawberry.py Co-authored-by: Daniel Szoke --- sentry_sdk/integrations/strawberry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 4977c6ce2a..ac2fc8753a 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -149,7 +149,7 @@ def on_operation(self): if self.execution_context.query.strip().startswith("mutation"): operation_type = "mutation" op = OP.GRAPHQL_MUTATION - if self.execution_context.query.strip().startswith("subscription"): + elif self.execution_context.query.strip().startswith("subscription"): operation_type = "subscription" op = OP.GRAPHQL_SUBSCRIPTION From ebed8171b76dd423557e94e0b6c117a340206d99 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 10 Oct 2023 09:44:55 +0200 Subject: [PATCH 28/37] Update sentry_sdk/integrations/strawberry.py Co-authored-by: Daniel Szoke --- sentry_sdk/integrations/strawberry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index ac2fc8753a..e098af861f 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -76,7 +76,7 @@ def _patch_schema_init(): old_schema_init = Schema.__init__ def _sentry_patched_schema_init(self, *args, **kwargs): - # type: (Any, Any, Any) -> None + # type: (Schema, Any, Any) -> None integration = Hub.current.get_integration(StrawberryIntegration) if integration is None: return old_schema_init(self, *args, **kwargs) From f862b3c001d39afe2a612c4ec6b56b837be6bf39 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 10 Oct 2023 09:51:41 +0200 Subject: [PATCH 29/37] Update sentry_sdk/integrations/strawberry.py Co-authored-by: Daniel Szoke --- sentry_sdk/integrations/strawberry.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index e098af861f..a19f2631d2 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -153,10 +153,9 @@ def on_operation(self): operation_type = "subscription" op = OP.GRAPHQL_SUBSCRIPTION + description = operation_type if self._operation_name: - description = "{} {}".format(operation_type, self._operation_name) - else: - description = operation_type + description += " {}".format(self._operation_name) Hub.current.add_breadcrumb( category="graphql.operation", From 6d9f35d5610ee3dea3a9f5096fa72cf1ffc5114d Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 10 Oct 2023 10:12:25 +0200 Subject: [PATCH 30/37] review feedback, round 1 --- sentry_sdk/integrations/strawberry.py | 44 +++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index a19f2631d2..95df1b25e0 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -19,7 +19,7 @@ import strawberry.schema.schema as strawberry_schema # type: ignore from strawberry import Schema from strawberry.extensions import SchemaExtension # type: ignore - from strawberry.extensions.tracing.utils import should_skip_tracing # type: ignore + from strawberry.extensions.tracing.utils import should_skip_tracing as strawberry_should_skip_tracing # type: ignore from strawberry.extensions.tracing import ( # type: ignore SentryTracingExtension as StrawberrySentryAsyncExtension, SentryTracingExtensionSync as StrawberrySentrySyncExtension, @@ -204,17 +204,20 @@ def on_parse(self): def should_skip_tracing(self, _next, info): # type: (Callable[[Any, GraphQLResolveInfo, Any, Any], Any], GraphQLResolveInfo) -> bool - return should_skip_tracing(_next, info) + return strawberry_should_skip_tracing(_next, info) + + async def _resolve(self, _next, root, info, *args, **kwargs): + 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): - result = _next(root, info, *args, **kwargs) - - if isawaitable(result): - result = await result - - return result + return await self._resolve(_next, root, info, *args, **kwargs) field_path = "{}.{}".format(info.parent_type, info.field_name) @@ -226,12 +229,7 @@ async def resolve(self, _next, root, info, *args, **kwargs): span.set_data("graphql.field_path", field_path) span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) - result = _next(root, info, *args, **kwargs) - - if isawaitable(result): - result = await result - - return result + return await self._resolve(_next, root, info, *args, **kwargs) class SentrySyncExtension(SentryAsyncExtension): @@ -300,17 +298,17 @@ def _sentry_patched_execute_sync(*args, **kwargs): def _patch_views(): # type: () -> None - old_handle_errors_async = async_base_view.AsyncBaseHTTPView._handle_errors - old_handle_errors_sync = sync_base_view.SyncBaseHTTPView._handle_errors + 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_handle_errors_async(self, errors, response_data): + def _sentry_patched_async_view_handle_errors(self, errors, response_data): # type: (Any, List[GraphQLError], GraphQLHTTPResponse) -> None - old_handle_errors_async(self, errors, response_data) + old_async_view_handle_errors(self, errors, response_data) _sentry_patched_handle_errors(self, errors, response_data) - def _sentry_patched_handle_errors_sync(self, errors, response_data): + def _sentry_patched_sync_view_handle_errors(self, errors, response_data): # type: (Any, List[GraphQLError], GraphQLHTTPResponse) -> None - old_handle_errors_sync(self, errors, response_data) + old_sync_view_handle_errors(self, errors, response_data) _sentry_patched_handle_errors(self, errors, response_data) def _sentry_patched_handle_errors(self, errors, response_data): @@ -340,9 +338,11 @@ def _sentry_patched_handle_errors(self, errors, response_data): hub.capture_event(event, hint=hint) async_base_view.AsyncBaseHTTPView._handle_errors = ( - _sentry_patched_handle_errors_async + _sentry_patched_async_view_handle_errors + ) + sync_base_view.SyncBaseHTTPView._handle_errors = ( + _sentry_patched_sync_view_handle_errors ) - sync_base_view.SyncBaseHTTPView._handle_errors = _sentry_patched_handle_errors_sync def _make_request_event_processor(execution_context): From 8a75c2ad4ee4bc55669d388c790a190be7eae669 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 10 Oct 2023 10:48:34 +0200 Subject: [PATCH 31/37] review feedback 2 --- sentry_sdk/integrations/strawberry.py | 1 + tests/integrations/strawberry/test_strawberry_py3.py | 9 ++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 95df1b25e0..27be230eb5 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -207,6 +207,7 @@ def should_skip_tracing(self, _next, info): 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): diff --git a/tests/integrations/strawberry/test_strawberry_py3.py b/tests/integrations/strawberry/test_strawberry_py3.py index dcbc569913..8355b19093 100644 --- a/tests/integrations/strawberry/test_strawberry_py3.py +++ b/tests/integrations/strawberry/test_strawberry_py3.py @@ -4,6 +4,8 @@ pytest.importorskip("fastapi") pytest.importorskip("flask") +from unittest import mock + from fastapi import FastAPI from fastapi.testclient import TestClient from flask import Flask @@ -24,11 +26,6 @@ SentrySyncExtension, ) -try: - from unittest import mock # python 3.3 and above -except ImportError: - import mock # python < 3.3 - @strawberry.type class Query: @@ -101,6 +98,7 @@ def test_replace_existing_sentry_async_extension(sentry_init): schema = strawberry.Schema(Query, extensions=[SentryTracingExtension]) assert SentryTracingExtension not in schema.extensions + assert SentrySyncExtension not in schema.extensions assert SentryAsyncExtension in schema.extensions @@ -109,6 +107,7 @@ def test_replace_existing_sentry_sync_extension(sentry_init): schema = strawberry.Schema(Query, extensions=[SentryTracingExtensionSync]) assert SentryTracingExtensionSync not in schema.extensions + assert SentryAsyncExtension not in schema.extensions assert SentrySyncExtension in schema.extensions From 168fe8c0f7b16f5314b5a09be3264b41db898e1b Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 10 Oct 2023 12:43:04 +0200 Subject: [PATCH 32/37] use fixtures in tests --- .../strawberry/test_strawberry_py3.py | 653 ++++++------------ 1 file changed, 205 insertions(+), 448 deletions(-) diff --git a/tests/integrations/strawberry/test_strawberry_py3.py b/tests/integrations/strawberry/test_strawberry_py3.py index 8355b19093..784c6f73ae 100644 --- a/tests/integrations/strawberry/test_strawberry_py3.py +++ b/tests/integrations/strawberry/test_strawberry_py3.py @@ -45,6 +45,29 @@ def change(self, attribute: str) -> str: return attribute +@pytest.fixture +def async_app_client_factory(): + def create_app(schema): + async_app = FastAPI() + async_app.include_router(GraphQLRouter(schema), prefix="/graphql") + return TestClient(async_app) + + return create_app + + +@pytest.fixture +def sync_app_client_factory(): + def create_app(schema): + sync_app = Flask(__name__) + sync_app.add_url_rule( + "/graphql", + view_func=GraphQLView.as_view("graphql_view", schema=schema), + ) + return sync_app.test_client() + + return create_app + + def test_async_execution_uses_async_extension(sentry_init): sentry_init(integrations=[StrawberryIntegration(async_execution=True)]) @@ -111,26 +134,40 @@ def test_replace_existing_sentry_sync_extension(sentry_init): assert SentrySyncExtension in schema.extensions -def test_capture_request_if_available_and_send_pii_is_on_async( - sentry_init, capture_events +@pytest.mark.parametrize( + "client_factory,async_execution,framework_integrations", + ( + ( + "async_app_client_factory", + True, + [FastApiIntegration(), StarletteIntegration()], + ), + ("sync_app_client_factory", False, [FlaskIntegration()]), + ), +) +def test_capture_request_if_available_and_send_pii_is_on( + request, + sentry_init, + capture_events, + client_factory, + async_execution, + framework_integrations, ): sentry_init( send_default_pii=True, integrations=[ - StrawberryIntegration(), - FastApiIntegration(), - StarletteIntegration(), - ], + StrawberryIntegration(async_execution=async_execution), + ] + + framework_integrations, ) events = capture_events() schema = strawberry.Schema(Query) - async_app = FastAPI() - async_app.include_router(GraphQLRouter(schema), prefix="/graphql") + client_factory = request.getfixturevalue(client_factory) + client = client_factory(schema) query = "query ErrorQuery { error }" - client = TestClient(async_app) client.post("/graphql", json={"query": query, "operationName": "ErrorQuery"}) assert len(events) == 1 @@ -163,73 +200,39 @@ def test_capture_request_if_available_and_send_pii_is_on_async( } -def test_capture_request_if_available_and_send_pii_is_on_sync( - sentry_init, capture_events +@pytest.mark.parametrize( + "client_factory,async_execution,framework_integrations", + ( + ( + "async_app_client_factory", + True, + [FastApiIntegration(), StarletteIntegration()], + ), + ("sync_app_client_factory", False, [FlaskIntegration()]), + ), +) +def test_do_not_capture_request_if_send_pii_is_off( + request, + sentry_init, + capture_events, + client_factory, + async_execution, + framework_integrations, ): - sentry_init( - send_default_pii=True, - integrations=[StrawberryIntegration(async_execution=False), FlaskIntegration()], - ) - events = capture_events() - - schema = strawberry.Schema(Query) - - sync_app = Flask(__name__) - sync_app.add_url_rule( - "/graphql", - view_func=GraphQLView.as_view("graphql_view", schema=schema), - ) - - query = "query ErrorQuery { error }" - client = sync_app.test_client() - client.post("/graphql", json={"query": query, "operationName": "ErrorQuery"}) - - assert len(events) == 1 - - (error_event,) = events - assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" - assert error_event["request"]["api_target"] == "graphql" - assert error_event["request"]["data"] == { - "query": query, - "operationName": "ErrorQuery", - } - assert error_event["contexts"]["response"] == { - "data": { - "data": None, - "errors": [ - { - "message": "division by zero", - "locations": [{"line": 1, "column": 20}], - "path": ["error"], - } - ], - } - } - assert len(error_event["breadcrumbs"]["values"]) == 1 - assert error_event["breadcrumbs"]["values"][0]["category"] == "graphql.operation" - assert error_event["breadcrumbs"]["values"][0]["data"] == { - "operation_name": "ErrorQuery", - "operation_type": "query", - } - - -def test_do_not_capture_request_if_send_pii_is_off_async(sentry_init, capture_events): sentry_init( integrations=[ - StrawberryIntegration(), - FastApiIntegration(), - StarletteIntegration(), - ], + StrawberryIntegration(async_execution=async_execution), + ] + + framework_integrations, ) events = capture_events() schema = strawberry.Schema(Query) - async_app = FastAPI() - async_app.include_router(GraphQLRouter(schema), prefix="/graphql") + client_factory = request.getfixturevalue(client_factory) + client = client_factory(schema) query = "query ErrorQuery { error }" - client = TestClient(async_app) client.post("/graphql", json={"query": query, "operationName": "ErrorQuery"}) assert len(events) == 1 @@ -247,56 +250,39 @@ def test_do_not_capture_request_if_send_pii_is_off_async(sentry_init, capture_ev } -def test_do_not_capture_request_if_send_pii_is_off_sync(sentry_init, capture_events): - sentry_init( - integrations=[StrawberryIntegration(async_execution=False), FlaskIntegration()], - ) - events = capture_events() - - schema = strawberry.Schema(Query) - - sync_app = Flask(__name__) - sync_app.add_url_rule( - "/graphql", - view_func=GraphQLView.as_view("graphql_view", schema=schema), - ) - - query = "query ErrorQuery { error }" - client = sync_app.test_client() - client.post("/graphql", json={"query": query, "operationName": "ErrorQuery"}) - - assert len(events) == 1 - - (error_event,) = events - assert error_event["exception"]["values"][0]["mechanism"]["type"] == "strawberry" - assert "data" not in error_event["request"] - assert "response" not in error_event["contexts"] - - assert len(error_event["breadcrumbs"]["values"]) == 1 - assert error_event["breadcrumbs"]["values"][0]["category"] == "graphql.operation" - assert error_event["breadcrumbs"]["values"][0]["data"] == { - "operation_name": "ErrorQuery", - "operation_type": "query", - } - - -def test_breadcrumb_no_operation_name_async(sentry_init, capture_events): +@pytest.mark.parametrize( + "client_factory,async_execution,framework_integrations", + ( + ( + "async_app_client_factory", + True, + [FastApiIntegration(), StarletteIntegration()], + ), + ("sync_app_client_factory", False, [FlaskIntegration()]), + ), +) +def test_breadcrumb_no_operation_name( + request, + sentry_init, + capture_events, + client_factory, + async_execution, + framework_integrations, +): sentry_init( integrations=[ - StrawberryIntegration(), - FastApiIntegration(), - StarletteIntegration(), - ], + StrawberryIntegration(async_execution=async_execution), + ] + + framework_integrations, ) events = capture_events() schema = strawberry.Schema(Query) - async_app = FastAPI() - async_app.include_router(GraphQLRouter(schema), prefix="/graphql") + client_factory = request.getfixturevalue(client_factory) + client = client_factory(schema) query = "{ error }" - client = TestClient(async_app) client.post("/graphql", json={"query": query}) assert len(events) == 1 @@ -311,130 +297,51 @@ def test_breadcrumb_no_operation_name_async(sentry_init, capture_events): } -def test_breadcrumb_no_operation_name_sync(sentry_init, capture_events): - sentry_init( - integrations=[StrawberryIntegration(async_execution=False), FlaskIntegration()], - ) - events = capture_events() - - schema = strawberry.Schema(Query) - - sync_app = Flask(__name__) - sync_app.add_url_rule( - "/graphql", - view_func=GraphQLView.as_view("graphql_view", schema=schema), - ) - - query = "{ error }" - client = sync_app.test_client() - client.post("/graphql", json={"query": query}) - - assert len(events) == 1 - - (error_event,) = events - - assert len(error_event["breadcrumbs"]["values"]) == 1 - assert error_event["breadcrumbs"]["values"][0]["category"] == "graphql.operation" - assert error_event["breadcrumbs"]["values"][0]["data"] == { - "operation_name": None, - "operation_type": "query", - } - - -def test_capture_transaction_on_error_async(sentry_init, capture_events): +@pytest.mark.parametrize( + "client_factory,async_execution,framework_integrations", + ( + ( + "async_app_client_factory", + True, + [FastApiIntegration(), StarletteIntegration()], + ), + ("sync_app_client_factory", False, [FlaskIntegration()]), + ), +) +def test_capture_transaction_on_error( + request, + sentry_init, + capture_events, + client_factory, + async_execution, + framework_integrations, +): sentry_init( send_default_pii=True, integrations=[ - StrawberryIntegration(), - FastApiIntegration(), - StarletteIntegration(), - ], + StrawberryIntegration(async_execution=async_execution), + ] + + framework_integrations, traces_sample_rate=1, ) events = capture_events() schema = strawberry.Schema(Query) - async_app = FastAPI() - async_app.include_router(GraphQLRouter(schema), prefix="/graphql") + client_factory = request.getfixturevalue(client_factory) + client = client_factory(schema) query = "query ErrorQuery { error }" - client = TestClient(async_app) client.post("/graphql", json={"query": query, "operationName": "ErrorQuery"}) assert len(events) == 2 (_, transaction_event) = events - assert transaction_event["transaction"] == "/graphql" - assert transaction_event["spans"] - - query_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_QUERY - ] - assert len(query_spans) == 1, "exactly one query span expected" - query_span = query_spans[0] - assert query_span["description"] == "query ErrorQuery" - assert query_span["data"]["graphql.operation.type"] == "query" - assert query_span["data"]["graphql.operation.name"] == "ErrorQuery" - assert query_span["data"]["graphql.document"] == query - assert query_span["data"]["graphql.resource_name"] - - parse_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE - ] - assert len(parse_spans) == 1, "exactly one parse span expected" - parse_span = parse_spans[0] - assert parse_span["parent_span_id"] == query_span["span_id"] - assert parse_span["description"] == "parsing" - - validate_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE - ] - assert len(validate_spans) == 1, "exactly one validate span expected" - validate_span = validate_spans[0] - assert validate_span["parent_span_id"] == query_span["span_id"] - assert validate_span["description"] == "validation" - - resolve_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE - ] - assert len(resolve_spans) == 1, "exactly one resolve span expected" - resolve_span = resolve_spans[0] - assert resolve_span["parent_span_id"] == query_span["span_id"] - assert resolve_span["description"] == "resolving Query.error" - assert resolve_span["data"] == { - "graphql.field_name": "error", - "graphql.parent_type": "Query", - "graphql.field_path": "Query.error", - "graphql.path": "error", - } - + if async_execution: + assert transaction_event["transaction"] == "/graphql" + else: + assert transaction_event["transaction"] == "graphql_view" -def test_capture_transaction_on_error_sync(sentry_init, capture_events): - sentry_init( - send_default_pii=True, - integrations=[StrawberryIntegration(async_execution=False), FlaskIntegration()], - traces_sample_rate=1, - ) - events = capture_events() - - schema = strawberry.Schema(Query) - - sync_app = Flask(__name__) - sync_app.add_url_rule( - "/graphql", - view_func=GraphQLView.as_view("graphql_view", schema=schema), - ) - - query = "query ErrorQuery { error }" - client = sync_app.test_client() - client.post("/graphql", json={"query": query, "operationName": "ErrorQuery"}) - - assert len(events) == 2 - - (_, transaction_event) = events - - assert transaction_event["transaction"] == "graphql_view" assert transaction_event["spans"] query_spans = [ @@ -479,100 +386,50 @@ def test_capture_transaction_on_error_sync(sentry_init, capture_events): } -def test_capture_transaction_on_success_async(sentry_init, capture_events): +@pytest.mark.parametrize( + "client_factory,async_execution,framework_integrations", + ( + ( + "async_app_client_factory", + True, + [FastApiIntegration(), StarletteIntegration()], + ), + ("sync_app_client_factory", False, [FlaskIntegration()]), + ), +) +def test_capture_transaction_on_success( + request, + sentry_init, + capture_events, + client_factory, + async_execution, + framework_integrations, +): sentry_init( integrations=[ - StrawberryIntegration(), - FastApiIntegration(), - StarletteIntegration(), - ], + StrawberryIntegration(async_execution=async_execution), + ] + + framework_integrations, traces_sample_rate=1, ) events = capture_events() schema = strawberry.Schema(Query) - async_app = FastAPI() - async_app.include_router(GraphQLRouter(schema), prefix="/graphql") + client_factory = request.getfixturevalue(client_factory) + client = client_factory(schema) query = "query GreetingQuery { hello }" - client = TestClient(async_app) client.post("/graphql", json={"query": query, "operationName": "GreetingQuery"}) assert len(events) == 1 (transaction_event,) = events - assert transaction_event["transaction"] == "/graphql" - assert transaction_event["spans"] + if async_execution: + assert transaction_event["transaction"] == "/graphql" + else: + assert transaction_event["transaction"] == "graphql_view" - query_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_QUERY - ] - assert len(query_spans) == 1, "exactly one query span expected" - query_span = query_spans[0] - assert query_span["description"] == "query GreetingQuery" - assert query_span["data"]["graphql.operation.type"] == "query" - assert query_span["data"]["graphql.operation.name"] == "GreetingQuery" - assert query_span["data"]["graphql.document"] == query - assert query_span["data"]["graphql.resource_name"] - - parse_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE - ] - assert len(parse_spans) == 1, "exactly one parse span expected" - parse_span = parse_spans[0] - assert parse_span["parent_span_id"] == query_span["span_id"] - assert parse_span["description"] == "parsing" - - validate_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE - ] - assert len(validate_spans) == 1, "exactly one validate span expected" - validate_span = validate_spans[0] - assert validate_span["parent_span_id"] == query_span["span_id"] - assert validate_span["description"] == "validation" - - resolve_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE - ] - assert len(resolve_spans) == 1, "exactly one resolve span expected" - resolve_span = resolve_spans[0] - assert resolve_span["parent_span_id"] == query_span["span_id"] - assert resolve_span["description"] == "resolving Query.hello" - assert resolve_span["data"] == { - "graphql.field_name": "hello", - "graphql.parent_type": "Query", - "graphql.field_path": "Query.hello", - "graphql.path": "hello", - } - - -def test_capture_transaction_on_success_sync(sentry_init, capture_events): - sentry_init( - integrations=[ - StrawberryIntegration(async_execution=False), - FlaskIntegration(), - ], - traces_sample_rate=1, - ) - events = capture_events() - - schema = strawberry.Schema(Query) - - sync_app = Flask(__name__) - sync_app.add_url_rule( - "/graphql", - view_func=GraphQLView.as_view("graphql_view", schema=schema), - ) - - query = "query GreetingQuery { hello }" - client = sync_app.test_client() - client.post("/graphql", json={"query": query, "operationName": "GreetingQuery"}) - - assert len(events) == 1 - (transaction_event,) = events - - assert transaction_event["transaction"] == "graphql_view" assert transaction_event["spans"] query_spans = [ @@ -617,100 +474,50 @@ def test_capture_transaction_on_success_sync(sentry_init, capture_events): } -def test_transaction_no_operation_name_async(sentry_init, capture_events): +@pytest.mark.parametrize( + "client_factory,async_execution,framework_integrations", + ( + ( + "async_app_client_factory", + True, + [FastApiIntegration(), StarletteIntegration()], + ), + ("sync_app_client_factory", False, [FlaskIntegration()]), + ), +) +def test_transaction_no_operation_name( + request, + sentry_init, + capture_events, + client_factory, + async_execution, + framework_integrations, +): sentry_init( integrations=[ - StrawberryIntegration(), - FastApiIntegration(), - StarletteIntegration(), - ], + StrawberryIntegration(async_execution=async_execution), + ] + + framework_integrations, traces_sample_rate=1, ) events = capture_events() schema = strawberry.Schema(Query) - async_app = FastAPI() - async_app.include_router(GraphQLRouter(schema), prefix="/graphql") + client_factory = request.getfixturevalue(client_factory) + client = client_factory(schema) query = "{ hello }" - client = TestClient(async_app) client.post("/graphql", json={"query": query}) assert len(events) == 1 (transaction_event,) = events - assert transaction_event["transaction"] == "/graphql" - assert transaction_event["spans"] - - query_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_QUERY - ] - assert len(query_spans) == 1, "exactly one query span expected" - query_span = query_spans[0] - assert query_span["description"] == "query" - assert query_span["data"]["graphql.operation.type"] == "query" - assert query_span["data"]["graphql.operation.name"] is None - assert query_span["data"]["graphql.document"] == query - assert query_span["data"]["graphql.resource_name"] - - parse_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE - ] - assert len(parse_spans) == 1, "exactly one parse span expected" - parse_span = parse_spans[0] - assert parse_span["parent_span_id"] == query_span["span_id"] - assert parse_span["description"] == "parsing" - - validate_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE - ] - assert len(validate_spans) == 1, "exactly one validate span expected" - validate_span = validate_spans[0] - assert validate_span["parent_span_id"] == query_span["span_id"] - assert validate_span["description"] == "validation" - - resolve_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE - ] - assert len(resolve_spans) == 1, "exactly one resolve span expected" - resolve_span = resolve_spans[0] - assert resolve_span["parent_span_id"] == query_span["span_id"] - assert resolve_span["description"] == "resolving Query.hello" - assert resolve_span["data"] == { - "graphql.field_name": "hello", - "graphql.parent_type": "Query", - "graphql.field_path": "Query.hello", - "graphql.path": "hello", - } - - -def test_transaction_no_operation_name_sync(sentry_init, capture_events): - sentry_init( - integrations=[ - StrawberryIntegration(async_execution=False), - FlaskIntegration(), - ], - traces_sample_rate=1, - ) - events = capture_events() - - schema = strawberry.Schema(Query) - - sync_app = Flask(__name__) - sync_app.add_url_rule( - "/graphql", - view_func=GraphQLView.as_view("graphql_view", schema=schema), - ) - - query = "{ hello }" - client = sync_app.test_client() - client.post("/graphql", json={"query": query}) + if async_execution: + assert transaction_event["transaction"] == "/graphql" + else: + assert transaction_event["transaction"] == "graphql_view" - assert len(events) == 1 - (transaction_event,) = events - - assert transaction_event["transaction"] == "graphql_view" assert transaction_event["spans"] query_spans = [ @@ -755,100 +562,50 @@ def test_transaction_no_operation_name_sync(sentry_init, capture_events): } -def test_transaction_mutation_async(sentry_init, capture_events): +@pytest.mark.parametrize( + "client_factory,async_execution,framework_integrations", + ( + ( + "async_app_client_factory", + True, + [FastApiIntegration(), StarletteIntegration()], + ), + ("sync_app_client_factory", False, [FlaskIntegration()]), + ), +) +def test_transaction_mutation( + request, + sentry_init, + capture_events, + client_factory, + async_execution, + framework_integrations, +): sentry_init( integrations=[ - StrawberryIntegration(), - FastApiIntegration(), - StarletteIntegration(), - ], + StrawberryIntegration(async_execution=async_execution), + ] + + framework_integrations, traces_sample_rate=1, ) events = capture_events() schema = strawberry.Schema(Query, mutation=Mutation) - async_app = FastAPI() - async_app.include_router(GraphQLRouter(schema), prefix="/graphql") + client_factory = request.getfixturevalue(client_factory) + client = client_factory(schema) query = 'mutation Change { change(attribute: "something") }' - client = TestClient(async_app) client.post("/graphql", json={"query": query}) assert len(events) == 1 (transaction_event,) = events - assert transaction_event["transaction"] == "/graphql" - assert transaction_event["spans"] - - query_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_MUTATION - ] - assert len(query_spans) == 1, "exactly one mutation span expected" - query_span = query_spans[0] - assert query_span["description"] == "mutation" - assert query_span["data"]["graphql.operation.type"] == "mutation" - assert query_span["data"]["graphql.operation.name"] is None - assert query_span["data"]["graphql.document"] == query - assert query_span["data"]["graphql.resource_name"] - - parse_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_PARSE - ] - assert len(parse_spans) == 1, "exactly one parse span expected" - parse_span = parse_spans[0] - assert parse_span["parent_span_id"] == query_span["span_id"] - assert parse_span["description"] == "parsing" - - validate_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_VALIDATE - ] - assert len(validate_spans) == 1, "exactly one validate span expected" - validate_span = validate_spans[0] - assert validate_span["parent_span_id"] == query_span["span_id"] - assert validate_span["description"] == "validation" - - resolve_spans = [ - span for span in transaction_event["spans"] if span["op"] == OP.GRAPHQL_RESOLVE - ] - assert len(resolve_spans) == 1, "exactly one resolve span expected" - resolve_span = resolve_spans[0] - assert resolve_span["parent_span_id"] == query_span["span_id"] - assert resolve_span["description"] == "resolving Mutation.change" - assert resolve_span["data"] == { - "graphql.field_name": "change", - "graphql.parent_type": "Mutation", - "graphql.field_path": "Mutation.change", - "graphql.path": "change", - } - - -def test_transaction_mutation_sync(sentry_init, capture_events): - sentry_init( - integrations=[ - StrawberryIntegration(async_execution=False), - FlaskIntegration(), - ], - traces_sample_rate=1, - ) - events = capture_events() - - schema = strawberry.Schema(Query, mutation=Mutation) - - sync_app = Flask(__name__) - sync_app.add_url_rule( - "/graphql", - view_func=GraphQLView.as_view("graphql_view", schema=schema), - ) - - query = 'mutation Change { change(attribute: "something") }' - client = sync_app.test_client() - client.post("/graphql", json={"query": query}) - - assert len(events) == 1 - (transaction_event,) = events + if async_execution: + assert transaction_event["transaction"] == "/graphql" + else: + assert transaction_event["transaction"] == "graphql_view" - assert transaction_event["transaction"] == "graphql_view" assert transaction_event["spans"] query_spans = [ From 783152002244268784cab9b730722f1f0aac1e4e Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 10 Oct 2023 13:07:25 +0200 Subject: [PATCH 33/37] handle event['request'] being none --- sentry_sdk/integrations/strawberry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 27be230eb5..d2b3155ed8 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -366,7 +366,7 @@ def inner(event, hint): "operationName" ] = execution_context.operation_name - elif event.get("request", {}).get("data"): + elif (event.get("request") or {}).get("data"): del event["request"]["data"] return event From aed4cc92134f88b18c601b6c7c889d6eddb63dba Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 10 Oct 2023 14:10:47 +0200 Subject: [PATCH 34/37] introduce package_version helper --- sentry_sdk/integrations/ariadne.py | 8 +-- sentry_sdk/integrations/asgi.py | 2 +- sentry_sdk/integrations/flask.py | 10 +--- sentry_sdk/integrations/graphene.py | 8 +-- sentry_sdk/integrations/modules.py | 46 +-------------- .../integrations/opentelemetry/integration.py | 3 +- sentry_sdk/integrations/strawberry.py | 7 +-- sentry_sdk/utils.py | 51 ++++++++++++++++ tests/integrations/modules/test_modules.py | 59 +------------------ tests/test_utils.py | 53 +++++++++++++++++ 10 files changed, 120 insertions(+), 127 deletions(-) diff --git a/sentry_sdk/integrations/ariadne.py b/sentry_sdk/integrations/ariadne.py index 8025860a6f..86d6b5e28e 100644 --- a/sentry_sdk/integrations/ariadne.py +++ b/sentry_sdk/integrations/ariadne.py @@ -3,12 +3,11 @@ from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.integrations.modules import _get_installed_modules from sentry_sdk.integrations._wsgi_common import request_body_within_bounds from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, - parse_version, + package_version, ) from sentry_sdk._types import TYPE_CHECKING @@ -33,11 +32,10 @@ class AriadneIntegration(Integration): @staticmethod def setup_once(): # type: () -> None - installed_packages = _get_installed_modules() - version = parse_version(installed_packages["ariadne"]) + version = package_version("ariadne") if version is None: - raise DidNotEnable("Unparsable ariadne version: {}".format(version)) + raise DidNotEnable("Unparsable ariadne version.") if version < (0, 20): raise DidNotEnable("ariadne 0.20 or newer required.") diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 2cecdf9a81..901c6f5d23 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -19,7 +19,6 @@ _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, @@ -34,6 +33,7 @@ CONTEXTVARS_ERROR_MESSAGE, logger, transaction_from_function, + _get_installed_modules, ) from sentry_sdk.tracing import Transaction diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index 0da411c23d..453ab48ce3 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -5,13 +5,12 @@ from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations._wsgi_common import RequestExtractor from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.integrations.modules import _get_installed_modules from sentry_sdk.scope import Scope from sentry_sdk.tracing import SOURCE_FOR_STYLE from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, - parse_version, + package_version, ) if TYPE_CHECKING: @@ -64,13 +63,10 @@ def __init__(self, transaction_style="endpoint"): @staticmethod def setup_once(): # type: () -> None - - installed_packages = _get_installed_modules() - flask_version = installed_packages["flask"] - version = parse_version(flask_version) + version = package_version("flask") if version is None: - raise DidNotEnable("Unparsable Flask version: {}".format(flask_version)) + raise DidNotEnable("Unparsable Flask version.") if version < (0, 10): raise DidNotEnable("Flask 0.10 or newer is required.") diff --git a/sentry_sdk/integrations/graphene.py b/sentry_sdk/integrations/graphene.py index 5d3c656145..fa753d0812 100644 --- a/sentry_sdk/integrations/graphene.py +++ b/sentry_sdk/integrations/graphene.py @@ -1,10 +1,9 @@ from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.modules import _get_installed_modules from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, - parse_version, + package_version, ) from sentry_sdk._types import TYPE_CHECKING @@ -28,11 +27,10 @@ class GrapheneIntegration(Integration): @staticmethod def setup_once(): # type: () -> None - installed_packages = _get_installed_modules() - version = parse_version(installed_packages["graphene"]) + version = package_version("graphene") if version is None: - raise DidNotEnable("Unparsable graphene version: {}".format(version)) + raise DidNotEnable("Unparsable graphene version.") if version < (3, 3): raise DidNotEnable("graphene 3.3 or newer required.") diff --git a/sentry_sdk/integrations/modules.py b/sentry_sdk/integrations/modules.py index 3f9f356eed..5b595b4032 100644 --- a/sentry_sdk/integrations/modules.py +++ b/sentry_sdk/integrations/modules.py @@ -3,61 +3,17 @@ from sentry_sdk.hub import Hub from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import _get_installed_modules from sentry_sdk._types import TYPE_CHECKING if TYPE_CHECKING: from typing import Any from typing import Dict - from typing import Tuple - from typing import Iterator from sentry_sdk._types import Event -_installed_modules = None - - -def _normalize_module_name(name): - # type: (str) -> str - return name.lower() - - -def _generate_installed_modules(): - # type: () -> Iterator[Tuple[str, str]] - try: - from importlib import metadata - - for dist in metadata.distributions(): - name = dist.metadata["Name"] - # `metadata` values may be `None`, see: - # https://github.com/python/cpython/issues/91216 - # and - # https://github.com/python/importlib_metadata/issues/371 - if name is not None: - version = metadata.version(name) - if version is not None: - yield _normalize_module_name(name), version - - except ImportError: - # < py3.8 - try: - import pkg_resources - except ImportError: - return - - for info in pkg_resources.working_set: - yield _normalize_module_name(info.key), info.version - - -def _get_installed_modules(): - # type: () -> Dict[str, str] - global _installed_modules - if _installed_modules is None: - _installed_modules = dict(_generate_installed_modules()) - return _installed_modules - - class ModulesIntegration(Integration): identifier = "modules" diff --git a/sentry_sdk/integrations/opentelemetry/integration.py b/sentry_sdk/integrations/opentelemetry/integration.py index 20dc4625df..e1a4318f67 100644 --- a/sentry_sdk/integrations/opentelemetry/integration.py +++ b/sentry_sdk/integrations/opentelemetry/integration.py @@ -9,8 +9,7 @@ from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.modules import _get_installed_modules -from sentry_sdk.utils import logger +from sentry_sdk.utils import logger, _get_installed_modules from sentry_sdk._types import TYPE_CHECKING try: diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index d2b3155ed8..c2e90b599d 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -5,13 +5,13 @@ from sentry_sdk.consts import OP from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.integrations.modules import _get_installed_modules from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, + package_version, logger, - parse_version, + _get_installed_modules, ) from sentry_sdk._types import TYPE_CHECKING @@ -55,8 +55,7 @@ def __init__(self, async_execution=None): @staticmethod def setup_once(): # type: () -> None - installed_packages = _get_installed_modules() - version = parse_version(installed_packages["strawberry-graphql"]) + version = package_version("strawberry-graphql") if version is None: raise DidNotEnable( diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index c811d2d2fe..f9fb4ebfe3 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -76,6 +76,7 @@ # The logger is created here but initialized in the debug support module logger = logging.getLogger("sentry_sdk.errors") +_installed_modules = None BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$") @@ -1563,6 +1564,56 @@ def parse_version(version): return release_tuple +def _generate_installed_modules(): + # type: () -> Iterator[Tuple[str, str]] + try: + from importlib import metadata + + for dist in metadata.distributions(): + name = dist.metadata["Name"] + # `metadata` values may be `None`, see: + # https://github.com/python/cpython/issues/91216 + # and + # https://github.com/python/importlib_metadata/issues/371 + if name is not None: + version = metadata.version(name) + if version is not None: + yield _normalize_module_name(name), version + + except ImportError: + # < py3.8 + try: + import pkg_resources + except ImportError: + return + + for info in pkg_resources.working_set: + yield _normalize_module_name(info.key), info.version + + +def _normalize_module_name(name): + # type: (str) -> str + return name.lower() + + +def _get_installed_modules(): + # type: () -> Dict[str, str] + global _installed_modules + if _installed_modules is None: + _installed_modules = dict(_generate_installed_modules()) + return _installed_modules + + +def package_version(package): + # type: (str) -> Optional[Tuple[int, ...]] + installed_packages = _get_installed_modules() + version = installed_packages.get(package) + if version is None: + return None + + return parse_version(version) + + if PY37: def nanosecond_time(): diff --git a/tests/integrations/modules/test_modules.py b/tests/integrations/modules/test_modules.py index c7097972b0..3f4d7bd9dc 100644 --- a/tests/integrations/modules/test_modules.py +++ b/tests/integrations/modules/test_modules.py @@ -1,22 +1,6 @@ -import pytest -import re import sentry_sdk -from sentry_sdk.integrations.modules import ( - ModulesIntegration, - _get_installed_modules, -) - - -def _normalize_distribution_name(name): - # type: (str) -> str - """Normalize distribution name according to PEP-0503. - - See: - https://peps.python.org/pep-0503/#normalized-names - for more details. - """ - return re.sub(r"[-_.]+", "-", name).lower() +from sentry_sdk.integrations.modules import ModulesIntegration def test_basic(sentry_init, capture_events): @@ -28,44 +12,3 @@ def test_basic(sentry_init, capture_events): (event,) = events assert "sentry-sdk" in event["modules"] assert "pytest" in event["modules"] - - -def test_installed_modules(): - try: - from importlib.metadata import distributions, version - - importlib_available = True - except ImportError: - importlib_available = False - - try: - import pkg_resources - - pkg_resources_available = True - except ImportError: - pkg_resources_available = False - - installed_distributions = { - _normalize_distribution_name(dist): version - for dist, version in _get_installed_modules().items() - } - - if importlib_available: - importlib_distributions = { - _normalize_distribution_name(dist.metadata["Name"]): version( - dist.metadata["Name"] - ) - for dist in distributions() - if dist.metadata["Name"] is not None - and version(dist.metadata["Name"]) is not None - } - assert installed_distributions == importlib_distributions - - elif pkg_resources_available: - pkg_resources_distributions = { - _normalize_distribution_name(dist.key): dist.version - for dist in pkg_resources.working_set - } - assert installed_distributions == pkg_resources_distributions - else: - pytest.fail("Neither importlib nor pkg_resources is available") diff --git a/tests/test_utils.py b/tests/test_utils.py index ee73433dd5..66ff59f650 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -15,6 +15,7 @@ sanitize_url, serialize_frame, is_sentry_url, + _get_installed_modules, ) import sentry_sdk @@ -25,6 +26,17 @@ import mock # python < 3.3 +def _normalize_distribution_name(name): + # type: (str) -> str + """Normalize distribution name according to PEP-0503. + + See: + https://peps.python.org/pep-0503/#normalized-names + for more details. + """ + return re.sub(r"[-_.]+", "-", name).lower() + + @pytest.mark.parametrize( ("url", "expected_result"), [ @@ -488,3 +500,44 @@ def test_get_error_message(error, expected_result): exc_value.detail = error raise Exception assert get_error_message(exc_value) == expected_result(exc_value) + + +def test_installed_modules(): + try: + from importlib.metadata import distributions, version + + importlib_available = True + except ImportError: + importlib_available = False + + try: + import pkg_resources + + pkg_resources_available = True + except ImportError: + pkg_resources_available = False + + installed_distributions = { + _normalize_distribution_name(dist): version + for dist, version in _get_installed_modules().items() + } + + if importlib_available: + importlib_distributions = { + _normalize_distribution_name(dist.metadata["Name"]): version( + dist.metadata["Name"] + ) + for dist in distributions() + if dist.metadata["Name"] is not None + and version(dist.metadata["Name"]) is not None + } + assert installed_distributions == importlib_distributions + + elif pkg_resources_available: + pkg_resources_distributions = { + _normalize_distribution_name(dist.key): dist.version + for dist in pkg_resources.working_set + } + assert installed_distributions == pkg_resources_distributions + else: + pytest.fail("Neither importlib nor pkg_resources is available") From 3df725663d43e2d1b066985992eaff52ac5816da Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 10 Oct 2023 14:28:27 +0200 Subject: [PATCH 35/37] Revert "introduce package_version helper" This reverts commit aed4cc92134f88b18c601b6c7c889d6eddb63dba. Will do this in a new PR. --- sentry_sdk/integrations/ariadne.py | 8 ++- sentry_sdk/integrations/asgi.py | 2 +- sentry_sdk/integrations/flask.py | 10 +++- sentry_sdk/integrations/graphene.py | 8 ++- sentry_sdk/integrations/modules.py | 46 ++++++++++++++- .../integrations/opentelemetry/integration.py | 3 +- sentry_sdk/integrations/strawberry.py | 7 ++- sentry_sdk/utils.py | 51 ---------------- tests/integrations/modules/test_modules.py | 59 ++++++++++++++++++- tests/test_utils.py | 53 ----------------- 10 files changed, 127 insertions(+), 120 deletions(-) diff --git a/sentry_sdk/integrations/ariadne.py b/sentry_sdk/integrations/ariadne.py index 86d6b5e28e..8025860a6f 100644 --- a/sentry_sdk/integrations/ariadne.py +++ b/sentry_sdk/integrations/ariadne.py @@ -3,11 +3,12 @@ from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.integrations.modules import _get_installed_modules from sentry_sdk.integrations._wsgi_common import request_body_within_bounds from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, - package_version, + parse_version, ) from sentry_sdk._types import TYPE_CHECKING @@ -32,10 +33,11 @@ class AriadneIntegration(Integration): @staticmethod def setup_once(): # type: () -> None - version = package_version("ariadne") + installed_packages = _get_installed_modules() + version = parse_version(installed_packages["ariadne"]) if version is None: - raise DidNotEnable("Unparsable ariadne version.") + raise DidNotEnable("Unparsable ariadne version: {}".format(version)) if version < (0, 20): raise DidNotEnable("ariadne 0.20 or newer required.") diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 901c6f5d23..2cecdf9a81 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -19,6 +19,7 @@ _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, @@ -33,7 +34,6 @@ CONTEXTVARS_ERROR_MESSAGE, logger, transaction_from_function, - _get_installed_modules, ) from sentry_sdk.tracing import Transaction diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index 453ab48ce3..0da411c23d 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -5,12 +5,13 @@ from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations._wsgi_common import RequestExtractor from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.integrations.modules import _get_installed_modules from sentry_sdk.scope import Scope from sentry_sdk.tracing import SOURCE_FOR_STYLE from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, - package_version, + parse_version, ) if TYPE_CHECKING: @@ -63,10 +64,13 @@ def __init__(self, transaction_style="endpoint"): @staticmethod def setup_once(): # type: () -> None - version = package_version("flask") + + installed_packages = _get_installed_modules() + flask_version = installed_packages["flask"] + version = parse_version(flask_version) if version is None: - raise DidNotEnable("Unparsable Flask version.") + raise DidNotEnable("Unparsable Flask version: {}".format(flask_version)) if version < (0, 10): raise DidNotEnable("Flask 0.10 or newer is required.") diff --git a/sentry_sdk/integrations/graphene.py b/sentry_sdk/integrations/graphene.py index fa753d0812..5d3c656145 100644 --- a/sentry_sdk/integrations/graphene.py +++ b/sentry_sdk/integrations/graphene.py @@ -1,9 +1,10 @@ from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.modules import _get_installed_modules from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, - package_version, + parse_version, ) from sentry_sdk._types import TYPE_CHECKING @@ -27,10 +28,11 @@ class GrapheneIntegration(Integration): @staticmethod def setup_once(): # type: () -> None - version = package_version("graphene") + installed_packages = _get_installed_modules() + version = parse_version(installed_packages["graphene"]) if version is None: - raise DidNotEnable("Unparsable graphene version.") + raise DidNotEnable("Unparsable graphene version: {}".format(version)) if version < (3, 3): raise DidNotEnable("graphene 3.3 or newer required.") diff --git a/sentry_sdk/integrations/modules.py b/sentry_sdk/integrations/modules.py index 5b595b4032..3f9f356eed 100644 --- a/sentry_sdk/integrations/modules.py +++ b/sentry_sdk/integrations/modules.py @@ -3,17 +3,61 @@ from sentry_sdk.hub import Hub from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import _get_installed_modules from sentry_sdk._types import TYPE_CHECKING if TYPE_CHECKING: from typing import Any from typing import Dict + from typing import Tuple + from typing import Iterator from sentry_sdk._types import Event +_installed_modules = None + + +def _normalize_module_name(name): + # type: (str) -> str + return name.lower() + + +def _generate_installed_modules(): + # type: () -> Iterator[Tuple[str, str]] + try: + from importlib import metadata + + for dist in metadata.distributions(): + name = dist.metadata["Name"] + # `metadata` values may be `None`, see: + # https://github.com/python/cpython/issues/91216 + # and + # https://github.com/python/importlib_metadata/issues/371 + if name is not None: + version = metadata.version(name) + if version is not None: + yield _normalize_module_name(name), version + + except ImportError: + # < py3.8 + try: + import pkg_resources + except ImportError: + return + + for info in pkg_resources.working_set: + yield _normalize_module_name(info.key), info.version + + +def _get_installed_modules(): + # type: () -> Dict[str, str] + global _installed_modules + if _installed_modules is None: + _installed_modules = dict(_generate_installed_modules()) + return _installed_modules + + class ModulesIntegration(Integration): identifier = "modules" diff --git a/sentry_sdk/integrations/opentelemetry/integration.py b/sentry_sdk/integrations/opentelemetry/integration.py index e1a4318f67..20dc4625df 100644 --- a/sentry_sdk/integrations/opentelemetry/integration.py +++ b/sentry_sdk/integrations/opentelemetry/integration.py @@ -9,7 +9,8 @@ from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.utils import logger, _get_installed_modules +from sentry_sdk.integrations.modules import _get_installed_modules +from sentry_sdk.utils import logger from sentry_sdk._types import TYPE_CHECKING try: diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index c2e90b599d..d2b3155ed8 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -5,13 +5,13 @@ from sentry_sdk.consts import OP from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.integrations.modules import _get_installed_modules from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, - package_version, logger, - _get_installed_modules, + parse_version, ) from sentry_sdk._types import TYPE_CHECKING @@ -55,7 +55,8 @@ def __init__(self, async_execution=None): @staticmethod def setup_once(): # type: () -> None - version = package_version("strawberry-graphql") + installed_packages = _get_installed_modules() + version = parse_version(installed_packages["strawberry-graphql"]) if version is None: raise DidNotEnable( diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index f9fb4ebfe3..c811d2d2fe 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -76,7 +76,6 @@ # The logger is created here but initialized in the debug support module logger = logging.getLogger("sentry_sdk.errors") -_installed_modules = None BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$") @@ -1564,56 +1563,6 @@ def parse_version(version): return release_tuple -def _generate_installed_modules(): - # type: () -> Iterator[Tuple[str, str]] - try: - from importlib import metadata - - for dist in metadata.distributions(): - name = dist.metadata["Name"] - # `metadata` values may be `None`, see: - # https://github.com/python/cpython/issues/91216 - # and - # https://github.com/python/importlib_metadata/issues/371 - if name is not None: - version = metadata.version(name) - if version is not None: - yield _normalize_module_name(name), version - - except ImportError: - # < py3.8 - try: - import pkg_resources - except ImportError: - return - - for info in pkg_resources.working_set: - yield _normalize_module_name(info.key), info.version - - -def _normalize_module_name(name): - # type: (str) -> str - return name.lower() - - -def _get_installed_modules(): - # type: () -> Dict[str, str] - global _installed_modules - if _installed_modules is None: - _installed_modules = dict(_generate_installed_modules()) - return _installed_modules - - -def package_version(package): - # type: (str) -> Optional[Tuple[int, ...]] - installed_packages = _get_installed_modules() - version = installed_packages.get(package) - if version is None: - return None - - return parse_version(version) - - if PY37: def nanosecond_time(): diff --git a/tests/integrations/modules/test_modules.py b/tests/integrations/modules/test_modules.py index 3f4d7bd9dc..c7097972b0 100644 --- a/tests/integrations/modules/test_modules.py +++ b/tests/integrations/modules/test_modules.py @@ -1,6 +1,22 @@ +import pytest +import re import sentry_sdk -from sentry_sdk.integrations.modules import ModulesIntegration +from sentry_sdk.integrations.modules import ( + ModulesIntegration, + _get_installed_modules, +) + + +def _normalize_distribution_name(name): + # type: (str) -> str + """Normalize distribution name according to PEP-0503. + + See: + https://peps.python.org/pep-0503/#normalized-names + for more details. + """ + return re.sub(r"[-_.]+", "-", name).lower() def test_basic(sentry_init, capture_events): @@ -12,3 +28,44 @@ def test_basic(sentry_init, capture_events): (event,) = events assert "sentry-sdk" in event["modules"] assert "pytest" in event["modules"] + + +def test_installed_modules(): + try: + from importlib.metadata import distributions, version + + importlib_available = True + except ImportError: + importlib_available = False + + try: + import pkg_resources + + pkg_resources_available = True + except ImportError: + pkg_resources_available = False + + installed_distributions = { + _normalize_distribution_name(dist): version + for dist, version in _get_installed_modules().items() + } + + if importlib_available: + importlib_distributions = { + _normalize_distribution_name(dist.metadata["Name"]): version( + dist.metadata["Name"] + ) + for dist in distributions() + if dist.metadata["Name"] is not None + and version(dist.metadata["Name"]) is not None + } + assert installed_distributions == importlib_distributions + + elif pkg_resources_available: + pkg_resources_distributions = { + _normalize_distribution_name(dist.key): dist.version + for dist in pkg_resources.working_set + } + assert installed_distributions == pkg_resources_distributions + else: + pytest.fail("Neither importlib nor pkg_resources is available") diff --git a/tests/test_utils.py b/tests/test_utils.py index 66ff59f650..ee73433dd5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -15,7 +15,6 @@ sanitize_url, serialize_frame, is_sentry_url, - _get_installed_modules, ) import sentry_sdk @@ -26,17 +25,6 @@ import mock # python < 3.3 -def _normalize_distribution_name(name): - # type: (str) -> str - """Normalize distribution name according to PEP-0503. - - See: - https://peps.python.org/pep-0503/#normalized-names - for more details. - """ - return re.sub(r"[-_.]+", "-", name).lower() - - @pytest.mark.parametrize( ("url", "expected_result"), [ @@ -500,44 +488,3 @@ def test_get_error_message(error, expected_result): exc_value.detail = error raise Exception assert get_error_message(exc_value) == expected_result(exc_value) - - -def test_installed_modules(): - try: - from importlib.metadata import distributions, version - - importlib_available = True - except ImportError: - importlib_available = False - - try: - import pkg_resources - - pkg_resources_available = True - except ImportError: - pkg_resources_available = False - - installed_distributions = { - _normalize_distribution_name(dist): version - for dist, version in _get_installed_modules().items() - } - - if importlib_available: - importlib_distributions = { - _normalize_distribution_name(dist.metadata["Name"]): version( - dist.metadata["Name"] - ) - for dist in distributions() - if dist.metadata["Name"] is not None - and version(dist.metadata["Name"]) is not None - } - assert installed_distributions == importlib_distributions - - elif pkg_resources_available: - pkg_resources_distributions = { - _normalize_distribution_name(dist.key): dist.version - for dist in pkg_resources.working_set - } - assert installed_distributions == pkg_resources_distributions - else: - pytest.fail("Neither importlib nor pkg_resources is available") From 2ccd1eb3125decec6e58689b7c203632e44a59ff Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 10 Oct 2023 14:54:25 +0200 Subject: [PATCH 36/37] change to try..except --- sentry_sdk/integrations/strawberry.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index d2b3155ed8..63ddc44f25 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -366,8 +366,11 @@ def inner(event, hint): "operationName" ] = execution_context.operation_name - elif (event.get("request") or {}).get("data"): - del event["request"]["data"] + else: + try: + del event["request"]["data"] + except (KeyError, TypeError): + pass return event From ed52750d0e764e0755137ed243586a2faf420b11 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 10 Oct 2023 15:08:33 +0200 Subject: [PATCH 37/37] alias for parametrize --- .../strawberry/test_strawberry_py3.py | 97 ++++--------------- 1 file changed, 20 insertions(+), 77 deletions(-) diff --git a/tests/integrations/strawberry/test_strawberry_py3.py b/tests/integrations/strawberry/test_strawberry_py3.py index 784c6f73ae..b357779461 100644 --- a/tests/integrations/strawberry/test_strawberry_py3.py +++ b/tests/integrations/strawberry/test_strawberry_py3.py @@ -27,6 +27,19 @@ ) +parameterize_strawberry_test = pytest.mark.parametrize( + "client_factory,async_execution,framework_integrations", + ( + ( + "async_app_client_factory", + True, + [FastApiIntegration(), StarletteIntegration()], + ), + ("sync_app_client_factory", False, [FlaskIntegration()]), + ), +) + + @strawberry.type class Query: @strawberry.field @@ -134,17 +147,7 @@ def test_replace_existing_sentry_sync_extension(sentry_init): assert SentrySyncExtension in schema.extensions -@pytest.mark.parametrize( - "client_factory,async_execution,framework_integrations", - ( - ( - "async_app_client_factory", - True, - [FastApiIntegration(), StarletteIntegration()], - ), - ("sync_app_client_factory", False, [FlaskIntegration()]), - ), -) +@parameterize_strawberry_test def test_capture_request_if_available_and_send_pii_is_on( request, sentry_init, @@ -200,17 +203,7 @@ def test_capture_request_if_available_and_send_pii_is_on( } -@pytest.mark.parametrize( - "client_factory,async_execution,framework_integrations", - ( - ( - "async_app_client_factory", - True, - [FastApiIntegration(), StarletteIntegration()], - ), - ("sync_app_client_factory", False, [FlaskIntegration()]), - ), -) +@parameterize_strawberry_test def test_do_not_capture_request_if_send_pii_is_off( request, sentry_init, @@ -250,17 +243,7 @@ def test_do_not_capture_request_if_send_pii_is_off( } -@pytest.mark.parametrize( - "client_factory,async_execution,framework_integrations", - ( - ( - "async_app_client_factory", - True, - [FastApiIntegration(), StarletteIntegration()], - ), - ("sync_app_client_factory", False, [FlaskIntegration()]), - ), -) +@parameterize_strawberry_test def test_breadcrumb_no_operation_name( request, sentry_init, @@ -297,17 +280,7 @@ def test_breadcrumb_no_operation_name( } -@pytest.mark.parametrize( - "client_factory,async_execution,framework_integrations", - ( - ( - "async_app_client_factory", - True, - [FastApiIntegration(), StarletteIntegration()], - ), - ("sync_app_client_factory", False, [FlaskIntegration()]), - ), -) +@parameterize_strawberry_test def test_capture_transaction_on_error( request, sentry_init, @@ -386,17 +359,7 @@ def test_capture_transaction_on_error( } -@pytest.mark.parametrize( - "client_factory,async_execution,framework_integrations", - ( - ( - "async_app_client_factory", - True, - [FastApiIntegration(), StarletteIntegration()], - ), - ("sync_app_client_factory", False, [FlaskIntegration()]), - ), -) +@parameterize_strawberry_test def test_capture_transaction_on_success( request, sentry_init, @@ -474,17 +437,7 @@ def test_capture_transaction_on_success( } -@pytest.mark.parametrize( - "client_factory,async_execution,framework_integrations", - ( - ( - "async_app_client_factory", - True, - [FastApiIntegration(), StarletteIntegration()], - ), - ("sync_app_client_factory", False, [FlaskIntegration()]), - ), -) +@parameterize_strawberry_test def test_transaction_no_operation_name( request, sentry_init, @@ -562,17 +515,7 @@ def test_transaction_no_operation_name( } -@pytest.mark.parametrize( - "client_factory,async_execution,framework_integrations", - ( - ( - "async_app_client_factory", - True, - [FastApiIntegration(), StarletteIntegration()], - ), - ("sync_app_client_factory", False, [FlaskIntegration()]), - ), -) +@parameterize_strawberry_test def test_transaction_mutation( request, sentry_init,