Compare commits

...
Author SHA1 Message Date
dylan 58c63efac9 initial commit 2024-12-10 16:22:18 -08:00
7 changed files with 192 additions and 3 deletions
+4
View File
@@ -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`.
+1 -1
View File
@@ -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
View File
@@ -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
+65
View File
@@ -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`"""
+20
View File
@@ -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()
+58
View File
@@ -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
View File
@@ -1,4 +1,4 @@
VERSION = "3.7.4"
VERSION = "3.7.5"
if __name__ == "__main__":
print(VERSION, end="") # noqa: T201