diff --git a/.sampo/changesets/majestic-iceseeker-vainamoinen.md b/.sampo/changesets/majestic-iceseeker-vainamoinen.md new file mode 100644 index 00000000..92a6d39e --- /dev/null +++ b/.sampo/changesets/majestic-iceseeker-vainamoinen.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: patch +--- + +Cap capture v0 Retry-After delays diff --git a/posthog/consumer.py b/posthog/consumer.py index b60e156f..8976db9c 100644 --- a/posthog/consumer.py +++ b/posthog/consumer.py @@ -7,7 +7,7 @@ from posthog._logging import _configure_posthog_logging from posthog.capture_compression import CaptureCompression from posthog.capture_mode import CaptureMode -from posthog.capture_v1 import _send_v1_batch +from posthog.capture_v1 import _backoff, _send_v1_batch from posthog.request import ( EVENTS_ENDPOINT, APIError, @@ -209,12 +209,7 @@ def is_retryable(exc): if not is_retryable(e): raise if attempt < self.retries: - # Respect Retry-After header if present, otherwise use exponential backoff - retry_after = getattr(e, "retry_after", None) - if retry_after and retry_after > 0: - time.sleep(retry_after) - else: - time.sleep(min(2**attempt, 30)) + _backoff(attempt, getattr(e, "retry_after", None)) if last_exc: raise last_exc diff --git a/posthog/test/test_consumer.py b/posthog/test/test_consumer.py index ab582193..d9c96e54 100644 --- a/posthog/test/test_consumer.py +++ b/posthog/test/test_consumer.py @@ -1,6 +1,8 @@ import json import time import unittest +from datetime import datetime, timedelta, timezone +from email.utils import format_datetime from typing import Any from unittest import mock @@ -231,6 +233,44 @@ def mock_post(*args: Any, **kwargs: Any) -> None: ], ) + @parameterized.expand( + [ + ("huge_numeric", "1000000000", [30, 30]), + ("small_numeric", "0.25", [1, 2]), + ("huge_date", "Fri, 01 Jan 2100 00:00:00 GMT", [30, 30]), + ("small_date", None, [1, 2]), + ] + ) + def test_request_bounds_retry_after_without_reducing_attempts( + self, _name: str, retry_after_header: str | None, expected_sleeps: list[int] + ) -> None: + if retry_after_header is None: + retry_after_header = format_datetime( + datetime.now(timezone.utc) + timedelta(seconds=1), usegmt=True + ) + + retry_response = mock.Mock( + status_code=503, + headers={"Retry-After": retry_after_header}, + text="Service Unavailable", + ) + retry_response.json.return_value = {"detail": "Service Unavailable"} + success_response = mock.Mock(status_code=200) + session = mock.Mock() + session.post.side_effect = [retry_response, retry_response, success_response] + + consumer = Consumer(None, TEST_API_KEY, retries=2) + with ( + mock.patch("posthog.request._get_session", return_value=session), + mock.patch("posthog.consumer.time.sleep") as mock_sleep, + ): + consumer.request([_track_event()]) + + self.assertEqual(session.post.call_count, 3) + self.assertEqual( + [call.args[0] for call in mock_sleep.call_args_list], expected_sleeps + ) + def test_request_retries_on_408(self) -> None: call_count = [0]