Skip to content

Commit 1adf542

Browse files
authored
fix: Drop events when before_send callbacks raise (#880)
fix: drop events when before_send raises
1 parent 2f36c53 commit 1adf542

3 files changed

Lines changed: 53 additions & 14 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
pypi/posthog: patch
3+
---
4+
5+
Drop events when before_send callbacks raise exceptions

posthog/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2303,7 +2303,7 @@ def _enqueue(self, msg, disable_geoip, lane=None, property_allowlist=None):
23032303
msg = clean(modified_msg)
23042304
except Exception as e:
23052305
self.log.exception(f"Error in before_send callback: {e}")
2306-
# Continue with the original message if callback fails
2306+
return None
23072307

23082308
# Re-normalized after before_send, which may have replaced or removed
23092309
# msg["uuid"], so the returned uuid always matches the wire event.

posthog/test/test_before_send.py

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,11 @@ def drop_test_events(event):
145145
self.assertEqual(enqueued_msg["event"], "keep_me")
146146

147147
def test_before_send_callback_handles_exceptions(self):
148-
"""Test that exceptions in before_send don't crash the client."""
148+
"""Test that exceptions in before_send drop the event without crashing."""
149149

150150
def buggy_before_send(event):
151+
event["properties"]["partially_mutated"] = True
152+
event["uuid"] = "invalid"
151153
raise ValueError("Oops!")
152154

153155
with mock.patch("posthog.client.batch_post") as mock_post:
@@ -157,16 +159,49 @@ def buggy_before_send(event):
157159
before_send=buggy_before_send,
158160
sync_mode=True,
159161
)
160-
msg_uuid = client.capture("robust_event", distinct_id="user1")
162+
with (
163+
mock.patch.object(
164+
client,
165+
"_normalize_event_uuid",
166+
wraps=client._normalize_event_uuid,
167+
) as normalize_uuid,
168+
self.assertLogs("posthog", level="ERROR") as logs,
169+
):
170+
msg_uuid = client.capture("robust_event", distinct_id="user1")
161171

162-
# Event should still be sent despite the exception
163-
self.assertIsNotNone(msg_uuid)
172+
self.assertIsNone(msg_uuid)
173+
mock_post.assert_not_called()
174+
normalize_uuid.assert_called_once()
175+
self.assertIn("Error in before_send callback: Oops!", logs.output[0])
164176

165-
# Check the enqueued message
166-
mock_post.assert_called_once()
167-
batch_data = mock_post.call_args[1]["batch"]
168-
enqueued_msg = batch_data[0]
169-
self.assertEqual(enqueued_msg["event"], "robust_event")
177+
def test_before_send_callback_exception_does_not_enqueue_mutated_event(self):
178+
def buggy_before_send(event):
179+
event["properties"]["partially_mutated"] = True
180+
raise ValueError("Oops!")
181+
182+
client = Client(
183+
FAKE_TEST_API_KEY,
184+
on_error=self.set_fail,
185+
before_send=buggy_before_send,
186+
flush_at=1,
187+
)
188+
try:
189+
with (
190+
mock.patch("posthog.consumer.batch_post") as mock_post,
191+
mock.patch.object(
192+
client._analytics_lane,
193+
"enqueue",
194+
wraps=client._analytics_lane.enqueue,
195+
) as enqueue,
196+
self.assertLogs("posthog", level="ERROR"),
197+
):
198+
msg_uuid = client.capture("robust_event", distinct_id="user1")
199+
200+
self.assertIsNone(msg_uuid)
201+
enqueue.assert_not_called()
202+
mock_post.assert_not_called()
203+
finally:
204+
client.shutdown()
170205

171206
def test_before_send_callback_output_is_recleaned(self):
172207
marker = object()
@@ -186,7 +221,7 @@ def add_unsupported_value(event):
186221
sent_event = mock_post.call_args.kwargs["batch"][0]
187222
self.assertIsNone(sent_event["properties"]["marker"])
188223

189-
def test_before_send_callback_non_dict_output_uses_original_event(self):
224+
def test_before_send_callback_non_dict_output_drops_event(self):
190225
with (
191226
mock.patch("posthog.client.batch_post") as mock_post,
192227
mock.patch("posthog.client.Client.log.exception") as mock_log,
@@ -196,10 +231,9 @@ def test_before_send_callback_non_dict_output_uses_original_event(self):
196231
before_send=lambda _event: "invalid",
197232
sync_mode=True,
198233
)
199-
self.assertIsNotNone(client.capture("original", distinct_id="user1"))
234+
self.assertIsNone(client.capture("original", distinct_id="user1"))
200235

201-
sent_event = mock_post.call_args.kwargs["batch"][0]
202-
self.assertEqual(sent_event["event"], "original")
236+
mock_post.assert_not_called()
203237
self.assertIn(
204238
"before_send must return a dict or None", mock_log.call_args.args[0]
205239
)

0 commit comments

Comments
 (0)