Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .sampo/changesets/on-drop-callback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
pypi/posthog: minor
---

Add an on_drop callback invoked with the event when the internal queue is full, so callers can observe or count dropped events. The SDK always drops rather than blocking the caller.
14 changes: 14 additions & 0 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ def __init__(
exception_autocapture_bucket_size=ExceptionCapture.DEFAULT_BUCKET_SIZE,
exception_autocapture_refill_rate=ExceptionCapture.DEFAULT_REFILL_RATE,
exception_autocapture_refill_interval_seconds=ExceptionCapture.DEFAULT_REFILL_INTERVAL_SECONDS,
on_drop=None,
_dedicated_ai_endpoint=False,
):
"""
Expand Down Expand Up @@ -343,6 +344,10 @@ def __init__(
interval for each exception type's bucket.
exception_autocapture_refill_interval_seconds: Seconds between
token refills for autocaptured exception rate limiting.
on_drop: Optional callback invoked with the dropped event when the
internal queue is full. The SDK always drops rather than blocking
the caller, so this is how you observe or count dropped events.
Runs on the calling thread, so keep it fast and non-blocking.

Examples:
```python
Expand All @@ -354,6 +359,10 @@ def __init__(
Category:
Initialization
"""
if on_drop is not None and not callable(on_drop):
raise ValueError("on_drop must be callable")
self.on_drop = on_drop

self._max_queue_size = max_queue_size
self.queue: Queue = Queue(max_queue_size)

Expand Down Expand Up @@ -1647,6 +1656,11 @@ def _enqueue(self, msg, disable_geoip):
return sent_uuid
except Full:
self.log.warning("analytics-python queue is full")
if self.on_drop is not None:
try:
self.on_drop(msg)
except Exception:
self.log.exception("on_drop callback raised")
return None

def flush(self, timeout_seconds: Optional[float] = 10) -> None:
Expand Down
34 changes: 34 additions & 0 deletions posthog/test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2092,6 +2092,40 @@ def test_overflow(self):
# Make sure we are informed that the queue is at capacity
self.assertIsNone(msg_uuid)

def test_on_drop_called_when_queue_full(self):
dropped = []
client = Client(FAKE_TEST_API_KEY, max_queue_size=1, on_drop=dropped.append)
client.join()
first_uuid = client.capture("first event", distinct_id="distinct_id")
self.assertIsNotNone(first_uuid)

second_uuid = client.capture("second event", distinct_id="distinct_id")
self.assertIsNone(second_uuid)
self.assertEqual(len(dropped), 1)
self.assertEqual(dropped[0]["event"], "second event")

def test_on_drop_not_called_when_space_available(self):
dropped = []
client = Client(FAKE_TEST_API_KEY, max_queue_size=10, on_drop=dropped.append)
client.join()
first_uuid = client.capture("first event", distinct_id="distinct_id")
self.assertIsNotNone(first_uuid)
self.assertEqual(dropped, [])

def test_on_drop_callback_exception_is_swallowed(self):
def boom(_msg):
raise RuntimeError("boom")

client = Client(FAKE_TEST_API_KEY, max_queue_size=1, on_drop=boom)
client.join()
client.capture("first event", distinct_id="distinct_id")
second_uuid = client.capture("second event", distinct_id="distinct_id")
self.assertIsNone(second_uuid)

def test_on_drop_invalid_config(self):
with self.assertRaises(ValueError):
Client(FAKE_TEST_API_KEY, on_drop="not callable")

def test_unicode(self):
Client("unicode_key")

Expand Down