Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4f8a00c
feat: Add experimental async transport (port of PR #4572)
BYK Mar 12, 2026
82c0094
fix: Suppress mypy await type error in AsyncHttpTransport._request
BYK Mar 12, 2026
4b77519
fix: Move httpcore[asyncio] from global test deps to specific envs
BYK Mar 12, 2026
c46fb6f
fix: Cancel _target task in AsyncWorker.kill() and improve sync close()
BYK Mar 12, 2026
5ea3aac
fix: Skip async tests under gevent
BYK Mar 12, 2026
ff85a58
fix: Remove from __future__ import annotations for Python 3.6 compat
BYK Mar 16, 2026
156f32b
test: Add comprehensive coverage tests for async transport
BYK Mar 16, 2026
cb932d2
fix: Make test_async_worker_start_no_running_loop sync
BYK Mar 16, 2026
d19271e
fix: Add asyncio.sleep(0) after worker.kill() to clean up coroutines
BYK Mar 16, 2026
299947d
fix: Handle CancelledError in AsyncWorker._target
BYK Mar 16, 2026
71007ec
fix: Suppress PytestUnraisableExceptionWarning for async worker tests
BYK Mar 16, 2026
183e83b
Merge origin/master into feat/async-transport
BYK Mar 20, 2026
8883b78
test: Add sync wrapper tests for async code paths (coverage)
BYK Mar 20, 2026
86d6e36
fix: Address Bugbot feedback — stale terminator and flush components
BYK Mar 20, 2026
e74f4a7
Merge origin/master into feat/async-transport
BYK Mar 23, 2026
91072bb
fix: Address bot feedback from merge
BYK Mar 23, 2026
d64517f
test: Add pure-sync mock-based tests for async code coverage
BYK Mar 23, 2026
94b6c73
fix: Capture queue ref at dispatch time in _on_task_complete
BYK Mar 23, 2026
38f97c2
refactor: Address reviewer feedback
BYK Mar 26, 2026
1ac4196
fix: Guard isinstance AsyncHttpTransport with ASYNC_TRANSPORT_ENABLED
BYK Mar 26, 2026
b990610
fix: Skip loop close test when async transport deps missing
BYK Mar 26, 2026
b392bc4
refactor: Simplify and deduplicate async transport code
BYK Mar 26, 2026
36ad606
fix: Remove unused type:ignore on async method overrides
BYK Mar 26, 2026
025714e
fix: Always add keep-alive socket options in httpcore transports
BYK Mar 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,6 @@ Do NOT edit these directly — modify source scripts instead:
| `sentry_sdk/profiler/` | Performance profiling |
| `tests/integrations/{name}/` | Integration test suites |
| `scripts/populate_tox/config.py` | Test suite configuration |

<!-- This section is maintained by the coding agent via lore (https://github.com/BYK/opencode-lore) -->
<!-- End lore-managed section -->
6 changes: 4 additions & 2 deletions scripts/populate_tox/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
"pytest-asyncio",
"python-multipart",
"requests",
"anyio<4",
"anyio>=3,<5",
"jinja2",
],
# There's an incompatibility between FastAPI's TestClient, which is
Expand All @@ -133,6 +133,7 @@
# FastAPI versions we use older httpx which still supports the
# deprecated argument.
"<0.110.1": ["httpx<0.28.0"],
"<0.80": ["anyio<4"],
"py3.6": ["aiocontextvars"],
},
},
Expand Down Expand Up @@ -171,7 +172,8 @@
"httpx": {
"package": "httpx",
"deps": {
"*": ["anyio<4.0.0"],
"*": ["anyio>=3,<5"],
"<0.24": ["anyio<4"],
">=0.16,<0.17": ["pytest-httpx==0.10.0"],
">=0.17,<0.19": ["pytest-httpx==0.12.0"],
">=0.19,<0.21": ["pytest-httpx==0.14.0"],
Expand Down
1 change: 1 addition & 0 deletions sentry_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"configure_scope",
"continue_trace",
"flush",
"flush_async",
"get_baggage",
"get_client",
"get_global_scope",
Expand Down
9 changes: 9 additions & 0 deletions sentry_sdk/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def overload(x: "T") -> "T":
"configure_scope",
"continue_trace",
"flush",
"flush_async",
"get_baggage",
"get_client",
"get_global_scope",
Expand Down Expand Up @@ -351,6 +352,14 @@ def flush(
return get_client().flush(timeout=timeout, callback=callback)


@clientmethod
async def flush_async(
timeout: "Optional[float]" = None,
callback: "Optional[Callable[[int, float], None]]" = None,
) -> None:
return await get_client().flush_async(timeout=timeout, callback=callback)


@scopemethod
def start_span(
**kwargs: "Any",
Expand Down
127 changes: 108 additions & 19 deletions sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@
from sentry_sdk.serializer import serialize
from sentry_sdk.tracing import trace
from sentry_sdk.tracing_utils import has_span_streaming_enabled
from sentry_sdk.transport import BaseHttpTransport, make_transport
from sentry_sdk.transport import (
HttpTransportCore,
make_transport,
AsyncHttpTransport,
)
from sentry_sdk.consts import (
SPANDATA,
DEFAULT_MAX_VALUE_LENGTH,
Expand Down Expand Up @@ -251,6 +255,12 @@ def close(self, *args: "Any", **kwargs: "Any") -> None:
def flush(self, *args: "Any", **kwargs: "Any") -> None:
return None

async def close_async(self, *args: "Any", **kwargs: "Any") -> None:
return None

async def flush_async(self, *args: "Any", **kwargs: "Any") -> None:
return None

def __enter__(self) -> "BaseClient":
return self

Expand Down Expand Up @@ -472,7 +482,7 @@ def _record_lost_event(
or self.metrics_batcher
or self.span_batcher
or has_profiling_enabled(self.options)
or isinstance(self.transport, BaseHttpTransport)
or isinstance(self.transport, HttpTransportCore)
):
# If we have anything on that could spawn a background thread, we
# need to check if it's safe to use them.
Expand Down Expand Up @@ -999,6 +1009,32 @@ def get_integration(

return self.integrations.get(integration_name)

def _is_async_transport(self) -> bool:
"""Check if the current transport is async."""
return isinstance(self.transport, AsyncHttpTransport)

@property
def _batchers(self) -> "tuple[Any, ...]":
return tuple(
b
for b in (self.log_batcher, self.metrics_batcher, self.span_batcher)
if b is not None
)

def _close_components(self) -> None:
"""Kill all client components in the correct order."""
self.session_flusher.kill()
for b in self._batchers:
b.kill()
if self.monitor:
self.monitor.kill()

def _flush_components(self) -> None:
"""Flush all client components."""
self.session_flusher.flush()
for b in self._batchers:
b.flush()

def close(
self,
timeout: "Optional[float]" = None,
Expand All @@ -1009,19 +1045,41 @@ def close(
semantics as :py:meth:`Client.flush`.
"""
if self.transport is not None:
self.flush(timeout=timeout, callback=callback)
self.session_flusher.kill()
if self.log_batcher is not None:
self.log_batcher.kill()
if self.metrics_batcher is not None:
self.metrics_batcher.kill()
if self.span_batcher is not None:
self.span_batcher.kill()
if self.monitor:
self.monitor.kill()
if self._is_async_transport():
logger.warning(
"close() used with AsyncHttpTransport. "
"Prefer close_async() for graceful async shutdown. "
"Performing synchronous best-effort cleanup."
)
self._flush_components()
else:
self.flush(timeout=timeout, callback=callback)
self._close_components()
self.transport.kill()
self.transport = None

async def close_async(
self,
timeout: "Optional[float]" = None,
callback: "Optional[Callable[[int, float], None]]" = None,
) -> None:
"""
Asynchronously close the client and shut down the transport. Arguments have the same
semantics as :py:meth:`Client.flush_async`.
"""
if self.transport is not None:
if not self._is_async_transport():
logger.debug(
"close_async() used with non-async transport, aborting. Please use close() instead."
)
return
await self.flush_async(timeout=timeout, callback=callback)
self._close_components()
kill_task = self.transport.kill() # type: ignore
if kill_task is not None:
await kill_task
self.transport = None

def flush(
self,
timeout: "Optional[float]" = None,
Expand All @@ -1035,23 +1093,54 @@ def flush(
:param callback: Is invoked with the number of pending events and the configured timeout.
"""
if self.transport is not None:
if self._is_async_transport():
logger.warning(
"flush() used with AsyncHttpTransport. Please use flush_async() instead."
)
return
if timeout is None:
timeout = self.options["shutdown_timeout"]
self.session_flusher.flush()
if self.log_batcher is not None:
self.log_batcher.flush()
if self.metrics_batcher is not None:
self.metrics_batcher.flush()
if self.span_batcher is not None:
self.span_batcher.flush()
self._flush_components()

self.transport.flush(timeout=timeout, callback=callback)

async def flush_async(
self,
timeout: "Optional[float]" = None,
callback: "Optional[Callable[[int, float], None]]" = None,
) -> None:
"""
Asynchronously wait for the current events to be sent.

:param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used.

:param callback: Is invoked with the number of pending events and the configured timeout.
"""
if self.transport is not None:
if not self._is_async_transport():
logger.debug(
"flush_async() used with non-async transport, aborting. Please use flush() instead."
)
return
if timeout is None:
timeout = self.options["shutdown_timeout"]
self._flush_components()
flush_task = self.transport.flush(timeout=timeout, callback=callback) # type: ignore
if flush_task is not None:
await flush_task

def __enter__(self) -> "_Client":
return self

def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None:
self.close()

async def __aenter__(self) -> "_Client":
return self

async def __aexit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None:
await self.close_async()


from typing import TYPE_CHECKING

Expand Down
1 change: 1 addition & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class CompressionAlgo(Enum):
"transport_compression_algo": Optional[CompressionAlgo],
"transport_num_pools": Optional[int],
"transport_http2": Optional[bool],
"transport_async": Optional[bool],
"enable_logs": Optional[bool],
"before_send_log": Optional[Callable[[Log, Hint], Optional[Log]]],
"enable_metrics": Optional[bool],
Expand Down
Loading
Loading