feat: add asyncio capture client - #899
Conversation
posthog-python Compliance ReportDate: 2026-08-27 20:06:19 UTC ✅ All Tests Passed!111/111 tests passed Capture_V1 Tests✅ 94/94 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
Usage examplesThe three client modes are intended for different application lifecycles. Long-running synchronous applicationsUse the regular client for Django, Flask, workers, and other long-running synchronous processes. import posthog
client = posthog.Client(
project_api_key="phc_project_key",
host="https://us.i.posthog.com",
)
client.capture(
"order completed",
distinct_id="user-123",
properties={"order_id": "order-456"},
)
# During application shutdown:
client.shutdown()Short-lived synchronous processesUse import posthog
client = posthog.Client(
project_api_key="phc_project_key",
host="https://us.i.posthog.com",
sync_mode=True,
)
event_id = client.capture(
"job completed",
distinct_id="user-123",
)
if event_id is None:
print("PostHog delivery attempt failed")Avoid this mode in high-throughput or latency-sensitive request handlers. Asyncio applicationsUse import posthog
async def run():
async with posthog.AsyncPosthog(
project_api_key="phc_project_key",
host="https://us.i.posthog.com",
) as client:
# Buffered capture: fast and intentionally not awaited.
client.capture(
"background event",
distinct_id="user-123",
)
# Immediate capture: bypasses batching and waits for the request/retries.
event_id = await client.capture_immediate(
"important event",
distinct_id="user-123",
)
if event_id is None:
print("PostHog delivery attempt failed")Leaving the async context flushes queued events and closes the client-owned HTTP transport. |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 2 · PR risk: 0/10 |
|
dustinbyrne
left a comment
There was a problem hiding this comment.
Agent-led review, human-reviewed before posting.
|
|
|
|
|
|
|
Thanks for the detailed reproductions, @arnohillen. Addressed in I first added focused tests on
The same focused suite now passes (5 tests, including cross-origin redirect rejection and the shutdown-admission race). The implementation now:
Full validation: 2284 tests passed, Ruff/mypy/public API/import checks passed, and the final autoreview reported no actionable findings. |
💡 Motivation and Context
Implements the capture foundation for #103. This supersedes the capture work in #719 and incorporates its open review feedback.
This is additive. Existing
Client,Posthog, and module-level APIs are unchanged. The newAsyncClientandAsyncPosthogclasses are available through the optionalposthog[async]extra, while plainimport posthogcontinues to work withouthttpxinstalled.capture()remains a synchronous, non-blocking queue write, matching the existing server SDK behavior discussed on #719. Callers can useawait capture_immediate()when they need to wait for a delivery attempt. The client owns an asyncio queue, workers, and an instance-scoped HTTP transport. Shutdown stops admission, delivers accepted work, waits for in-flight immediate captures, and closes the transport without cancelling batches in progress.The client is standalone instead of inheriting from the synchronous
Client. It supports both capture wire protocols. Capture v0 useshttpx, while the existing v1 partial-retry submitter runs off the event loop to preserve its established protocol behavior.💚 How did you test it?
uv run ruff format --check .uv run ruff check .uv run mypy --no-site-packages --config-file mypy.ini . | uv run mypy-baseline filteruv run --extra test pytest --verbose --timeout=30- 2284 passed, 15 skipped, 36 subtests passeduv run --extra dev make public_api_checkuv lock --checkuv run python -W error -c "import posthog"posthoganalyticsmirror with the new classesorigin/mainreported no actionable findings at54cf24c📝 Checklist
If releasing new changes
sampo addto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Implemented with pi. We replaced the old inheritance-based design with a standalone client, kept buffered capture synchronous, and added an awaitable immediate-delivery method. Review findings from #719 guided the lifecycle, transport ownership, event-loop blocking, retry, logging, and compatibility tests.