Skip to content

Commit e868e23

Browse files
authored
fix: Prevent core Client methods from raising exceptions (#327)
* fix: Prevent core Client methods from raising exceptions The goal is to ensure that our client doesn't cause a panic in an end-user application. This change updates capture/set/set_once/group_identify/alias to swallow and log any exceptions that occur. Note that this won't prevent errors from propagating via the `on_error` callback if an error occurs while processing the queue. * test: Remove assertions that capture raises These tests were broken anyways. Capture would only raise because it was being called with no arguments, not because api_key or host are None.
1 parent 0bb6342 commit e868e23

File tree

3 files changed

+85
-9
lines changed

3 files changed

+85
-9
lines changed

posthog/client.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import os
44
import sys
55
from datetime import datetime, timedelta
6-
from typing import Any, Dict, Optional, Union
6+
from typing import Any, Callable, Dict, Optional, Union
77
from typing_extensions import Unpack
88
from uuid import uuid4
99

@@ -99,6 +99,34 @@ def add_context_tags(properties):
9999
return properties
100100

101101

102+
def no_throw(default_return=None):
103+
"""
104+
Decorator to prevent raising exceptions from public API methods.
105+
Note that this doesn't prevent errors from propagating via `on_error`.
106+
Exceptions will still be raised if the debug flag is enabled.
107+
108+
Args:
109+
default_return: Value to return on exception (default: None)
110+
"""
111+
112+
def decorator(func):
113+
from functools import wraps
114+
115+
@wraps(func)
116+
def wrapper(self, *args, **kwargs):
117+
try:
118+
return func(self, *args, **kwargs)
119+
except Exception as e:
120+
if self.debug:
121+
raise e
122+
self.log.exception(f"Error in {func.__name__}: {e}")
123+
return default_return
124+
125+
return wrapper
126+
127+
return decorator
128+
129+
102130
class Client(object):
103131
"""
104132
This is the SDK reference for the PostHog Python SDK.
@@ -481,6 +509,7 @@ def get_flags_decision(
481509

482510
return normalize_flags_response(resp_data)
483511

512+
@no_throw()
484513
def capture(
485514
self, event: str, **kwargs: Unpack[OptionalCaptureArgs]
486515
) -> Optional[str]:
@@ -657,6 +686,7 @@ def _parse_send_feature_flags(self, send_feature_flags) -> SendFeatureFlagsOptio
657686
f"Expected bool or dict."
658687
)
659688

689+
@no_throw()
660690
def set(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
661691
"""
662692
Set properties on a person profile.
@@ -690,6 +720,8 @@ def set(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
690720
691721
Category:
692722
Identification
723+
724+
Note: This method will not raise exceptions. Errors are logged.
693725
"""
694726
distinct_id = kwargs.get("distinct_id", None)
695727
properties = kwargs.get("properties", None)
@@ -716,6 +748,7 @@ def set(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
716748

717749
return self._enqueue(msg, disable_geoip)
718750

751+
@no_throw()
719752
def set_once(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
720753
"""
721754
Set properties on a person profile only if they haven't been set before.
@@ -734,6 +767,8 @@ def set_once(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
734767
735768
Category:
736769
Identification
770+
771+
Note: This method will not raise exceptions. Errors are logged.
737772
"""
738773
distinct_id = kwargs.get("distinct_id", None)
739774
properties = kwargs.get("properties", None)
@@ -759,6 +794,7 @@ def set_once(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
759794

760795
return self._enqueue(msg, disable_geoip)
761796

797+
@no_throw()
762798
def group_identify(
763799
self,
764800
group_type: str,
@@ -791,6 +827,8 @@ def group_identify(
791827
792828
Category:
793829
Identification
830+
831+
Note: This method will not raise exceptions. Errors are logged.
794832
"""
795833
properties = properties or {}
796834

@@ -815,6 +853,7 @@ def group_identify(
815853

816854
return self._enqueue(msg, disable_geoip)
817855

856+
@no_throw()
818857
def alias(
819858
self,
820859
previous_id: str,
@@ -840,6 +879,8 @@ def alias(
840879
841880
Category:
842881
Identification
882+
883+
Note: This method will not raise exceptions. Errors are logged.
843884
"""
844885
(distinct_id, personless) = get_identity_state(distinct_id)
845886

posthog/test/test_client.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2423,3 +2423,46 @@ def test_context_tags_added(self):
24232423
batch_data = mock_post.call_args[1]["batch"]
24242424
msg = batch_data[0]
24252425
self.assertEqual(msg["properties"]["$context_tags"], ["random_tag"])
2426+
2427+
@mock.patch(
2428+
"posthog.client.Client._enqueue", side_effect=Exception("Unexpected error")
2429+
)
2430+
def test_methods_handle_exceptions(self, mock_enqueue):
2431+
"""Test that all decorated methods handle exceptions gracefully."""
2432+
client = Client("test-key")
2433+
2434+
test_cases = [
2435+
("capture", ["test_event"], {}),
2436+
("set", [], {"distinct_id": "some-id", "properties": {"a": "b"}}),
2437+
("set_once", [], {"distinct_id": "some-id", "properties": {"a": "b"}}),
2438+
("group_identify", ["group-type", "group-key"], {}),
2439+
("alias", ["some-id", "new-id"], {}),
2440+
]
2441+
2442+
for method_name, args, kwargs in test_cases:
2443+
with self.subTest(method=method_name):
2444+
method = getattr(client, method_name)
2445+
result = method(*args, **kwargs)
2446+
self.assertEqual(result, None)
2447+
2448+
@mock.patch(
2449+
"posthog.client.Client._enqueue", side_effect=Exception("Expected error")
2450+
)
2451+
def test_debug_flag_re_raises_exceptions(self, mock_enqueue):
2452+
"""Test that methods re-raise exceptions when debug=True."""
2453+
client = Client("test-key", debug=True)
2454+
2455+
test_cases = [
2456+
("capture", ["test_event"], {}),
2457+
("set", [], {"distinct_id": "some-id", "properties": {"a": "b"}}),
2458+
("set_once", [], {"distinct_id": "some-id", "properties": {"a": "b"}}),
2459+
("group_identify", ["group-type", "group-key"], {}),
2460+
("alias", ["some-id", "new-id"], {}),
2461+
]
2462+
2463+
for method_name, args, kwargs in test_cases:
2464+
with self.subTest(method=method_name):
2465+
method = getattr(client, method_name)
2466+
with self.assertRaises(Exception) as cm:
2467+
method(*args, **kwargs)
2468+
self.assertEqual(str(cm.exception), "Expected error")

posthog/test/test_module.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,6 @@ def setUp(self):
1818
"testsecret", host="http://localhost:8000", on_error=self.failed
1919
)
2020

21-
def test_no_api_key(self):
22-
self.posthog.api_key = None
23-
self.assertRaises(Exception, self.posthog.capture)
24-
25-
def test_no_host(self):
26-
self.posthog.host = None
27-
self.assertRaises(Exception, self.posthog.capture)
28-
2921
def test_track(self):
3022
res = self.posthog.capture("python module event", distinct_id="distinct_id")
3123
self._assert_enqueue_result(res)

0 commit comments

Comments
 (0)