Skip to content

Commit 58c63ef

Browse files
committed
initial commit
1 parent 9e1bb8c commit 58c63ef

File tree

7 files changed

+192
-3
lines changed

7 files changed

+192
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.7.5 - 2024-12-11
2+
3+
1. Modify the SDK to use a Singleton factory pattern; warn on non-singleton instantiation.
4+
15
## 3.7.4 - 2024-11-25
26

37
1. Fix bug where this SDK incorrectly sent feature flag events with null values when calling `get_feature_flag_payload`.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,4 @@ Updated are released using GitHub Actions: after bumping `version.py` in `master
4343

4444
## Questions?
4545

46-
### [Join our Slack community.](https://join.slack.com/t/posthogusers/shared_invite/enQtOTY0MzU5NjAwMDY3LTc2MWQ0OTZlNjhkODk3ZDI3NDVjMDE1YjgxY2I4ZjI4MzJhZmVmNjJkN2NmMGJmMzc2N2U3Yjc3ZjI5NGFlZDQ)
46+
### [Check out our community page.](https://posthog.com/posts)

posthog/__init__.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,48 @@
1+
"""
2+
PostHog Python SDK - Main module for interacting with PostHog analytics.
3+
4+
This module provides the main interface for sending analytics data to PostHog.
5+
It includes functions for tracking events, identifying users, managing feature flags,
6+
and handling group analytics.
7+
8+
Basic usage:
9+
import posthog
10+
11+
# Configure the client
12+
posthog.api_key = 'your_api_key'
13+
14+
# Track an event
15+
posthog.capture('distinct_id', 'event_name')
16+
"""
117
import datetime # noqa: F401
218
from typing import Callable, Dict, List, Optional, Tuple # noqa: F401
319

420
from posthog.client import Client
521
from posthog.exception_capture import Integrations # noqa: F401
622
from posthog.version import VERSION
23+
from posthog.factory import PostHogFactory
724

825
__version__ = VERSION
926

10-
"""Settings."""
27+
"""Settings.
28+
These settings control the behavior of the PostHog client:
29+
30+
api_key: Your PostHog API key
31+
host: PostHog server URL (defaults to Cloud instance)
32+
debug: Enable debug logging
33+
send: Enable/disable sending events to PostHog
34+
sync_mode: Run in synchronous mode instead of async
35+
disabled: Completely disable the client
36+
personal_api_key: Personal API key for feature flag evaluation
37+
project_api_key: Project API key for direct feature flag access
38+
poll_interval: Interval in seconds between feature flag updates
39+
disable_geoip: Disable IP geolocation (recommended for server-side)
40+
feature_flags_request_timeout_seconds: Timeout for feature flag requests
41+
super_properties: Properties to be added to every event
42+
enable_exception_autocapture: (Alpha) Enable automatic exception capturing
43+
exception_autocapture_integrations: List of exception capture integrations
44+
project_root: Root directory for exception source mapping
45+
"""
1146
api_key = None # type: Optional[str]
1247
host = None # type: Optional[str]
1348
on_error = None # type: Optional[Callable]
@@ -521,5 +556,12 @@ def _proxy(method, *args, **kwargs):
521556
return fn(*args, **kwargs)
522557

523558

559+
# For backwards compatibility with older versions of the SDK.
560+
# This class is deprecated and will be removed in a future version.
524561
class Posthog(Client):
525562
pass
563+
564+
# The recommended way to create and manage PostHog client instances.
565+
# These factory methods ensure proper singleton management and configuration.
566+
create_posthog_client = PostHogFactory.create # Create a new PostHog client instance
567+
get_posthog_client = PostHogFactory.get_instance # Get the existing client instance

posthog/client.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,23 @@
3030

3131
class Client(object):
3232
"""Create a new PostHog client."""
33+
_instance = None
34+
_enforce_singleton = True # Can be disabled for testing
35+
36+
def __new__(cls, *args, **kwargs):
37+
if cls._enforce_singleton:
38+
if not cls._instance:
39+
cls._instance = super(Client, cls).__new__(cls)
40+
# Move initialization flag to __new__ since it needs to exist
41+
# before __init__ is called
42+
cls._instance._initialized = False
43+
return cls._instance
44+
# For non-singleton case (tests), still need to set _initialized
45+
instance = super(Client, cls).__new__(cls)
46+
instance._initialized = False
47+
return instance
48+
49+
3350

3451
log = logging.getLogger("posthog")
3552

@@ -60,6 +77,11 @@ def __init__(
6077
exception_autocapture_integrations=None,
6178
project_root=None,
6279
):
80+
if self._initialized:
81+
self._warn_multiple_initialization()
82+
return
83+
84+
self._initialized = True
6385
self.queue = queue.Queue(max_queue_size)
6486

6587
# api_key: This should be the Team API Key (token), public
@@ -925,6 +947,49 @@ def _add_local_person_and_group_properties(self, distinct_id, groups, person_pro
925947

926948
return all_person_properties, all_group_properties
927949

950+
def _warn_multiple_initialization(self):
951+
self.log.warning(
952+
"Warning: Attempting to create multiple PostHog client instances. "
953+
"PostHog client should be used as a singleton. "
954+
"The existing instance will be reused instead of creating a new one. "
955+
"Consider using PostHog.get_instance() to access the client."
956+
)
957+
958+
959+
@classmethod
960+
def get_instance(cls):
961+
"""
962+
Get the singleton instance of the PostHog client.
963+
964+
This method returns the existing PostHog client instance that was previously
965+
initialized. It ensures only one client instance exists throughout your application.
966+
967+
Returns:
968+
Client: The singleton PostHog client instance
969+
970+
Raises:
971+
RuntimeError: If no PostHog client has been initialized yet
972+
973+
Example:
974+
```python
975+
# First, initialize the client
976+
posthog.create_posthog_client('api_key', host='https://app.posthog.com')
977+
978+
# Later, get the same instance
979+
client = posthog.get_posthog_client()
980+
client.capture('user_id', 'event_name')
981+
```
982+
983+
Note:
984+
Make sure to initialize a client with `create_posthog_client()` or
985+
`Client(api_key, ...)` before calling this method.
986+
"""
987+
if not cls._instance:
988+
raise RuntimeError(
989+
"PostHog client has not been initialized. "
990+
"Please create an instance with Client(api_key, ...) first."
991+
)
992+
return cls._instance
928993

929994
def require(name, field, data_type):
930995
"""Require that the named `field` has the right `data_type`"""

posthog/factory.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from posthog.client import Client
2+
3+
class PostHogFactory:
4+
@staticmethod
5+
def create(
6+
api_key=None,
7+
host=None,
8+
**kwargs
9+
):
10+
"""
11+
Create a new PostHog client instance or return the existing one.
12+
"""
13+
return Client(api_key=api_key, host=host, **kwargs)
14+
15+
@staticmethod
16+
def get_instance():
17+
"""
18+
Get the existing PostHog client instance.
19+
"""
20+
return Client.get_instance()

posthog/test/test_client.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def set_fail(self, e, batch):
3333
def setUp(self):
3434
self.failed = False
3535
self.client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail)
36+
Client._enforce_singleton = False # Disable singleton for tests
3637

3738
def test_requires_api_key(self):
3839
self.assertRaises(AssertionError, Client)
@@ -159,6 +160,7 @@ def test_basic_capture_exception_with_correct_host_generation(self):
159160

160161
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
161162
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, host="https://aloha.com")
163+
print(client.host)
162164
exception = Exception("test exception")
163165
client.capture_exception(exception, "distinct_id")
164166

@@ -187,6 +189,7 @@ def test_basic_capture_exception_with_correct_host_generation_for_server_hosts(s
187189

188190
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
189191
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, host="https://app.posthog.com")
192+
print(client.host)
190193
exception = Exception("test exception")
191194
client.capture_exception(exception, "distinct_id")
192195

@@ -1073,3 +1076,58 @@ def test_default_properties_get_added_properly(self, patch_decide):
10731076
group_properties={},
10741077
disable_geoip=False,
10751078
)
1079+
1080+
def test_singleton_behavior(self):
1081+
# Reset singleton state
1082+
Client._instance = None
1083+
Client._enforce_singleton = True
1084+
1085+
# Create first instance
1086+
client1 = Client(FAKE_TEST_API_KEY, host="https://host1.com")
1087+
1088+
# Create second instance with different params
1089+
client2 = Client(FAKE_TEST_API_KEY, host="https://host2.com")
1090+
1091+
# Both should reference the same instance
1092+
self.assertIs(client1, client2)
1093+
1094+
# Host should be from first initialization
1095+
self.assertEqual(client1.host, "https://host1.com")
1096+
self.assertEqual(client2.host, "https://host1.com")
1097+
1098+
def test_singleton_disabled_for_testing(self):
1099+
# Reset singleton state
1100+
Client._instance = None
1101+
Client._enforce_singleton = False
1102+
1103+
# Create instances with different params
1104+
client1 = Client(FAKE_TEST_API_KEY, host="https://host1.com")
1105+
client2 = Client(FAKE_TEST_API_KEY, host="https://host2.com")
1106+
1107+
# Should be different instances
1108+
self.assertIsNot(client1, client2)
1109+
1110+
# Each should maintain their own host
1111+
self.assertEqual(client1.host, "https://host1.com")
1112+
self.assertEqual(client2.host, "https://host2.com")
1113+
1114+
def test_singleton_warning_on_multiple_initialization(self):
1115+
# Reset singleton state
1116+
Client._instance = None
1117+
Client._enforce_singleton = True
1118+
1119+
# Create first instance
1120+
client1 = Client(FAKE_TEST_API_KEY)
1121+
1122+
# Second initialization should log warning
1123+
with self.assertLogs("posthog", level="WARNING") as logs:
1124+
client2 = Client(FAKE_TEST_API_KEY)
1125+
self.assertEqual(
1126+
logs.output[0],
1127+
"WARNING:posthog:Warning: Attempting to create multiple PostHog client instances. "
1128+
"PostHog client should be used as a singleton. "
1129+
"The existing instance will be reused instead of creating a new one. "
1130+
"Consider using PostHog.get_instance() to access the client."
1131+
)
1132+
1133+

posthog/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION = "3.7.4"
1+
VERSION = "3.7.5"
22

33
if __name__ == "__main__":
44
print(VERSION, end="") # noqa: T201

0 commit comments

Comments
 (0)