Skip to content

Commit 72c24b8

Browse files
fix: return the uuid the wire event carries
Generated-By: PostHog Code Task-Id: 4b5e5cda-81f1-41a2-8fcd-4f335d19217e
1 parent b31c46f commit 72c24b8

3 files changed

Lines changed: 98 additions & 13 deletions

File tree

‎posthog/client.py‎

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2147,6 +2147,21 @@ def _reinit_after_fork(self):
21472147
else:
21482148
self.poller = None
21492149

2150+
def _normalize_event_uuid(self, msg):
2151+
# type: (...) -> None
2152+
"""Ensure `msg["uuid"]` is a valid uuid string, generating one if missing or invalid."""
2153+
if "uuid" in msg:
2154+
uuid = msg.pop("uuid")
2155+
if uuid is not None:
2156+
try:
2157+
msg["uuid"] = _stringify_event_uuid(uuid)
2158+
except ValueError as e:
2159+
self.log.error("%s Falling back to a generated UUID.", e)
2160+
2161+
if "uuid" not in msg:
2162+
# Always send a uuid, so we can always return one
2163+
msg["uuid"] = stringify_id(uuid4())
2164+
21502165
def _enqueue(self, msg, disable_geoip, lane=None, property_allowlist=None):
21512166
# type: (...) -> Optional[str]
21522167
"""Push a new `msg` onto a lane's queue (analytics when unspecified), return the event uuid or None."""
@@ -2165,19 +2180,7 @@ def _enqueue(self, msg, disable_geoip, lane=None, property_allowlist=None):
21652180
timestamp = guess_timezone(timestamp)
21662181
msg["timestamp"] = timestamp.isoformat()
21672182

2168-
if "uuid" in msg:
2169-
uuid = msg.pop("uuid")
2170-
if uuid is not None:
2171-
try:
2172-
msg["uuid"] = _stringify_event_uuid(uuid)
2173-
except ValueError as e:
2174-
self.log.error("%s Falling back to a generated UUID.", e)
2175-
2176-
if "uuid" not in msg:
2177-
# Always send a uuid, so we can always return one
2178-
msg["uuid"] = stringify_id(uuid4())
2179-
2180-
sent_uuid = msg["uuid"]
2183+
self._normalize_event_uuid(msg)
21812184

21822185
if not msg.get("properties"):
21832186
msg["properties"] = {}
@@ -2223,6 +2226,11 @@ def _enqueue(self, msg, disable_geoip, lane=None, property_allowlist=None):
22232226
self.log.exception(f"Error in before_send callback: {e}")
22242227
# Continue with the original message if callback fails
22252228

2229+
# Re-normalized after before_send, which may have replaced or removed
2230+
# msg["uuid"], so the returned uuid always matches the wire event.
2231+
self._normalize_event_uuid(msg)
2232+
sent_uuid = msg["uuid"]
2233+
22262234
self.log.debug("queueing: %s", msg)
22272235

22282236
# if send is False, return msg as if it was successfully queued, unless

‎posthog/test/test_ai_capture_lane.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,36 @@ def test_supplied_uuid_is_preserved_end_to_end(self):
558558
batch = mock_post.call_args.kwargs["batch"]
559559
self.assertEqual(batch[0]["uuid"], supplied_uuid)
560560

561+
def test_returned_uuid_reflects_before_send_replacement(self):
562+
replacement_uuid = str(uuid.uuid4())
563+
564+
def replace_uuid(event):
565+
event["uuid"] = replacement_uuid
566+
return event
567+
568+
client = self._client(before_send=replace_uuid)
569+
with mock.patch("posthog.consumer.batch_post") as mock_post:
570+
returned_uuid = client.capture_ai("$ai_generation", distinct_id="d")
571+
client.flush()
572+
573+
self.assertEqual(returned_uuid, replacement_uuid)
574+
batch = mock_post.call_args.kwargs["batch"]
575+
self.assertEqual(batch[0]["uuid"], replacement_uuid)
576+
577+
def test_returned_uuid_is_regenerated_when_before_send_removes_it(self):
578+
def drop_uuid(event):
579+
del event["uuid"]
580+
return event
581+
582+
client = self._client(before_send=drop_uuid)
583+
with mock.patch("posthog.consumer.batch_post") as mock_post:
584+
returned_uuid = client.capture_ai("$ai_generation", distinct_id="d")
585+
client.flush()
586+
587+
self.assertIsNotNone(returned_uuid)
588+
batch = mock_post.call_args.kwargs["batch"]
589+
self.assertEqual(batch[0]["uuid"], returned_uuid)
590+
561591

562592
class TestCaptureAiPrivacyMode(unittest.TestCase):
563593
"""Privacy mode always wins over `enable_full_ai_capture`."""

‎posthog/test/test_before_send.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,53 @@ def my_before_send(event):
6767
self.assertEqual(len(processed_events), 1)
6868
self.assertEqual(processed_events[0]["event"], "test_event")
6969

70+
def test_before_send_callback_replacing_uuid_changes_the_returned_uuid(self):
71+
"""capture()'s return value must match the uuid on the wire event."""
72+
replacement_uuid = "12345678-1234-5678-1234-567812345678"
73+
74+
def replace_uuid(event):
75+
event["uuid"] = replacement_uuid
76+
return event
77+
78+
with mock.patch("posthog.client.batch_post") as mock_post:
79+
client = Client(
80+
FAKE_TEST_API_KEY,
81+
on_error=self.set_fail,
82+
before_send=replace_uuid,
83+
sync_mode=True,
84+
)
85+
msg_uuid = client.capture("test_event", distinct_id="user1")
86+
87+
self.assertEqual(msg_uuid, replacement_uuid)
88+
89+
mock_post.assert_called_once()
90+
batch_data = mock_post.call_args[1]["batch"]
91+
enqueued_msg = batch_data[0]
92+
self.assertEqual(enqueued_msg["uuid"], replacement_uuid)
93+
94+
def test_before_send_callback_removing_uuid_regenerates_it(self):
95+
"""If before_send drops the uuid, a fresh one is generated and returned."""
96+
97+
def remove_uuid(event):
98+
del event["uuid"]
99+
return event
100+
101+
with mock.patch("posthog.client.batch_post") as mock_post:
102+
client = Client(
103+
FAKE_TEST_API_KEY,
104+
on_error=self.set_fail,
105+
before_send=remove_uuid,
106+
sync_mode=True,
107+
)
108+
msg_uuid = client.capture("test_event", distinct_id="user1")
109+
110+
self.assertIsNotNone(msg_uuid)
111+
112+
mock_post.assert_called_once()
113+
batch_data = mock_post.call_args[1]["batch"]
114+
enqueued_msg = batch_data[0]
115+
self.assertEqual(enqueued_msg["uuid"], msg_uuid)
116+
70117
def test_before_send_callback_drops_event(self):
71118
"""Test that before_send callback can drop events by returning None."""
72119

0 commit comments

Comments
 (0)