From 9f67b35f6076a9efd1d876b7c920e9dbe14b0f21 Mon Sep 17 00:00:00 2001 From: Alexey Shalaev <75322386+AlexeyShalaev@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:33:45 +0400 Subject: [PATCH] docs: put Sentry inside the exception handler in the documented interceptor order The module docstring of grpc_server_kit.aio.interceptors listed AsyncSentryInterceptor before AsyncExceptionHandlerInterceptor, the opposite of the provider, the Sentry docstring and every guide. Followed as written, Sentry only sees the AbortError the handler produces and captures nothing. A test now compares the documented order with the chain the dishka provider builds. Fixes #49 --- grpc_server_kit/aio/interceptors/__init__.py | 8 ++++++-- tests/unit/aio/dishka/test_providers.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/grpc_server_kit/aio/interceptors/__init__.py b/grpc_server_kit/aio/interceptors/__init__.py index fc34315..0c2bebd 100644 --- a/grpc_server_kit/aio/interceptors/__init__.py +++ b/grpc_server_kit/aio/interceptors/__init__.py @@ -10,8 +10,12 @@ 2. ``AsyncContextInterceptor`` (extract metadata into context vars) 3. ``AsyncRequestLoggerInterceptor`` (request/response logging) 4. ``AsyncTracingInterceptor`` (tracing span) -5. ``AsyncSentryInterceptor`` (error tracking) -6. ``AsyncExceptionHandlerInterceptor`` (exception → gRPC status mapping) +5. ``AsyncExceptionHandlerInterceptor`` (exception → gRPC status mapping) +6. ``AsyncSentryInterceptor`` (error tracking) + +Sentry sits inside the exception handler so it sees the original exception; +outside, it would only see the ``grpc.aio.AbortError`` the handler turned it +into, and it never captures those. """ from .base import AsyncServerInterceptor, RpcCall, split_method_name diff --git a/tests/unit/aio/dishka/test_providers.py b/tests/unit/aio/dishka/test_providers.py index bdef599..161c044 100644 --- a/tests/unit/aio/dishka/test_providers.py +++ b/tests/unit/aio/dishka/test_providers.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from collections.abc import AsyncIterator, Awaitable, Callable from unittest.mock import MagicMock, patch @@ -10,6 +11,7 @@ from dishka import AsyncContainer, Provider, make_async_container from dishka.integrations.grpcio import DishkaAioInterceptor +from grpc_server_kit.aio import interceptors as interceptors_module from grpc_server_kit.aio.dishka import ( AsyncGrpcServerProvider, GrpcServerInterceptorsProvider, @@ -105,6 +107,22 @@ async def test__grpc_server_providers__default_bundle__wires_service_name_chain_ assert server.raw_server is not None +async def test__grpc_server_providers__default_bundle__matches_order_documented_in_interceptors_module( + container_factory: ContainerFactory, + make_settings: Callable[..., BaseGrpcServerSettings], +) -> None: + # Arrange + container = await container_factory(*grpc_server_providers(make_settings(), service_name=SERVICE)) + documented = re.findall(r"^\d+\. ``(\w+)``", interceptors_module.__doc__ or "", flags=re.MULTILINE) + + # Act + chain = await container.get(INTERCEPTORS_KEY) + + # Assert + # The module docstring is the order users copy by hand; it must not drift from the provider. + assert documented == [type(i).__name__ for i in chain if not isinstance(i, DishkaAioInterceptor)] + + async def test__grpc_server_interceptors_provider__sentry_and_tracing_disabled__drops_them_from_chain( container_factory: ContainerFactory, make_settings: Callable[..., BaseGrpcServerSettings],