Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .sampo/changesets/noble-iceseeker-aurelien.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
pypi/posthog: patch
---

Fix module-level settings propagation to the default client
20 changes: 15 additions & 5 deletions posthog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,10 @@ def get_tags() -> Dict[str, Any]:
the corresponding constructor arguments.

Attributes:
api_key: Project API key/token used by the global client. Missing or blank
values create a disabled no-op global client.
api_key: Deprecated legacy alias for ``project_api_key``.
project_api_key: Preferred project API key setting. It takes precedence when
both it and ``api_key`` are configured; missing or blank values create a
disabled no-op global client.
host: PostHog ingestion host. Defaults to the US ingestion endpoint when not
set.
on_error: Optional callback invoked by background consumers when event upload
Expand Down Expand Up @@ -335,6 +337,8 @@ def get_tags() -> Dict[str, Any]:
call if the metrics API hasn't been used yet.
enable_exception_autocapture: Automatically capture uncaught exceptions.
log_captured_exceptions: Also log exceptions captured by error tracking.
project_root: Root path used to determine in-app exception stack frames.
privacy_mode: Capture AI usage metadata without prompt inputs or outputs.
before_send: Optional callback that can modify or drop events before upload.
Return ``None`` to drop an event.
enable_local_evaluation: Whether to poll feature flag definitions for local
Expand Down Expand Up @@ -362,6 +366,7 @@ def get_tags() -> Dict[str, Any]:
exception_autocapture_refill_interval_seconds: Seconds between token refills
for autocaptured exception rate limiting.
"""
# Deprecated legacy alias for project_api_key. Kept for backwards compatibility.
api_key = None # type: Optional[str]
host = None # type: Optional[str]
on_error = None # type: Optional[Callable]
Expand All @@ -371,6 +376,7 @@ def get_tags() -> Dict[str, Any]:
disabled = False # type: bool
secret_key = None # type: Optional[str]
personal_api_key = None # type: Optional[str] # Deprecated: use secret_key
# Preferred project token setting; takes precedence over the legacy api_key alias.
project_api_key = None # type: Optional[str]
poll_interval = 30 # type: int
disable_geoip = True # type: bool
Expand Down Expand Up @@ -1157,15 +1163,16 @@ def setup() -> Client:
``setup()`` is called automatically by global APIs such as ``capture()``.

Returns:
The global ``Client`` instance. If ``api_key`` is missing or blank,
the client is disabled and module-level calls become no-ops.
The global ``Client`` instance. If both ``api_key`` and
``project_api_key`` are missing or blank, the client is disabled and
module-level calls become no-ops.

Category:
Initialization
"""
global default_client
if not default_client:
configured_api_key = api_key.strip() if api_key else ""
configured_api_key = (project_api_key or api_key or "").strip()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Blank preferred key disables capture

When project_api_key contains only whitespace and api_key is valid, this expression selects the truthy preferred key before trimming it, producing an empty configured key and disabling the default client, so module-level events are silently dropped.

Knowledge Base Used: Client Core

Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/__init__.py
Line: 1175

Comment:
**Blank preferred key disables capture**

When `project_api_key` contains only whitespace and `api_key` is valid, this expression selects the truthy preferred key before trimming it, producing an empty configured key and disabling the default client, so module-level events are silently dropped.

**Knowledge Base Used:** [Client Core](https://app.greptile.com/posthog-org-19734/-/custom-context/knowledge-base/posthog/posthog-python/-/docs/client-core.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

default_client = Client(
configured_api_key,
host=host,
Expand All @@ -1188,6 +1195,8 @@ def setup() -> Client:
# or deprecate this proxy option fully (it's already in the process of deprecation, no new clients should be using this method since like 5-6 months)
enable_exception_autocapture=enable_exception_autocapture,
log_captured_exceptions=log_captured_exceptions,
project_root=project_root,
privacy_mode=privacy_mode,
before_send=before_send,
enable_local_evaluation=enable_local_evaluation,
flag_definition_cache_provider=flag_definition_cache_provider,
Expand All @@ -1208,6 +1217,7 @@ def setup() -> Client:
# for API keys that become empty after trimming.
default_client.disabled = disabled or not default_client.api_key
default_client.debug = debug
default_client.privacy_mode = bool(privacy_mode)
default_client._set_before_send(before_send)
default_client._use_ai_lane = bool(_use_ai_lane)
default_client._enable_multimodal_capture = bool(_enable_multimodal_capture)
Expand Down
47 changes: 47 additions & 0 deletions posthog/test/test_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,25 @@ class TestModuleLevelSetup(unittest.TestCase):
def setUp(self):
self._original_default_client = posthog.default_client
self._original_api_key = posthog.api_key
self._original_project_api_key = posthog.project_api_key
self._original_project_root = posthog.project_root
self._original_privacy_mode = posthog.privacy_mode
self._original_disabled = posthog.disabled
self._original_send = posthog.send
posthog.default_client = None
posthog.api_key = None
posthog.project_api_key = None
posthog.project_root = None
posthog.privacy_mode = False
posthog.disabled = False
posthog.send = False

def tearDown(self):
posthog.default_client = self._original_default_client
posthog.api_key = self._original_api_key
posthog.project_api_key = self._original_project_api_key
posthog.project_root = self._original_project_root
posthog.privacy_mode = self._original_privacy_mode
posthog.disabled = self._original_disabled
posthog.send = self._original_send

Expand All @@ -77,6 +87,43 @@ def test_setup_configures_disabled_client_when_api_key_is_blank(
self.assertTrue(client.disabled)
self.assertIsNone(posthog.capture("Module Python Event", distinct_id="john"))

def test_setup_uses_project_api_key_when_api_key_is_unset(self):
posthog.project_api_key = " phc_project_key "

client = posthog.setup()

self.assertEqual(client.api_key, "phc_project_key")
self.assertFalse(client.disabled)

def test_setup_prefers_project_api_key_over_api_key(self):
posthog.api_key = "phc_api_key"
posthog.project_api_key = "phc_project_key"

client = posthog.setup()

self.assertEqual(client.api_key, "phc_project_key")

def test_setup_propagates_project_root(self):
posthog.api_key = "phc_test"
posthog.project_root = "/path/to/project"

client = posthog.setup()

self.assertEqual(client.project_root, "/path/to/project")

def test_setup_propagates_and_updates_privacy_mode(self):
posthog.api_key = "phc_test"
posthog.privacy_mode = True

client = posthog.setup()

self.assertTrue(client.privacy_mode)

posthog.privacy_mode = False

self.assertIs(posthog.setup(), client)
self.assertFalse(client.privacy_mode)


class TestModuleLevelWrappers(unittest.TestCase):
"""Test that module-level wrapper functions in posthog/__init__.py
Expand Down
Loading