diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000..7aef45f4d0 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,8 @@ +Release type: minor + +After a year-long deprecation period, the `SentryTracingExtension` has been +removed in favor of the official Sentry SDK integration. + +To migrate, remove the `SentryTracingExtension` from your Strawberry schema and +then follow the +[official Sentry SDK integration guide](https://docs.sentry.io/platforms/python/integrations/strawberry/). diff --git a/TWEET.md b/TWEET.md new file mode 100644 index 0000000000..284e1c2ba1 --- /dev/null +++ b/TWEET.md @@ -0,0 +1,8 @@ +After a year-long deprecation period, the SentryTracingExtension has +been removed in favor of the official Sentry SDK integration. + +Checkout out our migration guides if you have not migrated yet. + +Thanks to $contributor for the PR 👏 + +$release_url diff --git a/docs/breaking-changes.md b/docs/breaking-changes.md index 3df11e2b23..c87fabd804 100644 --- a/docs/breaking-changes.md +++ b/docs/breaking-changes.md @@ -4,6 +4,7 @@ title: List of breaking changes and deprecations # List of breaking changes and deprecations +- [Version 0.249.0 - 18 November 2024](./breaking-changes/0.249.0.md) - [Version 0.243.0 - 25 September 2024](./breaking-changes/0.243.0.md) - [Version 0.240.0 - 10 September 2024](./breaking-changes/0.240.0.md) - [Version 0.236.0 - 17 July 2024](./breaking-changes/0.236.0.md) diff --git a/docs/breaking-changes/0.249.0.md b/docs/breaking-changes/0.249.0.md new file mode 100644 index 0000000000..7d0a8f8951 --- /dev/null +++ b/docs/breaking-changes/0.249.0.md @@ -0,0 +1,13 @@ +--- +title: 0.249.0 Breaking Changes +slug: breaking-changes/0.249.0 +--- + +# v0.249.0 Breaking Changes + +After a year-long deprecation period, the `SentryTracingExtension` has been +removed in favor of the official Sentry SDK integration. + +To migrate, remove the `SentryTracingExtension` from your Strawberry schema and +then follow the +[official Sentry SDK integration guide](https://docs.sentry.io/platforms/python/integrations/strawberry/). diff --git a/docs/extensions/sentry-tracing.md b/docs/extensions/sentry-tracing.md index 52b70d3582..e62359783d 100644 --- a/docs/extensions/sentry-tracing.md +++ b/docs/extensions/sentry-tracing.md @@ -4,68 +4,16 @@ summary: Add Sentry tracing to your GraphQL server. tags: tracing --- - - -As of Sentry 1.32.0, Strawberry is now supported by default. This extension is -no longer necessary. For more details, please refer to the -[release notes](https://github.com/getsentry/sentry-python/releases/tag/1.32.0). - -Below is the revised usage example: - -```python -import sentry_sdk -from sentry_sdk.integrations.strawberry import StrawberryIntegration - -sentry_sdk.init( - dsn="___PUBLIC_DSN___", - integrations=[ - # make sure to set async_execution to False if you're executing - # GraphQL queries synchronously - StrawberryIntegration(async_execution=True), - ], - traces_sample_rate=1.0, -) -``` - - - # `SentryTracingExtension` -This extension adds support for tracing with Sentry. - -## Usage example: - -```python -import strawberry -from strawberry.extensions.tracing import SentryTracingExtension - -schema = strawberry.Schema( - Query, - extensions=[ - SentryTracingExtension, - ], -) -``` - - - -If you are not running in an Async context then you'll need to use the sync -version: - -```python -import strawberry -from strawberry.extensions.tracing import SentryTracingExtensionSync - -schema = strawberry.Schema( - Query, - extensions=[ - SentryTracingExtensionSync, - ], -) -``` + - +As of Sentry 1.32.0, Strawberry is officially supported by the Sentry SDK. +Therefore, Strawberry's `SentryTracingExtension` has been deprecated in version +0.210.0 and finally removed with Strawberry 0.249.0 in favor of the official +Sentry SDK integration. -## API reference: +For more details, please refer to the +[documentation for the official Sentry Strawberry integration](https://docs.sentry.io/platforms/python/integrations/strawberry/). -_No arguments_ + diff --git a/poetry.lock b/poetry.lock index 898f856ef7..b02f9c6899 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3474,53 +3474,6 @@ files = [ cryptography = ">=2.0" jeepney = ">=0.6" -[[package]] -name = "sentry-sdk" -version = "1.45.1" -description = "Python client for Sentry (https://sentry.io)" -optional = false -python-versions = "*" -files = [ - {file = "sentry_sdk-1.45.1-py2.py3-none-any.whl", hash = "sha256:608887855ccfe39032bfd03936e3a1c4f4fc99b3a4ac49ced54a4220de61c9c1"}, - {file = "sentry_sdk-1.45.1.tar.gz", hash = "sha256:a16c997c0f4e3df63c0fc5e4207ccb1ab37900433e0f72fef88315d317829a26"}, -] - -[package.dependencies] -certifi = "*" -urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""} - -[package.extras] -aiohttp = ["aiohttp (>=3.5)"] -arq = ["arq (>=0.23)"] -asyncpg = ["asyncpg (>=0.23)"] -beam = ["apache-beam (>=2.12)"] -bottle = ["bottle (>=0.12.13)"] -celery = ["celery (>=3)"] -celery-redbeat = ["celery-redbeat (>=2)"] -chalice = ["chalice (>=1.16.0)"] -clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] -django = ["django (>=1.8)"] -falcon = ["falcon (>=1.4)"] -fastapi = ["fastapi (>=0.79.0)"] -flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] -grpcio = ["grpcio (>=1.21.1)"] -httpx = ["httpx (>=0.16.0)"] -huey = ["huey (>=2)"] -loguru = ["loguru (>=0.5)"] -openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] -opentelemetry = ["opentelemetry-distro (>=0.35b0)"] -opentelemetry-experimental = ["opentelemetry-distro (>=0.40b0,<1.0)", "opentelemetry-instrumentation-aiohttp-client (>=0.40b0,<1.0)", "opentelemetry-instrumentation-django (>=0.40b0,<1.0)", "opentelemetry-instrumentation-fastapi (>=0.40b0,<1.0)", "opentelemetry-instrumentation-flask (>=0.40b0,<1.0)", "opentelemetry-instrumentation-requests (>=0.40b0,<1.0)", "opentelemetry-instrumentation-sqlite3 (>=0.40b0,<1.0)", "opentelemetry-instrumentation-urllib (>=0.40b0,<1.0)"] -pure-eval = ["asttokens", "executing", "pure-eval"] -pymongo = ["pymongo (>=3.1)"] -pyspark = ["pyspark (>=2.4.4)"] -quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] -rq = ["rq (>=0.6)"] -sanic = ["sanic (>=0.8)"] -sqlalchemy = ["sqlalchemy (>=1.2)"] -starlette = ["starlette (>=0.19.1)"] -starlite = ["starlite (>=1.48)"] -tornado = ["tornado (>=5)"] - [[package]] name = "service-identity" version = "24.1.0" @@ -4595,4 +4548,4 @@ sanic = ["sanic"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "ed925030eb8c2f2baa836c6f64990e2f4eabfb2b1c177294623671d1e6760081" +content-hash = "5b4688f203c3b744c228b13f33c6ed30515e00bf259a4c5ddd83d270293cda08" diff --git a/pyproject.toml b/pyproject.toml index 44b70dc1ed..06ef141d92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,6 @@ pytest-xdist = {extras = ["psutil"], version = "^3.1.0"} python-multipart = ">=0.0.7" rich = {version = ">=12.5.1", optional = false} sanic-testing = ">=22.9,<24.0" -sentry-sdk = "^1.39.2" typer = {version = ">=0.7.0", optional = false} types-aiofiles = ">=22.1" types-certifi = "^2021.10.8" diff --git a/strawberry/extensions/tracing/__init__.py b/strawberry/extensions/tracing/__init__.py index 772b18dbf0..593ed0b2ea 100644 --- a/strawberry/extensions/tracing/__init__.py +++ b/strawberry/extensions/tracing/__init__.py @@ -8,7 +8,6 @@ OpenTelemetryExtension, OpenTelemetryExtensionSync, ) - from .sentry import SentryTracingExtension, SentryTracingExtensionSync __all__ = [ "ApolloTracingExtension", @@ -17,8 +16,6 @@ "DatadogTracingExtensionSync", "OpenTelemetryExtension", "OpenTelemetryExtensionSync", - "SentryTracingExtension", - "SentryTracingExtensionSync", ] @@ -32,7 +29,4 @@ def __getattr__(name: str) -> Any: if name in {"OpenTelemetryExtension", "OpenTelemetryExtensionSync"}: return getattr(importlib.import_module(".opentelemetry", __name__), name) - if name in {"SentryTracingExtension", "SentryTracingExtensionSync"}: - return getattr(importlib.import_module(".sentry", __name__), name) - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/strawberry/extensions/tracing/sentry.py b/strawberry/extensions/tracing/sentry.py deleted file mode 100644 index 7a0c6188b4..0000000000 --- a/strawberry/extensions/tracing/sentry.py +++ /dev/null @@ -1,161 +0,0 @@ -from __future__ import annotations - -import hashlib -import warnings -from functools import cached_property -from inspect import isawaitable -from typing import TYPE_CHECKING, Any, Callable, Generator, Optional - -from sentry_sdk import configure_scope, start_span - -from strawberry.extensions import SchemaExtension -from strawberry.extensions.tracing.utils import should_skip_tracing - -if TYPE_CHECKING: - from graphql import GraphQLResolveInfo - - from strawberry.types.execution import ExecutionContext - - -class SentryTracingExtension(SchemaExtension): - def __init__( - self, - *, - execution_context: Optional[ExecutionContext] = None, - ) -> None: - warnings.warn( - "The Sentry tracing extension is deprecated, please update to sentry-sdk>=1.32.0", - DeprecationWarning, - stacklevel=2, - ) - - if execution_context: - self.execution_context = execution_context - - @cached_property - def _resource_name(self) -> str: - 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: str) -> str: - return hashlib.md5(query.encode("utf-8")).hexdigest() - - def on_operation(self) -> 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) -> 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) -> 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: Callable, info: GraphQLResolveInfo) -> bool: - return should_skip_tracing(_next, info) - - async def resolve( - self, - _next: Callable, - root: Any, - info: GraphQLResolveInfo, - *args: str, - **kwargs: 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 SentryTracingExtensionSync(SentryTracingExtension): - def resolve( - self, - _next: Callable, - root: Any, - info: GraphQLResolveInfo, - *args: str, - **kwargs: 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) - - -__all__ = ["SentryTracingExtension", "SentryTracingExtensionSync"] diff --git a/tests/schema/extensions/test_sentry.py b/tests/schema/extensions/test_sentry.py deleted file mode 100644 index 696818999d..0000000000 --- a/tests/schema/extensions/test_sentry.py +++ /dev/null @@ -1,368 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, AsyncGenerator, Tuple, Type -from unittest.mock import MagicMock - -import pytest -from pytest_mock import MockerFixture - -import strawberry - -if TYPE_CHECKING: - from strawberry.extensions.tracing.sentry import ( - SentryTracingExtension, - SentryTracingExtensionSync, - ) - - -@pytest.fixture -def sentry_extension( - mocker: MockerFixture, -) -> Tuple[Type[SentryTracingExtension], MagicMock]: - sentry_mock = mocker.MagicMock() - - mocker.patch.dict("sys.modules", sentry_sdk=sentry_mock) - - from strawberry.extensions.tracing.sentry import SentryTracingExtension - - return SentryTracingExtension, sentry_mock - - -@pytest.fixture -def sentry_extension_sync( - mocker: MockerFixture, -) -> Tuple[Type[SentryTracingExtension], MagicMock]: - sentry_mock = mocker.MagicMock() - - mocker.patch.dict("sys.modules", sentry_sdk=sentry_mock) - - from strawberry.extensions.tracing.sentry import SentryTracingExtensionSync - - return SentryTracingExtensionSync, sentry_mock - - -@strawberry.type -class Person: - name: str = "Jack" - - -@strawberry.type -class Query: - @strawberry.field - def person(self) -> Person: - return Person() - - @strawberry.field - async def person_async(self) -> Person: - return Person() - - -@strawberry.type -class Mutation: - @strawberry.mutation - def say_hi(self) -> str: - return "hello" - - -@strawberry.type -class Subscription: - @strawberry.field - async def on_hi(self) -> AsyncGenerator[str, None]: - yield "Hello" - - -@pytest.mark.asyncio -async def test_sentry_tracer( - sentry_extension: Tuple[SentryTracingExtension, MagicMock], mocker: MockerFixture -): - extension, mock = sentry_extension - - schema = strawberry.Schema( - query=Query, - mutation=Mutation, - extensions=[extension], - ) - - query = """ - query { - personAsync { - name - } - } - """ - - with pytest.warns( - DeprecationWarning, match="The Sentry tracing extension is deprecated" - ): - await schema.execute(query) - - assert mock.configure_scope.mock_calls == [ - mocker.call(), - mocker.call().__enter__(), - mocker.call().__enter__().span.__bool__(), - mocker.call() - .__enter__() - .span.start_child(op="gql", description="Anonymous Query"), - mocker.call().__exit__(None, None, None), - mocker.call() - .__enter__() - .span.start_child() - .set_tag("graphql.operation_type", "query"), - mocker.call() - .__enter__() - .span.start_child() - .set_tag("graphql.resource_name", "63a280256ca4e8514e06cf90b30c8c3a"), - mocker.call().__enter__().span.start_child().set_data("graphql.query", query), - mocker.call() - .__enter__() - .span.start_child() - .start_child(op="parsing", description="Parsing"), - mocker.call().__enter__().span.start_child().start_child().finish(), - mocker.call() - .__enter__() - .span.start_child() - .start_child(op="validation", description="Validation"), - mocker.call().__enter__().span.start_child().start_child().finish(), - mocker.call() - .__enter__() - .span.start_child() - .start_child(op="resolve", description="Resolving: Query.personAsync"), - mocker.call().__enter__().span.start_child().start_child().__enter__(), - mocker.call() - .__enter__() - .span.start_child() - .start_child() - .__enter__() - .set_tag("graphql.field_name", "personAsync"), - mocker.call() - .__enter__() - .span.start_child() - .start_child() - .__enter__() - .set_tag("graphql.parent_type", "Query"), - mocker.call() - .__enter__() - .span.start_child() - .start_child() - .__enter__() - .set_tag("graphql.field_path", "Query.personAsync"), - mocker.call() - .__enter__() - .span.start_child() - .start_child() - .__enter__() - .set_tag("graphql.path", "personAsync"), - mocker.call() - .__enter__() - .span.start_child() - .start_child() - .__exit__(None, None, None), - mocker.call().__enter__().span.start_child().finish(), - ] - - -@pytest.mark.asyncio -async def test_uses_operation_name( - sentry_extension: Tuple[SentryTracingExtension, MagicMock], -): - extension, mock = sentry_extension - - schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) - - query = """ - query MyExampleQuery { - person { - name - } - } - """ - - with pytest.warns( - DeprecationWarning, match="The Sentry tracing extension is deprecated" - ): - await schema.execute(query, operation_name="MyExampleQuery") - - mock.configure_scope().__enter__().span.start_child.assert_any_call( - op="gql", description="MyExampleQuery" - ) - - -@pytest.mark.asyncio -async def test_uses_operation_type( - sentry_extension: Tuple[SentryTracingExtension, MagicMock], -): - extension, mock = sentry_extension - - schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) - - query = """ - mutation MyMutation { - sayHi - } - """ - - with pytest.warns( - DeprecationWarning, match="The Sentry tracing extension is deprecated" - ): - await schema.execute(query, operation_name="MyMutation") - - mock.configure_scope().__enter__().span.start_child().set_tag.assert_any_call( - "graphql.operation_type", "mutation" - ) - - -@pytest.mark.asyncio -async def test_uses_operation_subscription( - sentry_extension: Tuple[SentryTracingExtension, MagicMock], -): - extension, mock = sentry_extension - - schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) - - query = """ - subscription MySubscription { - onHi - } - """ - - with pytest.warns( - DeprecationWarning, match="The Sentry tracing extension is deprecated" - ): - await schema.execute(query, operation_name="MySubscription") - - mock.configure_scope().__enter__().span.start_child().set_tag.assert_any_call( - "graphql.operation_type", "subscription" - ) - - -def test_sentry_tracer_sync( - sentry_extension_sync: Tuple[SentryTracingExtensionSync, MagicMock], - mocker: MockerFixture, -): - extension, mock = sentry_extension_sync - schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) - - query = """ - query { - person { - name - } - } - """ - - with pytest.warns( - DeprecationWarning, match="The Sentry tracing extension is deprecated" - ): - schema.execute_sync(query) - - assert mock.configure_scope.mock_calls == [ - mocker.call(), - mocker.call().__enter__(), - mocker.call().__enter__().span.__bool__(), - mocker.call() - .__enter__() - .span.start_child(op="gql", description="Anonymous Query"), - mocker.call().__exit__(None, None, None), - mocker.call() - .__enter__() - .span.start_child() - .set_tag("graphql.operation_type", "query"), - mocker.call() - .__enter__() - .span.start_child() - .set_tag("graphql.resource_name", "659edba9e6ac9c20d03da1b2d0f9a956"), - mocker.call().__enter__().span.start_child().set_data("graphql.query", query), - mocker.call() - .__enter__() - .span.start_child() - .start_child(op="parsing", description="Parsing"), - mocker.call().__enter__().span.start_child().start_child().finish(), - mocker.call() - .__enter__() - .span.start_child() - .start_child(op="validation", description="Validation"), - mocker.call().__enter__().span.start_child().start_child().finish(), - mocker.call() - .__enter__() - .span.start_child() - .start_child(op="resolve", description="Resolving: Query.person"), - mocker.call().__enter__().span.start_child().start_child().__enter__(), - mocker.call() - .__enter__() - .span.start_child() - .start_child() - .__enter__() - .set_tag("graphql.field_name", "person"), - mocker.call() - .__enter__() - .span.start_child() - .start_child() - .__enter__() - .set_tag("graphql.parent_type", "Query"), - mocker.call() - .__enter__() - .span.start_child() - .start_child() - .__enter__() - .set_tag("graphql.field_path", "Query.person"), - mocker.call() - .__enter__() - .span.start_child() - .start_child() - .__enter__() - .set_tag("graphql.path", "person"), - mocker.call() - .__enter__() - .span.start_child() - .start_child() - .__exit__(None, None, None), - mocker.call().__enter__().span.start_child().finish(), - ] - - -def test_uses_operation_name_sync( - sentry_extension_sync: Tuple[SentryTracingExtensionSync, MagicMock], -): - extension, mock = sentry_extension_sync - - schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) - - query = """ - query MyExampleQuery { - person { - name - } - } - """ - - with pytest.warns( - DeprecationWarning, match="The Sentry tracing extension is deprecated" - ): - schema.execute_sync(query, operation_name="MyExampleQuery") - - mock.configure_scope().__enter__().span.start_child.assert_any_call( - op="gql", description="MyExampleQuery" - ) - - -def test_uses_operation_type_sync( - sentry_extension_sync: Tuple[SentryTracingExtensionSync, MagicMock], -): - extension, mock = sentry_extension_sync - - schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) - - query = """ - mutation MyMutation { - sayHi - } - """ - - with pytest.warns( - DeprecationWarning, match="The Sentry tracing extension is deprecated" - ): - schema.execute_sync(query, operation_name="MyMutation") - - mock.configure_scope().__enter__().span.start_child().set_tag.assert_any_call( - "graphql.operation_type", "mutation" - )