Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58c63efac9 |
@@ -1,3 +1,7 @@
|
||||
## 3.7.5 - 2024-12-11
|
||||
|
||||
1. Modify the SDK to use a Singleton factory pattern; warn on non-singleton instantiation.
|
||||
|
||||
## 3.7.4 - 2024-11-25
|
||||
|
||||
1. Fix bug where this SDK incorrectly sent feature flag events with null values when calling `get_feature_flag_payload`.
|
||||
|
||||
@@ -43,4 +43,4 @@ Updated are released using GitHub Actions: after bumping `version.py` in `master
|
||||
|
||||
## Questions?
|
||||
|
||||
### [Join our Slack community.](https://join.slack.com/t/posthogusers/shared_invite/enQtOTY0MzU5NjAwMDY3LTc2MWQ0OTZlNjhkODk3ZDI3NDVjMDE1YjgxY2I4ZjI4MzJhZmVmNjJkN2NmMGJmMzc2N2U3Yjc3ZjI5NGFlZDQ)
|
||||
### [Check out our community page.](https://posthog.com/posts)
|
||||
|
||||
+43
-1
@@ -1,13 +1,48 @@
|
||||
"""
|
||||
PostHog Python SDK - Main module for interacting with PostHog analytics.
|
||||
|
||||
This module provides the main interface for sending analytics data to PostHog.
|
||||
It includes functions for tracking events, identifying users, managing feature flags,
|
||||
and handling group analytics.
|
||||
|
||||
Basic usage:
|
||||
import posthog
|
||||
|
||||
# Configure the client
|
||||
posthog.api_key = 'your_api_key'
|
||||
|
||||
# Track an event
|
||||
posthog.capture('distinct_id', 'event_name')
|
||||
"""
|
||||
import datetime # noqa: F401
|
||||
from typing import Callable, Dict, List, Optional, Tuple # noqa: F401
|
||||
|
||||
from posthog.client import Client
|
||||
from posthog.exception_capture import Integrations # noqa: F401
|
||||
from posthog.version import VERSION
|
||||
from posthog.factory import PostHogFactory
|
||||
|
||||
__version__ = VERSION
|
||||
|
||||
"""Settings."""
|
||||
"""Settings.
|
||||
These settings control the behavior of the PostHog client:
|
||||
|
||||
api_key: Your PostHog API key
|
||||
host: PostHog server URL (defaults to Cloud instance)
|
||||
debug: Enable debug logging
|
||||
send: Enable/disable sending events to PostHog
|
||||
sync_mode: Run in synchronous mode instead of async
|
||||
disabled: Completely disable the client
|
||||
personal_api_key: Personal API key for feature flag evaluation
|
||||
project_api_key: Project API key for direct feature flag access
|
||||
poll_interval: Interval in seconds between feature flag updates
|
||||
disable_geoip: Disable IP geolocation (recommended for server-side)
|
||||
feature_flags_request_timeout_seconds: Timeout for feature flag requests
|
||||
super_properties: Properties to be added to every event
|
||||
enable_exception_autocapture: (Alpha) Enable automatic exception capturing
|
||||
exception_autocapture_integrations: List of exception capture integrations
|
||||
project_root: Root directory for exception source mapping
|
||||
"""
|
||||
api_key = None # type: Optional[str]
|
||||
host = None # type: Optional[str]
|
||||
on_error = None # type: Optional[Callable]
|
||||
@@ -521,5 +556,12 @@ def _proxy(method, *args, **kwargs):
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
|
||||
# For backwards compatibility with older versions of the SDK.
|
||||
# This class is deprecated and will be removed in a future version.
|
||||
class Posthog(Client):
|
||||
pass
|
||||
|
||||
# The recommended way to create and manage PostHog client instances.
|
||||
# These factory methods ensure proper singleton management and configuration.
|
||||
create_posthog_client = PostHogFactory.create # Create a new PostHog client instance
|
||||
get_posthog_client = PostHogFactory.get_instance # Get the existing client instance
|
||||
|
||||
@@ -30,6 +30,23 @@ MAX_DICT_SIZE = 50_000
|
||||
|
||||
class Client(object):
|
||||
"""Create a new PostHog client."""
|
||||
_instance = None
|
||||
_enforce_singleton = True # Can be disabled for testing
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._enforce_singleton:
|
||||
if not cls._instance:
|
||||
cls._instance = super(Client, cls).__new__(cls)
|
||||
# Move initialization flag to __new__ since it needs to exist
|
||||
# before __init__ is called
|
||||
cls._instance._initialized = False
|
||||
return cls._instance
|
||||
# For non-singleton case (tests), still need to set _initialized
|
||||
instance = super(Client, cls).__new__(cls)
|
||||
instance._initialized = False
|
||||
return instance
|
||||
|
||||
|
||||
|
||||
log = logging.getLogger("posthog")
|
||||
|
||||
@@ -60,6 +77,11 @@ class Client(object):
|
||||
exception_autocapture_integrations=None,
|
||||
project_root=None,
|
||||
):
|
||||
if self._initialized:
|
||||
self._warn_multiple_initialization()
|
||||
return
|
||||
|
||||
self._initialized = True
|
||||
self.queue = queue.Queue(max_queue_size)
|
||||
|
||||
# api_key: This should be the Team API Key (token), public
|
||||
@@ -925,6 +947,49 @@ class Client(object):
|
||||
|
||||
return all_person_properties, all_group_properties
|
||||
|
||||
def _warn_multiple_initialization(self):
|
||||
self.log.warning(
|
||||
"Warning: Attempting to create multiple PostHog client instances. "
|
||||
"PostHog client should be used as a singleton. "
|
||||
"The existing instance will be reused instead of creating a new one. "
|
||||
"Consider using PostHog.get_instance() to access the client."
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""
|
||||
Get the singleton instance of the PostHog client.
|
||||
|
||||
This method returns the existing PostHog client instance that was previously
|
||||
initialized. It ensures only one client instance exists throughout your application.
|
||||
|
||||
Returns:
|
||||
Client: The singleton PostHog client instance
|
||||
|
||||
Raises:
|
||||
RuntimeError: If no PostHog client has been initialized yet
|
||||
|
||||
Example:
|
||||
```python
|
||||
# First, initialize the client
|
||||
posthog.create_posthog_client('api_key', host='https://app.posthog.com')
|
||||
|
||||
# Later, get the same instance
|
||||
client = posthog.get_posthog_client()
|
||||
client.capture('user_id', 'event_name')
|
||||
```
|
||||
|
||||
Note:
|
||||
Make sure to initialize a client with `create_posthog_client()` or
|
||||
`Client(api_key, ...)` before calling this method.
|
||||
"""
|
||||
if not cls._instance:
|
||||
raise RuntimeError(
|
||||
"PostHog client has not been initialized. "
|
||||
"Please create an instance with Client(api_key, ...) first."
|
||||
)
|
||||
return cls._instance
|
||||
|
||||
def require(name, field, data_type):
|
||||
"""Require that the named `field` has the right `data_type`"""
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from posthog.client import Client
|
||||
|
||||
class PostHogFactory:
|
||||
@staticmethod
|
||||
def create(
|
||||
api_key=None,
|
||||
host=None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Create a new PostHog client instance or return the existing one.
|
||||
"""
|
||||
return Client(api_key=api_key, host=host, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def get_instance():
|
||||
"""
|
||||
Get the existing PostHog client instance.
|
||||
"""
|
||||
return Client.get_instance()
|
||||
@@ -33,6 +33,7 @@ class TestClient(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.failed = False
|
||||
self.client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail)
|
||||
Client._enforce_singleton = False # Disable singleton for tests
|
||||
|
||||
def test_requires_api_key(self):
|
||||
self.assertRaises(AssertionError, Client)
|
||||
@@ -159,6 +160,7 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, host="https://aloha.com")
|
||||
print(client.host)
|
||||
exception = Exception("test exception")
|
||||
client.capture_exception(exception, "distinct_id")
|
||||
|
||||
@@ -187,6 +189,7 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, host="https://app.posthog.com")
|
||||
print(client.host)
|
||||
exception = Exception("test exception")
|
||||
client.capture_exception(exception, "distinct_id")
|
||||
|
||||
@@ -1073,3 +1076,58 @@ class TestClient(unittest.TestCase):
|
||||
group_properties={},
|
||||
disable_geoip=False,
|
||||
)
|
||||
|
||||
def test_singleton_behavior(self):
|
||||
# Reset singleton state
|
||||
Client._instance = None
|
||||
Client._enforce_singleton = True
|
||||
|
||||
# Create first instance
|
||||
client1 = Client(FAKE_TEST_API_KEY, host="https://host1.com")
|
||||
|
||||
# Create second instance with different params
|
||||
client2 = Client(FAKE_TEST_API_KEY, host="https://host2.com")
|
||||
|
||||
# Both should reference the same instance
|
||||
self.assertIs(client1, client2)
|
||||
|
||||
# Host should be from first initialization
|
||||
self.assertEqual(client1.host, "https://host1.com")
|
||||
self.assertEqual(client2.host, "https://host1.com")
|
||||
|
||||
def test_singleton_disabled_for_testing(self):
|
||||
# Reset singleton state
|
||||
Client._instance = None
|
||||
Client._enforce_singleton = False
|
||||
|
||||
# Create instances with different params
|
||||
client1 = Client(FAKE_TEST_API_KEY, host="https://host1.com")
|
||||
client2 = Client(FAKE_TEST_API_KEY, host="https://host2.com")
|
||||
|
||||
# Should be different instances
|
||||
self.assertIsNot(client1, client2)
|
||||
|
||||
# Each should maintain their own host
|
||||
self.assertEqual(client1.host, "https://host1.com")
|
||||
self.assertEqual(client2.host, "https://host2.com")
|
||||
|
||||
def test_singleton_warning_on_multiple_initialization(self):
|
||||
# Reset singleton state
|
||||
Client._instance = None
|
||||
Client._enforce_singleton = True
|
||||
|
||||
# Create first instance
|
||||
client1 = Client(FAKE_TEST_API_KEY)
|
||||
|
||||
# Second initialization should log warning
|
||||
with self.assertLogs("posthog", level="WARNING") as logs:
|
||||
client2 = Client(FAKE_TEST_API_KEY)
|
||||
self.assertEqual(
|
||||
logs.output[0],
|
||||
"WARNING:posthog:Warning: Attempting to create multiple PostHog client instances. "
|
||||
"PostHog client should be used as a singleton. "
|
||||
"The existing instance will be reused instead of creating a new one. "
|
||||
"Consider using PostHog.get_instance() to access the client."
|
||||
)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
VERSION = "3.7.4"
|
||||
VERSION = "3.7.5"
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(VERSION, end="") # noqa: T201
|
||||
|
||||
Reference in New Issue
Block a user